//go:build linux package ike import ( "context" "errors" "net" "os" "os/exec" "strconv" "strings" "testing" "time" ) func TestValidateUserspaceRoutesAllowsOnlyNegotiatedEndpoints(t *testing.T) { t.Parallel() config := userspaceRouteTestConfig() if err := validateUserspaceRoutes(config); err != nil { t.Fatalf("valid negotiated route: %v", err) } config.PCSCF = []net.IP{net.IPv4(203, 0, 113, 10)} if err := validateUserspaceRoutes(config); err == nil { t.Fatal("P-CSCF outside responder selector was accepted") } } func TestValidateUserspaceRoutesRequiresMatchingAddressFamily(t *testing.T) { t.Parallel() config := userspaceRouteTestConfig() config.PCSCF = []net.IP{net.ParseIP("2001:db8::20")} config.ResponderSelectors = []trafficSelector{{ StartPort: 0, EndPort: 65535, StartIP: net.ParseIP("2001:db8::"), EndIP: net.ParseIP("2001:db8::ffff"), }} if err := validateUserspaceRoutes(config); err == nil { t.Fatal("IPv6 P-CSCF without an assigned inner IPv6 address was accepted") } } func TestUserspaceRulePriorityAlwaysPrecedesMainRoute(t *testing.T) { t.Parallel() for _, spi := range []uint32{ 0, 1, 32767, 0x01020304, 0x80000000, 0xffffffff, } { table, priority := userspaceRoutingIdentifiers(spi) if table <= 255 { t.Fatalf("SPI %08x produced reserved routing table %d", spi, table) } if priority < 10000 || priority >= 30000 || priority >= 32766 { t.Fatalf( "SPI %08x produced unsafe rule priority %d", spi, priority, ) } } } func userspaceRouteTestConfig() ChildSAConfig { return ChildSAConfig{ InnerLocalIPv4: net.IPv4(10, 132, 116, 34), PCSCF: []net.IP{net.IPv4(10, 127, 192, 82)}, InitiatorSelectors: []trafficSelector{{ StartPort: 0, EndPort: 65535, StartIP: net.IPv4(10, 132, 116, 34), EndIP: net.IPv4(10, 132, 116, 34), }}, ResponderSelectors: []trafficSelector{{ StartPort: 0, EndPort: 65535, StartIP: net.IPv4(10, 0, 0, 0), EndIP: net.IPv4(10, 255, 255, 255), }}, } } type blockingNATTRelay struct{} func (blockingNATTRelay) SendESP(ctx context.Context, _ []byte) error { return ctx.Err() } func (blockingNATTRelay) ReceiveESP(ctx context.Context, _ []byte) (int, error) { <-ctx.Done() return 0, ctx.Err() } func TestLinuxUserspaceInstallerLifecycle(t *testing.T) { if os.Getenv("VOCAT_NETNS_TEST") != "1" { t.Skip("set VOCAT_NETNS_TEST=1 inside an isolated Linux network namespace") } config := userspaceRouteTestConfig() config.Name = "vocat-swu-test" config.InboundSPI = 0x01020304 config.OutboundSPI = 0x05060708 config.Encryption = "aes-cbc-128" config.Integrity = "hmac-sha1-96" config.InboundEncKey = make([]byte, 16) config.InboundAuthKey = make([]byte, 20) config.OutboundEncKey = make([]byte, 16) config.OutboundAuthKey = make([]byte, 20) config.UDPEncapsulation = true config.Relay = blockingNATTRelay{} tableID, _ := userspaceRoutingIdentifiers(config.InboundSPI) table := strconv.FormatUint(uint64(tableID), 10) if output, err := exec.Command( "ip", "-4", "route", "add", "table", table, "unreachable", "default", ).CombinedOutput(); err != nil { t.Fatalf("preoccupy route table: %v: %s", err, output) } if conflicting, err := (linuxUserspaceInstaller{ipCommand: "ip"}).Install( context.Background(), config, ); err == nil { _ = conflicting.Close(context.Background()) t.Fatal("installer accepted a preoccupied routing table") } if output, err := exec.Command( "ip", "-4", "route", "delete", "table", table, "unreachable", "default", ).CombinedOutput(); err != nil { t.Fatalf("release preoccupied route table: %v: %s", err, output) } handle, err := (linuxUserspaceInstaller{ipCommand: "ip"}).Install( context.Background(), config, ) if err != nil { t.Fatalf("install user-space CHILD_SA: %v", err) } if mode := handle.(DataplaneEvidence).DataplaneMode(); mode != "userspace" { t.Fatalf("dataplane mode = %q", mode) } if output, err := exec.Command("ip", "link", "show", "dev", config.Name).CombinedOutput(); err != nil { t.Fatalf("TUN interface was not created: %v: %s", err, output) } if output, err := exec.Command( "ip", "-4", "route", "show", "table", table, ).CombinedOutput(); err != nil || !strings.Contains(string(output), "10.127.192.82") || !strings.Contains(string(output), "unreachable default") { t.Fatalf("isolated P-CSCF routes are missing: %v: %s", err, output) } if output, err := exec.Command( "ip", "-4", "route", "get", "10.127.192.82", "from", "10.132.116.34", ).CombinedOutput(); err != nil || !strings.Contains(string(output), "dev "+config.Name) || !strings.Contains(string(output), "table "+table) { t.Fatalf( "P-CSCF route did not win before the main table: %v: %s", err, output, ) } if output, err := exec.Command( "ip", "-4", "route", "get", "198.51.100.1", "from", "10.132.116.34", ).CombinedOutput(); err == nil { t.Fatalf( "non-P-CSCF inner traffic escaped the unreachable default: %s", output, ) } expectedFailure := errors.New("test relay stopped") concrete := handle.(*linuxUserspaceHandle) concrete.fail(expectedFailure) select { case failure := <-concrete.Failures(): if !errors.Is(failure, expectedFailure) { t.Fatalf("runtime failure = %v", failure) } case <-time.After(time.Second): t.Fatal("terminal failure was not published") } closeContext, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := handle.Close(closeContext); err != nil { t.Fatalf("close user-space CHILD_SA: %v", err) } if err := exec.Command("ip", "link", "show", "dev", config.Name).Run(); err == nil { t.Fatal("TUN interface survived Close") } output, err := exec.Command("ip", "-4", "rule", "show").CombinedOutput() if err != nil { t.Fatal(err) } if strings.Contains(string(output), "from 10.132.116.34") { t.Fatalf("source rule survived Close: %s", output) } } var _ NATTPacketRelay = blockingNATTRelay{}