| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //go:build !build_with_native_toolchain |
| |
| package netstack |
| |
| import ( |
| "bytes" |
| "context" |
| "errors" |
| "flag" |
| "fmt" |
| "math" |
| "net" |
| "os" |
| "sort" |
| "sync/atomic" |
| "syscall/zx" |
| "testing" |
| "time" |
| |
| "fidl/fuchsia/logger" |
| fnet "fidl/fuchsia/net" |
| "fidl/fuchsia/unknown" |
| |
| "go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/dhcp" |
| "go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/dns" |
| "go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/fidlconv" |
| "go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/routes" |
| "go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/routetypes" |
| "go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/sync" |
| "go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/util" |
| "go.fuchsia.dev/fuchsia/src/lib/component" |
| syslog "go.fuchsia.dev/fuchsia/src/lib/syslog/go" |
| |
| "github.com/google/go-cmp/cmp" |
| "gvisor.dev/gvisor/pkg/tcpip" |
| "gvisor.dev/gvisor/pkg/tcpip/faketime" |
| "gvisor.dev/gvisor/pkg/tcpip/header" |
| "gvisor.dev/gvisor/pkg/tcpip/link/sniffer" |
| "gvisor.dev/gvisor/pkg/tcpip/network/arp" |
| "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" |
| "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" |
| tcpipstack "gvisor.dev/gvisor/pkg/tcpip/stack" |
| "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" |
| "gvisor.dev/gvisor/pkg/tcpip/transport/udp" |
| "gvisor.dev/gvisor/pkg/waiter" |
| ) |
| |
| const ( |
| testTopoPath string = "/fake/ethernet/device" |
| dadResolutionTimeout = dadRetransmitTimer*dadTransmits + time.Second |
| ) |
| |
| var ( |
| testV4Address = util.Parse("192.168.42.16") |
| testV6Address = util.Parse("200a::1") |
| testLinkLocalV6Addr1 = util.Parse("fe80::1") |
| testLinkLocalV6Addr2 = util.Parse("fe80::2") |
| ) |
| |
| func TestMain(m *testing.M) { |
| flag.Parse() |
| if testing.Verbose() { |
| componentCtx := component.NewContextFromStartupInfo() |
| req, logSink, err := logger.NewLogSinkWithCtxInterfaceRequest() |
| if err != nil { |
| panic(err) |
| } |
| componentCtx.ConnectToEnvService(req) |
| options := syslog.LogInitOptions{ |
| LogLevel: math.MinInt8, |
| } |
| options.LogSink = logSink |
| options.MinSeverityForFileAndLineInfo = math.MinInt8 |
| l, err := syslog.NewLogger(options) |
| if err != nil { |
| panic(fmt.Sprintf("syslog.NewLogger(%#v) = %s", options, err)) |
| } |
| syslog.SetDefaultLogger(l) |
| |
| // As of this writing we set this value to 0 in netstack/main.go. |
| sniffer.LogPackets.Store(1) |
| } |
| |
| os.Exit(m.Run()) |
| } |
| |
| func TestDelRouteErrors(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| |
| ifs := addNoopEndpoint(t, ns, "") |
| t.Cleanup(ifs.RemoveByUser) |
| |
| rt := tcpip.Route{ |
| Destination: header.IPv4EmptySubnet, |
| Gateway: util.Parse("1.2.3.4"), |
| NIC: ifs.nicid, |
| } |
| |
| // Deleting a route we never added should return no routes deleted. |
| if got := ns.DelRoute(rt, routetypes.GlobalRouteSet()); len(got) != 0 { |
| t.Errorf("DelRoute(%s) = %#v not empty", rt, got) |
| } |
| |
| if _, err := ns.AddRoute(rt, nil /* metric */, false, true /* replaceMatchingGvisorRoutes */, routetypes.GlobalRouteSet()); err != nil { |
| t.Fatalf("AddRoute(%s, metricNotSet, false): %s", rt, err) |
| } |
| // Deleting a route we added should not result in an error. |
| want := routetypes.ExtendedRoute{ |
| Route: rt, |
| Prf: routetypes.MediumPreference, |
| Metric: defaultInterfaceMetric, |
| MetricTracksInterface: true, |
| } |
| if diff := cmp.Diff(ns.DelRoute(rt, routetypes.GlobalRouteSet()), []routetypes.ExtendedRoute{want}, extendedRouteTableCmpOptions()); diff != "" { |
| t.Fatalf("DelRoute(%s): -got +want %s", rt, diff) |
| } |
| // Deleting a route we just deleted should result in no routes actually deleted. |
| if got := ns.DelRoute(rt, routetypes.GlobalRouteSet()); len(got) != 0 { |
| t.Errorf("DelRoute(%s) = %#v not empty", rt, got) |
| } |
| } |
| |
| func (ifs *ifState) IsUp() bool { |
| ifs.mu.Lock() |
| defer ifs.mu.Unlock() |
| return ifs.IsUpLocked() |
| } |
| |
| func installAndValidateIface(t *testing.T, ns *Netstack, addEndpoint func(*testing.T, *Netstack, string) *ifState) *ifState { |
| ifs := addEndpoint(t, ns, "") |
| t.Cleanup(ifs.RemoveByUser) |
| |
| // The NIC should initially be disabled in stack.Stack. |
| if enabled := ns.stack.CheckNIC(ifs.nicid); enabled { |
| t.Fatalf("got ns.stack.CheckNIC(%d) = true, want = false", ifs.nicid) |
| } |
| |
| if ifs.IsUp() { |
| t.Fatal("got initial link state up, want down") |
| } |
| |
| // Bringing the link up should enable the NIC in stack.Stack. |
| if err := ifs.Up(); err != nil { |
| t.Fatal("ifs.Up(): ", err) |
| } |
| if enabled := ns.stack.CheckNIC(ifs.nicid); !enabled { |
| t.Fatalf("got ns.stack.CheckNIC(%d) = false, want = true", ifs.nicid) |
| } |
| |
| if !ifs.IsUp() { |
| t.Fatal("got post-up link state down, want up") |
| } |
| return ifs |
| } |
| |
| // TestStackNICEnableDisable tests that the NIC in stack.Stack is enabled or |
| // disabled when the underlying link is brought up or down, respectively. |
| func TestStackNICEnableDisable(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| ifs := installAndValidateIface(t, ns, addNoopEndpoint) |
| |
| // Bringing the link down should disable the NIC in stack.Stack. |
| if err := ifs.Down(); err != nil { |
| t.Fatal("ifs.Down(): ", err) |
| } |
| if enabled := ns.stack.CheckNIC(ifs.nicid); enabled { |
| t.Fatalf("got ns.stack.CheckNIC(%d) = true, want = false", ifs.nicid) |
| } |
| |
| if ifs.IsUp() { |
| t.Fatal("got post-down link state up, want down") |
| } |
| } |
| |
| var _ NICRemovedHandler = (*testNicRemovedHandler)(nil) |
| |
| type testNicRemovedHandler struct { |
| removedNICID tcpip.NICID |
| } |
| |
| func (h *testNicRemovedHandler) RemovedNIC(nicID tcpip.NICID) { |
| h.removedNICID = nicID |
| } |
| |
| // TestStackNICRemove tests that the NIC in stack.Stack is removed when the |
| // underlying link is closed. |
| func TestStackNICRemove(t *testing.T) { |
| nicRemovedHandler := testNicRemovedHandler{} |
| ns, _ := newNetstack(t, netstackTestOptions{nicRemovedHandler: &nicRemovedHandler}) |
| var obs noopObserver |
| |
| ifs, err := ns.addEndpoint( |
| func(tcpip.NICID) string { return t.Name() }, |
| &noopEndpoint{}, |
| &noopController{}, |
| &obs, |
| defaultInterfaceMetric, /* metric */ |
| qdiscConfig{}, |
| ) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // The NIC should initially be disabled in stack.Stack. |
| if enabled := ns.stack.CheckNIC(ifs.nicid); enabled { |
| t.Errorf("got ns.stack.CheckNIC(%d) = true, want = false", ifs.nicid) |
| } |
| if _, ok := ns.stack.NICInfo()[ifs.nicid]; !ok { |
| t.Errorf("missing NICInfo for NIC %d", ifs.nicid) |
| } |
| if _, err := ns.stack.GetMainNICAddress(ifs.nicid, header.IPv6ProtocolNumber); err != nil { |
| t.Errorf("GetMainNICAddress(%d, header.IPv6ProtocolNumber): %s", ifs.nicid, err) |
| } |
| |
| if t.Failed() { |
| t.FailNow() |
| } |
| |
| // Closing the link should remove the NIC from stack.Stack. |
| obs.onLinkClosed() |
| |
| if enabled := ns.stack.CheckNIC(ifs.nicid); enabled { |
| t.Errorf("got ns.stack.CheckNIC(%d) = false, want = true", ifs.nicid) |
| } |
| if nicInfo, ok := ns.stack.NICInfo()[ifs.nicid]; ok { |
| t.Errorf("unexpected NICInfo found for NIC %d = %+v", ifs.nicid, nicInfo) |
| } |
| { |
| addr, err := ns.stack.GetMainNICAddress(ifs.nicid, header.IPv6ProtocolNumber) |
| if _, ok := err.(*tcpip.ErrUnknownNICID); !ok { |
| t.Errorf("got GetMainNICAddress(%d, header.IPv6ProtocolNumber) = (%s, %T), want = (_, *tcpip.ErrUnknownNICID)", ifs.nicid, addr, err) |
| } |
| } |
| if nicRemovedHandler.removedNICID != ifs.nicid { |
| t.Errorf("got nicRemovedHandler.removedNICID = %d, want = %d", nicRemovedHandler.removedNICID, ifs.nicid) |
| } |
| |
| // Wait for the controller to stop and free up its resources. |
| ifs.endpoint.Wait() |
| } |
| |
| func containsRoute(rs []tcpip.Route, r tcpip.Route) bool { |
| for _, i := range rs { |
| if i == r { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| func TestEndpoint_Close(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| var wq waiter.Queue |
| // Avoid polluting everything with err of type tcpip.Error. |
| ep := func() tcpip.Endpoint { |
| ep, err := ns.stack.NewEndpoint(tcp.ProtocolNumber, ipv6.ProtocolNumber, &wq) |
| if err != nil { |
| t.Fatalf("NewEndpoint(tcp.ProtocolNumber, ipv6.ProtocolNumber, _) = %s", err) |
| } |
| return ep |
| }() |
| defer ep.Close() |
| |
| eps, err := newEndpointWithSocket(ep, &wq, tcp.ProtocolNumber, ipv6.ProtocolNumber, ns, zx.SocketStream) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer eps.close() |
| |
| eps.mu.Lock() |
| channels := []struct { |
| ch <-chan struct{} |
| name string |
| }{ |
| {ch: eps.closing, name: "closing"}, |
| {ch: eps.mu.loopReadDone, name: "loopReadDone"}, |
| {ch: eps.mu.loopWriteDone, name: "loopWriteDone"}, |
| } |
| eps.mu.Unlock() |
| |
| // Check starting conditions. |
| for _, ch := range channels { |
| select { |
| case <-ch.ch: |
| t.Errorf("%s cleaned up prematurely", ch.name) |
| default: |
| } |
| } |
| |
| if _, ok := eps.ns.endpoints.Load(eps.endpoint.key); !ok { |
| var keys []uint64 |
| eps.ns.endpoints.Range(func(key uint64, _ endpointAndStats) bool { |
| keys = append(keys, key) |
| return true |
| }) |
| t.Errorf("got endpoints map = %d at creation, want %d", keys, eps.endpoint.key) |
| } |
| |
| if t.Failed() { |
| t.FailNow() |
| } |
| |
| // Create a referent. |
| s, err := newStreamSocket(makeStreamSocketImpl(eps)) |
| if err != nil { |
| t.Fatalf("newStreamSocket() = %s", err) |
| } |
| defer func() { |
| func() { |
| result, err := s.Close(context.Background()) |
| if err, ok := err.(*zx.Error); ok && err.Status == zx.ErrPeerClosed { |
| return |
| } |
| t.Errorf("s.Close() = (%#v, %v)", result, err) |
| }() |
| if err := s.Channel.Close(); err != nil { |
| t.Errorf("s.Channel.Close() = %s", err) |
| } |
| }() |
| |
| // Create another referent. |
| localC, peerC, err := zx.NewChannel(0) |
| if err != nil { |
| t.Fatalf("zx.NewChannel() = %s", err) |
| } |
| defer func() { |
| // Already closed below. |
| if err := localC.Close(); err != nil { |
| t.Errorf("localC.Close() = %s", err) |
| } |
| |
| // By-value copy already closed by the server when we closed the peer. |
| err := peerC.Close() |
| if err, ok := err.(*zx.Error); ok && err.Status == zx.ErrBadHandle { |
| return |
| } |
| t.Errorf("peerC.Close() = %v", err) |
| }() |
| |
| if err := s.Clone2(context.Background(), unknown.CloneableWithCtxInterfaceRequest{Channel: peerC}); err != nil { |
| t.Fatalf("s.Clone() = %s", err) |
| } |
| |
| // Close the original referent. |
| if result, err := s.Close(context.Background()); err != nil { |
| t.Fatalf("s.Close() = %s", err) |
| } else if result.Which() != unknown.CloseableCloseResultResponse { |
| t.Fatalf("s.Close() = %s", zx.Status(result.Err)) |
| } |
| |
| // There's still a referent. |
| for _, ch := range channels { |
| select { |
| case <-ch.ch: |
| t.Errorf("%s cleaned up prematurely", ch.name) |
| default: |
| } |
| } |
| |
| if _, ok := eps.ns.endpoints.Load(eps.endpoint.key); !ok { |
| var keys []uint64 |
| eps.ns.endpoints.Range(func(key uint64, _ endpointAndStats) bool { |
| keys = append(keys, key) |
| return true |
| }) |
| t.Errorf("got endpoints map prematurely = %d, want %d", keys, eps.endpoint.key) |
| } |
| |
| if t.Failed() { |
| t.FailNow() |
| } |
| |
| // Close the last reference. |
| if err := localC.Close(); err != nil { |
| t.Fatalf("localC.Close() = %s", err) |
| } |
| |
| // Give a generous timeout for the closed channel to be detected. |
| timeout := make(chan struct{}) |
| time.AfterFunc(5*time.Second, func() { close(timeout) }) |
| for _, ch := range channels { |
| if ch.ch != nil { |
| select { |
| case <-ch.ch: |
| case <-timeout: |
| t.Errorf("%s not cleaned up", ch.name) |
| } |
| } |
| } |
| |
| for { |
| if _, ok := eps.ns.endpoints.Load(eps.endpoint.key); ok { |
| select { |
| case <-timeout: |
| var keys []uint64 |
| eps.ns.endpoints.Range(func(key uint64, _ endpointAndStats) bool { |
| keys = append(keys, key) |
| return true |
| }) |
| t.Errorf("got endpoints map = %d after closure, want *not* %d", keys, eps.endpoint.key) |
| default: |
| continue |
| } |
| } |
| break |
| } |
| |
| if t.Failed() { |
| t.FailNow() |
| } |
| } |
| |
| // TestTCPEndpointMapAcceptAfterReset tests that an already-reset endpoint |
| // isn't added to the endpoints map, since such an endpoint wouldn't receive a |
| // hangup notification and its reference in the map would leak. |
| func TestTCPEndpointMapAcceptAfterReset(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| if err := ns.addLoopback(); err != nil { |
| t.Fatalf("ns.addLoopback() = %s", err) |
| } |
| |
| listener := makeStreamSocketImpl(createEP(t, ns, new(waiter.Queue))) |
| |
| if err := listener.endpoint.ep.Bind(tcpip.FullAddress{}); err != nil { |
| t.Fatalf("Bind({}) = %s", err) |
| } |
| if err := listener.endpoint.ep.Listen(1); err != nil { |
| t.Fatalf("Listen(1) = %s", err) |
| } |
| |
| client := createEP(t, ns, new(waiter.Queue)) |
| |
| // Connect and wait for the incoming connection. |
| func() { |
| connectAddr, err := listener.endpoint.ep.GetLocalAddress() |
| if err != nil { |
| t.Fatalf("GetLocalAddress() = %s", err) |
| } |
| |
| waitEntry, notifyCh := waiter.NewChannelEntry(waiter.EventIn) |
| listener.wq.EventRegister(&waitEntry) |
| defer listener.wq.EventUnregister(&waitEntry) |
| |
| switch err := client.ep.Connect(connectAddr); err.(type) { |
| case *tcpip.ErrConnectStarted: |
| default: |
| t.Fatalf("Connect(%#v) = %s", connectAddr, err) |
| } |
| <-notifyCh |
| }() |
| |
| // Initiate a RST of the connection sitting in the accept queue. |
| client.ep.SocketOptions().SetLinger(tcpip.LingerOption{ |
| Enabled: true, |
| Timeout: 0, |
| }) |
| client.ep.Close() |
| |
| // Wait for the RST to be processed by the stack. |
| time.Sleep(100 * time.Millisecond) |
| |
| _, _, s, err := listener.accept(false) |
| if err != nil { |
| t.Fatalf("ep.accept(false) = %s", err) |
| } |
| defer s.endpointWithSocket.close() |
| |
| // Expect the `Accept` to have removed the endpoint from the map. |
| if _, ok := ns.endpoints.Load(s.endpoint.key); ok { |
| t.Fatalf("got endpoints.Load(%d) = (_, true)", s.endpoint.key) |
| } |
| |
| s.mu.Lock() |
| channels := []struct { |
| ch <-chan struct{} |
| name string |
| }{ |
| {ch: s.closing, name: "closing"}, |
| {ch: s.mu.loopReadDone, name: "loopReadDone"}, |
| {ch: s.mu.loopWriteDone, name: "loopWriteDone"}, |
| } |
| s.mu.Unlock() |
| |
| // Give a generous timeout for the closed channel to be detected. |
| timeout := make(chan struct{}) |
| time.AfterFunc(5*time.Second, func() { close(timeout) }) |
| for _, ch := range channels { |
| if ch.ch != nil { |
| select { |
| case <-ch.ch: |
| case <-timeout: |
| t.Errorf("%s not cleaned up", ch.name) |
| } |
| } |
| } |
| } |
| |
| func createEP(t *testing.T, ns *Netstack, wq *waiter.Queue) *endpointWithSocket { |
| // Avoid polluting the scope with err of type tcpip.Error. |
| ep := func() tcpip.Endpoint { |
| ep, err := ns.stack.NewEndpoint(tcp.ProtocolNumber, ipv4.ProtocolNumber, wq) |
| if err != nil { |
| t.Fatalf("NewEndpoint(tcp.ProtocolNumber, ipv4.ProtocolNumber, _) = %s", err) |
| } |
| return ep |
| }() |
| t.Cleanup(ep.Close) |
| eps, err := newEndpointWithSocket(ep, wq, tcp.ProtocolNumber, ipv4.ProtocolNumber, ns, zx.SocketStream) |
| if err != nil { |
| t.Fatal(err) |
| } |
| t.Cleanup(eps.close) |
| return eps |
| } |
| |
| func TestTCPEndpointMapClose(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| eps := createEP(t, ns, new(waiter.Queue)) |
| |
| // Closing the endpoint should remove it from the endpoints map. |
| if _, ok := ns.endpoints.Load(eps.endpoint.key); !ok { |
| t.Fatalf("got endpoints.Load(%d) = (_, false)", eps.endpoint.key) |
| } |
| eps.close() |
| if _, ok := ns.endpoints.Load(eps.endpoint.key); ok { |
| t.Fatalf("got endpoints.Load(%d) = (_, true)", eps.endpoint.key) |
| } |
| } |
| |
| func TestTCPEndpointMapConnect(t *testing.T) { |
| ns, clock := newNetstack(t, netstackTestOptions{}) |
| |
| var linkEP tcpipstack.LinkEndpoint = &noopEndpoint{ |
| capabilities: tcpipstack.CapabilityResolutionRequired, |
| } |
| if testing.Verbose() { |
| linkEP = sniffer.New(linkEP) |
| } |
| ifs, err := ns.addEndpoint( |
| func(tcpip.NICID) string { return t.Name() }, |
| linkEP, |
| &noopController{}, |
| nil, |
| defaultInterfaceMetric, /* metric */ |
| qdiscConfig{}, |
| ) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err := ns.stack.EnableNIC(ifs.nicid); err != nil { |
| t.Fatal(err) |
| } |
| |
| address := tcpip.AddrFrom4([4]byte{1, 2, 3, 4}) |
| destination := tcpip.FullAddress{ |
| Addr: address, |
| Port: 1, |
| } |
| source := tcpip.AddrFrom4([4]byte{5, 6, 7, 8}) |
| protocolAddress := tcpip.ProtocolAddress{ |
| Protocol: ipv4.ProtocolNumber, |
| AddressWithPrefix: source.WithPrefix(), |
| } |
| if err := ns.stack.AddProtocolAddress(ifs.nicid, protocolAddress, tcpipstack.AddressProperties{}); err != nil { |
| t.Fatalf("AddProtocolAddress(%d, %#v, {}) = %s", ifs.nicid, protocolAddress, err) |
| } |
| |
| ns.stack.SetRouteTable([]tcpip.Route{ |
| { |
| Destination: address.WithPrefix().Subnet(), |
| NIC: ifs.nicid, |
| }, |
| }) |
| |
| var wq waiter.Queue |
| eps := createEP(t, ns, &wq) |
| |
| // We trigger link resolution with a TCP segment being sent, and then send |
| // another segment after 1 second, and another after 2 seconds. This means we |
| // could send up to 3 TCP segments, meaning the waiter can get notified up to |
| // 3 times when link resolution fails. We only consume 1 waiter event in this |
| // test, so we need to leave room in the buffer for two events in order for |
| // the test not to deadlock. |
| events := make(chan waiter.EventMask, 2) |
| waitEntry := waiter.NewFunctionEntry(math.MaxUint64, func(m waiter.EventMask) { |
| events <- m |
| }) |
| wq.EventRegister(&waitEntry) |
| defer wq.EventUnregister(&waitEntry) |
| |
| switch err := eps.ep.Connect(destination); err.(type) { |
| case *tcpip.ErrConnectStarted: |
| default: |
| t.Fatalf("got Connect(%#v) = %v, want %s", destination, err, &tcpip.ErrConnectStarted{}) |
| } |
| |
| { |
| nudConfig, err := ns.stack.NUDConfigurations(ifs.nicid, ipv4.ProtocolNumber) |
| if err != nil { |
| t.Fatalf("stack.NUDConfigurations(): %s", err) |
| } |
| clock.Advance(time.Duration(nudConfig.MaxMulticastProbes) * nudConfig.RetransmitTimer) |
| } |
| |
| if got, want := <-events, waiter.ReadableEvents|waiter.WritableEvents|waiter.EventErr|waiter.EventHUp; got != want { |
| t.Fatalf("got event = %b, want %b", got, want) |
| } |
| |
| // The callback on HUp should have removed the endpoint from the map. |
| if _, ok := ns.endpoints.Load(eps.endpoint.key); ok { |
| t.Fatalf("got endpoints.Load(%d) = (_, true)", eps.endpoint.key) |
| } |
| } |
| |
| // TestTCPEndpointMapClosing validates the endpoint in a closing state like |
| // FIN_WAIT2 to be present in the endpoints map and is deleted when the |
| // endpoint transitions to CLOSED state. |
| func TestTCPEndpointMapClosing(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| if err := ns.addLoopback(); err != nil { |
| t.Fatalf("ns.addLoopback() = %s", err) |
| } |
| listener := createEP(t, ns, new(waiter.Queue)) |
| |
| if err := listener.ep.Bind(tcpip.FullAddress{}); err != nil { |
| t.Fatalf("ep.Bind({}) = %s", err) |
| } |
| if err := listener.ep.Listen(1); err != nil { |
| t.Fatalf("ep.Listen(1) = %s", err) |
| } |
| connectAddr, err := listener.ep.GetLocalAddress() |
| if err != nil { |
| t.Fatalf("ep.GetLocalAddress() = %s", err) |
| } |
| client := createEP(t, ns, new(waiter.Queue)) |
| |
| waitEntry, inCh := waiter.NewChannelEntry(waiter.EventIn) |
| listener.wq.EventRegister(&waitEntry) |
| defer listener.wq.EventUnregister(&waitEntry) |
| |
| switch err := client.ep.Connect(connectAddr); err.(type) { |
| case *tcpip.ErrConnectStarted: |
| default: |
| t.Fatalf("ep.Connect(%#v) = %s", connectAddr, err) |
| } |
| // Wait for the newly established connection to show up as acceptable by |
| // the peer. |
| <-inCh |
| |
| server, _, err := listener.ep.Accept(nil) |
| if err != nil { |
| t.Fatalf("ep.Accept(nil) = %s", err) |
| } |
| |
| // Ensure that the client endpoint is present in our internal map. |
| if _, ok := ns.endpoints.Load(client.endpoint.key); !ok { |
| t.Fatalf("got endpoints.Load(%d) = (_,false)", client.endpoint.key) |
| } |
| |
| // Trigger an active close from the client. |
| client.close() |
| |
| // The client endpoint should not be removed from endpoints map even after |
| // an endpoint close. |
| if _, ok := ns.endpoints.Load(client.endpoint.key); !ok { |
| t.Fatalf("got endpoints.Load(%d) = (_,false)", client.endpoint.key) |
| } |
| |
| ticker := time.NewTicker(10 * time.Millisecond) |
| defer ticker.Stop() |
| // Wait and check for the client active close to reach FIN_WAIT2 state. |
| for { |
| if tcp.EndpointState(client.ep.State()) == tcp.StateFinWait2 { |
| break |
| } |
| <-ticker.C |
| } |
| |
| // Lookup for the client once more in the endpoints map, it should still not |
| // be removed. |
| if _, ok := ns.endpoints.Load(client.endpoint.key); !ok { |
| t.Fatalf("got endpoints.Load(%d) = (_,false)", client.endpoint.key) |
| } |
| |
| timeWaitOpt := tcpip.TCPTimeWaitTimeoutOption(time.Duration(0)) |
| if err := ns.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &timeWaitOpt); err != nil { |
| t.Fatalf("SetTransportProtocolOption(%d, &%T(%d)) = %s", tcp.ProtocolNumber, timeWaitOpt, timeWaitOpt, err) |
| } |
| |
| // Trigger server close, so that client enters TIME_WAIT. |
| server.Close() |
| |
| // gVisor stack notifies EventHUp on entering TIME_WAIT. Wait for some time |
| // for the EventHUp to be processed by netstack. |
| for { |
| // The client endpoint would be removed from the endpoints map as a result |
| // of processing EventHUp. |
| if _, ok := ns.endpoints.Load(client.endpoint.key); !ok { |
| break |
| } |
| <-ticker.C |
| } |
| } |
| |
| func TestEndpointsMapKey(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| if ns.endpoints.nextKey != 0 { |
| t.Fatalf("got ns.endpoints.nextKey = %d, want 0", ns.endpoints.nextKey) |
| } |
| |
| tcpipEP := func() (*waiter.Queue, tcpip.Endpoint) { |
| var wq waiter.Queue |
| ep, err := ns.stack.NewEndpoint(tcp.ProtocolNumber, ipv6.ProtocolNumber, &wq) |
| if err != nil { |
| t.Fatalf("NewEndpoint(tcp.ProtocolNumber, ipv6.ProtocolNumber, _) = %s", err) |
| } |
| t.Cleanup(ep.Close) |
| return &wq, ep |
| } |
| // Test if we always skip key value 0 while adding to the map. |
| for _, key := range []uint64{0, math.MaxUint64} { |
| wq, ep := tcpipEP() |
| |
| // Set a test value to nextKey which is used to compute the endpoint key. |
| ns.endpoints.nextKey = key |
| eps, err := newEndpointWithSocket(ep, wq, tcp.ProtocolNumber, ipv6.ProtocolNumber, ns, zx.SocketStream) |
| if err != nil { |
| t.Fatal(err) |
| } |
| t.Cleanup(eps.close) |
| if ns.endpoints.nextKey != 1 { |
| t.Fatalf("got ns.endpoints.nextKey = %d, want 1", ns.endpoints.nextKey) |
| } |
| if eps.endpoint.key != 1 { |
| t.Fatalf("got eps.endpoint.key = %d, want 1", eps.endpoint.key) |
| } |
| if _, ok := ns.endpoints.Load(eps.endpoint.key); !ok { |
| t.Fatalf("got endpoints.Load(%d) = (_,false)", eps.endpoint.key) |
| } |
| // Closing the endpoint should remove the endpoint with key value 1 |
| // from the endpoints map. This lets the subsequent iteration to reuse |
| // key value 1 to add a new endpoint to the map. |
| eps.close() |
| } |
| |
| // Key value 0 is not expected to be removed from the map. |
| _, ep := tcpipEP() |
| ns.endpoints.Store(0, endpointAndStats{ep: ep}) |
| if ns.onRemoveEndpoint(0) { |
| t.Errorf("got ns.onRemoveEndpoint(0) = true, want false") |
| } |
| if _, ok := ns.endpoints.Load(0); !ok { |
| t.Fatal("got endpoints.Load(0) = (_,false)") |
| } |
| } |
| |
| func TestNICName(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| |
| if want, got := "unknown(NICID=0)", ns.name(0); got != want { |
| t.Fatalf("got ns.name(0) = %q, want %q", got, want) |
| } |
| |
| { |
| ifs := addNoopEndpoint(t, ns, "") |
| t.Cleanup(ifs.RemoveByUser) |
| if got, want := ifs.ns.name(ifs.nicid), t.Name()+"1"; got != want { |
| t.Fatalf("got ifs.mu.name = %q, want = %q", got, want) |
| } |
| } |
| |
| { |
| const name = "VerySpecialName" |
| ifs := addNoopEndpoint(t, ns, name) |
| t.Cleanup(ifs.RemoveByUser) |
| if got, want := ifs.ns.name(ifs.nicid), name; got != want { |
| t.Fatalf("got ifs.mu.name = %q, want = %q", got, want) |
| } |
| } |
| } |
| |
| func TestNotStartedByDefault(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| |
| startCalled := false |
| controller := noopController{ |
| onUp: func() { startCalled = true }, |
| } |
| if _, err := ns.addEndpoint( |
| func(tcpip.NICID) string { return t.Name() }, |
| &noopEndpoint{}, |
| &controller, |
| nil, /* observer */ |
| defaultInterfaceMetric, /* metric */ |
| qdiscConfig{}, |
| ); err != nil { |
| t.Fatal(err) |
| } |
| |
| if startCalled { |
| t.Error("unexpected call to Controller.Up") |
| } |
| } |
| |
| type ndpDADEvent struct { |
| nicID tcpip.NICID |
| addr tcpip.Address |
| result tcpipstack.DADResult |
| } |
| |
| var _ ipv6.NDPDispatcher = (*testNDPDispatcher)(nil) |
| |
| // testNDPDispatcher is a tcpip.NDPDispatcher that sends an NDP DAD event on |
| // dadC when OnDuplicateAddressDetectionResult gets called. |
| type testNDPDispatcher struct { |
| dadC chan ndpDADEvent |
| } |
| |
| // OnDuplicateAddressDetectionResult implements ipv6.NDPDispatcher. |
| func (n *testNDPDispatcher) OnDuplicateAddressDetectionResult(nicID tcpip.NICID, addr tcpip.Address, result tcpipstack.DADResult) { |
| if c := n.dadC; c != nil { |
| c <- ndpDADEvent{ |
| nicID: nicID, |
| addr: addr, |
| result: result, |
| } |
| } |
| } |
| |
| // OnDefaultRouterDiscovered implements ipv6.NDPDispatcher. |
| func (*testNDPDispatcher) OnOffLinkRouteUpdated(tcpip.NICID, tcpip.Subnet, tcpip.Address, header.NDPRoutePreference) { |
| } |
| |
| // OnDefaultRouterInvalidated implements ipv6.NDPDispatcher. |
| func (*testNDPDispatcher) OnOffLinkRouteInvalidated(tcpip.NICID, tcpip.Subnet, tcpip.Address) { |
| } |
| |
| // OnOnLinkPrefixDiscovered implements ipv6.NDPDispatcher. |
| func (*testNDPDispatcher) OnOnLinkPrefixDiscovered(tcpip.NICID, tcpip.Subnet) { |
| } |
| |
| // OnOnLinkPrefixInvalidated implements ipv6.NDPDispatcher. |
| func (*testNDPDispatcher) OnOnLinkPrefixInvalidated(tcpip.NICID, tcpip.Subnet) { |
| } |
| |
| // OnAutoGenAddress implements ipv6.NDPDispatcher. |
| func (*testNDPDispatcher) OnAutoGenAddress(tcpip.NICID, tcpip.AddressWithPrefix) tcpipstack.AddressDispatcher { |
| return nil |
| } |
| |
| // OnAutoGenAddressDeprecated implements ipv6.NDPDispatcher. |
| func (*testNDPDispatcher) OnAutoGenAddressDeprecated(tcpip.NICID, tcpip.AddressWithPrefix) { |
| } |
| |
| // OnAutoGenAddressInvalidated implements ipv6.NDPDispatcher. |
| func (*testNDPDispatcher) OnAutoGenAddressInvalidated(tcpip.NICID, tcpip.AddressWithPrefix) { |
| } |
| |
| // OnRecursiveDNSServerOption implements ipv6.NDPDispatcher. |
| func (*testNDPDispatcher) OnRecursiveDNSServerOption(tcpip.NICID, []tcpip.Address, time.Duration) { |
| } |
| |
| // OnDNSSearchListOption implements ipv6.NDPDispatcher. |
| func (*testNDPDispatcher) OnDNSSearchListOption(tcpip.NICID, []string, time.Duration) { |
| } |
| |
| // OnDHCPv6Configuration implements ipv6.NDPDispatcher. |
| func (*testNDPDispatcher) OnDHCPv6Configuration(tcpip.NICID, ipv6.DHCPv6ConfigurationFromNDPRA) { |
| } |
| |
| func TestIpv6LinkLocalOnLinkRoute(t *testing.T) { |
| if got, want := ipv6LinkLocalOnLinkRoute(6), (tcpip.Route{Destination: header.IPv6LinkLocalPrefix.Subnet(), NIC: 6}); got != want { |
| t.Fatalf("got ipv6LinkLocalOnLinkRoute(6) = %s, want = %s", got, want) |
| } |
| } |
| |
| // Test that NICs get an on-link route to the IPv6 link-local subnet when it is |
| // brought up. |
| func TestIpv6LinkLocalOnLinkRouteOnUp(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| |
| ep := noopEndpoint{ |
| linkAddress: tcpip.LinkAddress([]byte{2, 3, 4, 5, 6, 7}), |
| } |
| ifs, err := ns.addEndpoint( |
| func(tcpip.NICID) string { return t.Name() }, |
| &ep, |
| &noopController{}, |
| nil, /* observer */ |
| defaultInterfaceMetric, /* metric */ |
| qdiscConfig{}, |
| ) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| linkLocalRoute := ipv6LinkLocalOnLinkRoute(ifs.nicid) |
| |
| // Initially should not have the link-local route. |
| rt := ns.stack.GetRouteTable() |
| if containsRoute(rt, linkLocalRoute) { |
| t.Fatalf("got GetRouteTable() = %+v, don't want = %s", rt, linkLocalRoute) |
| } |
| |
| // Bringing the ethernet device up should result in the link-local |
| // route being added. |
| if err := ifs.Up(); err != nil { |
| t.Fatalf("eth.Up(): %s", err) |
| } |
| rt = ns.stack.GetRouteTable() |
| if !containsRoute(rt, linkLocalRoute) { |
| t.Fatalf("got GetRouteTable() = %+v, want = %s", rt, linkLocalRoute) |
| } |
| |
| // Bringing the ethernet device down should result in the link-local |
| // route being removed. |
| if err := ifs.Down(); err != nil { |
| t.Fatalf("eth.Down(): %s", err) |
| } |
| rt = ns.stack.GetRouteTable() |
| if containsRoute(rt, linkLocalRoute) { |
| t.Fatalf("got GetRouteTable() = %+v, don't want = %s", rt, linkLocalRoute) |
| } |
| } |
| |
| func TestOnLinkV6Route(t *testing.T) { |
| subAddr := util.Parse("abcd:1234::") |
| subMask := util.ParseMask("ffff:ffff::") |
| subnet, err := tcpip.NewSubnet(subAddr, subMask) |
| if err != nil { |
| t.Fatalf("NewSubnet(%s, %s): %s", subAddr, subMask, err) |
| } |
| |
| if got, want := onLinkV6Route(6, subnet), (tcpip.Route{Destination: subnet, NIC: 6}); got != want { |
| t.Fatalf("got onLinkV6Route(6, %s) = %s, want = %s", subnet, got, want) |
| } |
| } |
| |
| func TestUniqueFallbackNICNames(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| |
| ifs1 := addNoopEndpoint(t, ns, "") |
| t.Cleanup(ifs1.RemoveByUser) |
| ifs2 := addNoopEndpoint(t, ns, "") |
| t.Cleanup(ifs2.RemoveByUser) |
| |
| nicInfos := ns.stack.NICInfo() |
| |
| nicInfo1, ok := nicInfos[ifs1.nicid] |
| if !ok { |
| t.Fatalf("stack.NICInfo()[%d] = (_, false)", ifs1.nicid) |
| } |
| nicInfo2, ok := nicInfos[ifs2.nicid] |
| if !ok { |
| t.Fatalf("stack.NICInfo()[%d]: (_, false)", ifs2.nicid) |
| } |
| |
| if nicInfo1.Name == nicInfo2.Name { |
| t.Fatalf("got (%+v).Name == (%+v).Name, want non-equal", nicInfo1, nicInfo2) |
| } |
| } |
| |
| var _ NICRemovedHandler = (*noopNicRemovedHandler)(nil) |
| |
| type noopNicRemovedHandler struct{} |
| |
| func (*noopNicRemovedHandler) RemovedNIC(tcpip.NICID) {} |
| |
| type netstackTestOptions struct { |
| nicRemovedHandler NICRemovedHandler |
| // TODO(https://fxbug.dev/42055981): Tests which pass a NDPDispatcher impl |
| // and runs a goroutine should have the goroutine joined in addition to |
| // being cancelled. |
| ndpDisp ipv6.NDPDispatcher |
| interfaceEventChan chan<- interfaceEvent |
| } |
| |
| func newNetstack(t *testing.T, options netstackTestOptions) (*Netstack, *faketime.ManualClock) { |
| t.Helper() |
| |
| clock := faketime.NewManualClock() |
| |
| stk := tcpipstack.New(tcpipstack.Options{ |
| NetworkProtocols: []tcpipstack.NetworkProtocolFactory{ |
| arp.NewProtocol, |
| ipv4.NewProtocol, |
| ipv6.NewProtocolWithOptions(ipv6.Options{ |
| NDPDisp: options.ndpDisp, |
| }), |
| }, |
| TransportProtocols: []tcpipstack.TransportProtocolFactory{ |
| tcp.NewProtocol, |
| udp.NewProtocol, |
| }, |
| Clock: clock, |
| }) |
| t.Cleanup(func() { |
| stk.Close() |
| stk.Wait() |
| }) |
| |
| ns := &Netstack{ |
| stack: stk, |
| // Required initialization because adding/removing interfaces interacts with |
| // DNS configuration. |
| dnsConfig: dns.MakeServersConfig(stk.Clock()), |
| nicRemovedHandlers: []NICRemovedHandler{func() NICRemovedHandler { |
| if h := options.nicRemovedHandler; h != nil { |
| return h |
| } |
| return &noopNicRemovedHandler{} |
| |
| }()}, |
| interfaceEventChan: options.interfaceEventChan, |
| } |
| if ndpDisp, ok := options.ndpDisp.(*ndpDispatcher); ok { |
| ndpDisp.ns = ns |
| ndpDisp.dynamicAddressSourceTracker.init(ns) |
| } |
| |
| return ns, clock |
| } |
| |
| func getInterfaceAddresses( |
| t *testing.T, |
| ni *stackImpl, |
| nicid tcpip.NICID, |
| ) []tcpip.AddressWithPrefix { |
| t.Helper() |
| nicInfos := ni.ns.stack.NICInfo() |
| nicInfo, ok := nicInfos[nicid] |
| if !ok { |
| t.Fatalf("couldn't find NICID=%d in %#v", nicid, nicInfos) |
| } |
| addrs := make([]tcpip.AddressWithPrefix, len(nicInfo.ProtocolAddresses)) |
| for i := range nicInfo.ProtocolAddresses { |
| addrs[i] = nicInfo.ProtocolAddresses[i].AddressWithPrefix |
| } |
| return addrs |
| } |
| |
| func compareInterfaceAddresses(t *testing.T, got, want []tcpip.AddressWithPrefix) { |
| t.Helper() |
| sort.Slice(got, func(i, j int) bool { return string(got[i].Address.AsSlice()) < string(got[j].Address.AsSlice()) }) |
| sort.Slice(want, func(i, j int) bool { return string(want[i].Address.AsSlice()) < string(want[j].Address.AsSlice()) }) |
| if diff := cmp.Diff(got, want); diff != "" { |
| t.Errorf("Interface addresses mismatch (-want +got):\n%s", diff) |
| } |
| } |
| |
| // Test adding a list of both IPV4 and IPV6 addresses and then removing them |
| // again one-by-one. |
| func TestListInterfaceAddresses(t *testing.T) { |
| ndpDisp := testNDPDispatcher{ |
| dadC: make(chan ndpDADEvent, 1), |
| } |
| ns, clock := newNetstack(t, netstackTestOptions{ndpDisp: &ndpDisp}) |
| ni := &stackImpl{ns: ns} |
| |
| ep := noopEndpoint{ |
| linkAddress: tcpip.LinkAddress([]byte{2, 3, 4, 5, 6, 7}), |
| } |
| ifState, err := ns.addEndpoint( |
| func(tcpip.NICID) string { return t.Name() }, |
| &ep, |
| &noopController{}, |
| nil, /* observer */ |
| defaultInterfaceMetric, /* metric */ |
| qdiscConfig{}, |
| ) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err := ifState.Up(); err != nil { |
| t.Fatal("ifState.Up(): ", err) |
| } |
| |
| // Wait for and account for any addresses added automatically. |
| clock.Advance(dadResolutionTimeout) |
| for { |
| select { |
| case d := <-ndpDisp.dadC: |
| t.Logf("startup DAD event: %#v", d) |
| continue |
| default: |
| } |
| break |
| } |
| |
| wantAddrs := getInterfaceAddresses(t, ni, ifState.nicid) |
| |
| testAddresses := []tcpip.AddressWithPrefix{ |
| {util.Parse("1.1.1.1"), 32}, |
| {util.Parse("2.2.2.2"), 24}, |
| {util.Parse("3.3.3.3"), 16}, |
| {util.Parse("4.4.4.4"), 8}, |
| {util.Parse("101:101:101:101:101:101:101:101"), 128}, |
| {util.Parse("202:202:202:202:202:202:202:202"), 64}, |
| {util.Parse("303:303:303:303:303:303:303:303"), 32}, |
| {util.Parse("404:404:404:404:404:404:404:404"), 8}, |
| } |
| |
| t.Run("Add", func(t *testing.T) { |
| for _, addr := range testAddresses { |
| t.Run(addr.String(), func(t *testing.T) { |
| ifAddr := fnet.Subnet{ |
| Addr: fidlconv.ToNetIpAddress(addr.Address), |
| PrefixLen: uint8(addr.PrefixLen), |
| } |
| protocolAddr := fidlconv.ToTCPIPProtocolAddress(ifAddr) |
| if ok, status := ifState.addAddress(protocolAddr, tcpipstack.AddressProperties{}); !ok { |
| t.Fatalf("ifState.addAddress(%#v): %s", protocolAddr, status) |
| } |
| expectDad := header.IsV6UnicastAddress(addr.Address) |
| clock.Advance(dadResolutionTimeout) |
| select { |
| case d := <-ndpDisp.dadC: |
| if !expectDad { |
| t.Fatalf("unexpected DAD event: %#v", d) |
| } |
| if diff := cmp.Diff(ndpDADEvent{nicID: ifState.nicid, addr: addr.Address, result: &tcpipstack.DADSucceeded{}}, d, cmp.AllowUnexported(d)); diff != "" { |
| t.Fatalf("ndp DAD event mismatch (-want +got):\n%s", diff) |
| } |
| default: |
| if expectDad { |
| t.Fatal("timed out waiting for DAD event") |
| } |
| } |
| wantAddrs = append(wantAddrs, addr) |
| gotAddrs := getInterfaceAddresses(t, ni, ifState.nicid) |
| |
| compareInterfaceAddresses(t, gotAddrs, wantAddrs) |
| }) |
| } |
| }) |
| |
| t.Run("Remove", func(t *testing.T) { |
| for _, addr := range testAddresses { |
| t.Run(addr.String(), func(t *testing.T) { |
| ifAddr := fnet.Subnet{ |
| Addr: fidlconv.ToNetIpAddress(addr.Address), |
| PrefixLen: uint8(addr.PrefixLen), |
| } |
| protocolAddr := fidlconv.ToTCPIPProtocolAddress(ifAddr) |
| if status := ifState.removeAddress(protocolAddr); status != zx.ErrOk { |
| t.Fatalf("ifState.removeAddress(%#v): %s", protocolAddr, status) |
| } |
| |
| // Remove address from list. |
| for i, a := range wantAddrs { |
| if a == addr { |
| wantAddrs = append(wantAddrs[:i], wantAddrs[i+1:]...) |
| break |
| } |
| } |
| gotAddrs := getInterfaceAddresses(t, ni, ifState.nicid) |
| compareInterfaceAddresses(t, gotAddrs, wantAddrs) |
| }) |
| } |
| }) |
| } |
| |
| func addAddressAndRoute(t *testing.T, ns *Netstack, ifState *ifState, addr tcpip.ProtocolAddress) { |
| if ok, reason := ifState.addAddress(addr, tcpipstack.AddressProperties{}); !ok { |
| t.Fatalf("ifState.addAddress(%s, {}): %s", addr.AddressWithPrefix, reason) |
| } |
| route := addressWithPrefixRoute(ifState.nicid, addr.AddressWithPrefix) |
| if _, err := ns.AddRoute(route, nil /* metric */, false /* dynamic */, true /* replaceMatchingGvisorRoutes */, routetypes.GlobalRouteSet()); err != nil { |
| t.Fatalf("ns.AddRoute(%s, 0, false): %s", route, err) |
| } |
| } |
| |
| func TestAddRouteParameterValidation(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| addr := tcpip.ProtocolAddress{ |
| Protocol: ipv4.ProtocolNumber, |
| AddressWithPrefix: tcpip.AddressWithPrefix{ |
| Address: util.Parse("240.240.240.240"), |
| PrefixLen: 24, |
| }, |
| } |
| subnetLocalAddress := util.Parse("240.240.240.241") |
| ifState := addNoopEndpoint(t, ns, "") |
| t.Cleanup(ifState.RemoveByUser) |
| |
| addAddressAndRoute(t, ns, ifState, addr) |
| |
| tests := []struct { |
| name string |
| route tcpip.Route |
| metric routetypes.Metric |
| dynamic bool |
| err error |
| }{ |
| { |
| name: "IPv4 destination no NIC invalid gateway", |
| route: tcpip.Route{ |
| Destination: util.PointSubnet(testV4Address), |
| Gateway: testV4Address, |
| NIC: 0, |
| }, |
| metric: routetypes.Metric(0), |
| err: routes.ErrNoSuchNIC, |
| }, |
| { |
| name: "IPv6 destination no NIC invalid gateway", |
| route: tcpip.Route{ |
| Destination: util.PointSubnet(testV6Address), |
| Gateway: testV6Address, |
| NIC: 0, |
| }, |
| metric: routetypes.Metric(0), |
| err: routes.ErrNoSuchNIC, |
| }, |
| { |
| name: "IPv4 destination no NIC valid gateway", |
| route: tcpip.Route{ |
| Destination: util.PointSubnet(testV4Address), |
| Gateway: subnetLocalAddress, |
| NIC: 0, |
| }, |
| }, |
| { |
| name: "zero length gateway", |
| route: tcpip.Route{ |
| Destination: util.PointSubnet(testV4Address), |
| NIC: ifState.nicid, |
| }, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| if _, err := ns.AddRoute(test.route, &test.metric, test.dynamic, true /* replaceMatchingGvisorRoutes */, routetypes.GlobalRouteSet()); !errors.Is(err, test.err) { |
| t.Errorf("got ns.AddRoute(...) = %v, want %v", err, test.err) |
| } |
| }) |
| } |
| } |
| |
| func extendedRouteTableCmpOptions() cmp.Options { |
| return []cmp.Option{ |
| cmp.AllowUnexported(tcpip.Subnet{}), |
| cmp.FilterPath(func(p cmp.Path) bool { |
| if p.Last().String() == "OwningSets" { |
| return false |
| } |
| return true |
| }, cmp.Ignore()), |
| } |
| } |
| |
| func TestDHCPAcquired(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| ifState := addNoopEndpoint(t, ns, "") |
| t.Cleanup(ifState.RemoveByUser) |
| |
| addressBytes := testV4Address.AsSlice() |
| nextAddress := func() tcpip.Address { |
| addressBytes[len(addressBytes)-1]++ |
| return tcpip.AddrFromSlice(addressBytes) |
| } |
| |
| serverAddress := nextAddress() |
| router1Address := nextAddress() |
| router2Address := nextAddress() |
| multicastAddress := net.IPv4(0xe8, 0x2b, 0xd3, 0xea) |
| if !multicastAddress.IsMulticast() { |
| t.Fatalf("%s is not a multicast address", multicastAddress) |
| } |
| |
| defaultMask := net.IP(testV4Address.AsSlice()).DefaultMask() |
| prefixLen, _ := defaultMask.Size() |
| |
| destination1, err := tcpip.NewSubnet(util.Parse("192.168.42.0"), util.ParseMask("255.255.255.0")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| destination2, err := tcpip.NewSubnet(util.Parse("0.0.0.0"), util.ParseMask("0.0.0.0")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // The multicast subnet routes that are implicitly installed on all devices. |
| ipv4MulticastSubnetRoute := routetypes.ExtendedRoute{ |
| Route: tcpip.Route{ |
| Destination: ipv4MulticastSubnet().Subnet(), |
| NIC: ifState.nicid, |
| }, |
| Prf: routetypes.MediumPreference, |
| MetricTracksInterface: true, |
| Metric: defaultInterfaceMetric, |
| Dynamic: false, |
| Enabled: true, |
| } |
| ipv6MulticastSubnetRoute := routetypes.ExtendedRoute{ |
| Route: tcpip.Route{ |
| Destination: ipv6MulticastSubnet().Subnet(), |
| NIC: ifState.nicid, |
| }, |
| Prf: routetypes.MediumPreference, |
| MetricTracksInterface: true, |
| Metric: defaultInterfaceMetric, |
| Dynamic: false, |
| Enabled: true, |
| } |
| |
| tests := []struct { |
| name string |
| oldAddr, newAddr tcpip.AddressWithPrefix |
| config dhcp.Config |
| expectedRouteTable []routetypes.ExtendedRoute |
| }{ |
| { |
| name: "subnet mask provided", |
| oldAddr: tcpip.AddressWithPrefix{}, |
| newAddr: tcpip.AddressWithPrefix{ |
| Address: testV4Address, |
| PrefixLen: prefixLen, |
| }, |
| config: dhcp.Config{ |
| ServerAddress: serverAddress, |
| Router: []tcpip.Address{ |
| router1Address, |
| router2Address, |
| header.IPv4Any, |
| header.IPv4Broadcast, |
| tcpip.AddrFromSlice(multicastAddress.To4()), |
| }, |
| SubnetMask: tcpip.MaskFromBytes(defaultMask), |
| DNS: []tcpip.Address{ |
| router1Address, |
| router2Address, |
| }, |
| LeaseLength: dhcp.Seconds(60), |
| }, |
| expectedRouteTable: []routetypes.ExtendedRoute{ |
| { |
| Route: tcpip.Route{ |
| Destination: destination1, |
| NIC: ifState.nicid, |
| }, |
| Prf: routetypes.MediumPreference, |
| Metric: defaultInterfaceMetric, |
| MetricTracksInterface: true, |
| Dynamic: true, |
| Enabled: false, |
| }, |
| ipv4MulticastSubnetRoute, |
| ipv6MulticastSubnetRoute, |
| { |
| Route: tcpip.Route{ |
| Destination: destination2, |
| Gateway: util.Parse("192.168.42.18"), |
| NIC: ifState.nicid, |
| }, |
| Prf: routetypes.MediumPreference, |
| Metric: defaultInterfaceMetric, |
| MetricTracksInterface: true, |
| Dynamic: true, |
| Enabled: false, |
| }, |
| { |
| Route: tcpip.Route{ |
| Destination: destination2, |
| Gateway: util.Parse("192.168.42.19"), |
| NIC: ifState.nicid, |
| }, |
| Prf: routetypes.MediumPreference, |
| Metric: defaultInterfaceMetric, |
| MetricTracksInterface: true, |
| Dynamic: true, |
| Enabled: false, |
| }, |
| }, |
| }, |
| { |
| name: "no routers", |
| oldAddr: tcpip.AddressWithPrefix{}, |
| newAddr: tcpip.AddressWithPrefix{ |
| Address: testV4Address, |
| PrefixLen: prefixLen, |
| }, |
| config: dhcp.Config{}, |
| expectedRouteTable: []routetypes.ExtendedRoute{ |
| { |
| Route: tcpip.Route{ |
| Destination: destination1, |
| NIC: ifState.nicid, |
| }, |
| Prf: routetypes.MediumPreference, |
| Metric: defaultInterfaceMetric, |
| MetricTracksInterface: true, |
| Dynamic: true, |
| Enabled: false, |
| }, |
| ipv4MulticastSubnetRoute, |
| ipv6MulticastSubnetRoute, |
| }, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| // save current route table for later |
| originalRouteTable := ifState.ns.GetExtendedRouteTable() |
| |
| // Update the DHCP address to the given test values and verify it took |
| // effect. |
| ifState.dhcpAcquired(context.Background(), test.oldAddr, test.newAddr, test.config) |
| |
| if diff := cmp.Diff(test.config.DNS, ifState.dns.mu.servers); diff != "" { |
| t.Errorf("ifState.mu.dnsServers mismatch (-want +got):\n%s", diff) |
| } |
| |
| if diff := cmp.Diff(test.expectedRouteTable, ifState.ns.GetExtendedRouteTable(), extendedRouteTableCmpOptions()); diff != "" { |
| t.Errorf("GetExtendedRouteTable() mismatch (-want +got):\n%s", diff) |
| } |
| |
| infoMap := ns.stack.NICInfo() |
| if info, ok := infoMap[ifState.nicid]; ok { |
| found := false |
| for _, address := range info.ProtocolAddresses { |
| if address.Protocol == ipv4.ProtocolNumber { |
| switch address.AddressWithPrefix { |
| case test.oldAddr: |
| t.Errorf("expired address %s was not removed from NIC addresses %v", test.oldAddr, info.ProtocolAddresses) |
| case test.newAddr: |
| found = true |
| } |
| } |
| } |
| |
| if !found { |
| t.Errorf("new address %s was not added to NIC addresses %v", test.newAddr, info.ProtocolAddresses) |
| } |
| } else { |
| t.Errorf("NIC %d not found in %v", ifState.nicid, infoMap) |
| } |
| |
| // Remove the address and verify everything is cleaned up correctly. |
| remAddr := test.newAddr |
| ifState.dhcpAcquired(context.Background(), remAddr, tcpip.AddressWithPrefix{}, dhcp.Config{}) |
| |
| if diff := cmp.Diff([]tcpip.Address(nil), ifState.dns.mu.servers); diff != "" { |
| t.Errorf("ifState.mu.dnsServers mismatch (-want +got):\n%s", diff) |
| } |
| |
| if diff := cmp.Diff(ifState.ns.GetExtendedRouteTable(), originalRouteTable, extendedRouteTableCmpOptions()); diff != "" { |
| t.Errorf("GetExtendedRouteTable() mismatch (-want +got):\n%s", diff) |
| } |
| |
| infoMap = ns.stack.NICInfo() |
| if info, ok := infoMap[ifState.nicid]; ok { |
| for _, address := range info.ProtocolAddresses { |
| if address.Protocol == ipv4.ProtocolNumber { |
| if address.AddressWithPrefix == remAddr { |
| t.Errorf("address %s/%d was not removed from NIC addresses %v", remAddr.Address, remAddr.PrefixLen, info.ProtocolAddresses) |
| } |
| } |
| } |
| } else { |
| t.Errorf("NIC %d not found in %v", ifState.nicid, infoMap) |
| } |
| }) |
| } |
| } |
| |
| func TestInFlightPacketsConsumeUDPSendBuffer(t *testing.T) { |
| ns, _ := newNetstack(t, netstackTestOptions{}) |
| linkEp := &sentinelEndpoint{} |
| linkEp.SetBlocking(true) |
| ifState := installAndValidateIface(t, ns, func(t *testing.T, ns *Netstack, name string) *ifState { |
| return addLinkEndpoint(t, ns, name, linkEp) |
| }) |
| t.Cleanup(ifState.RemoveByUser) |
| |
| addr := tcpip.ProtocolAddress{ |
| Protocol: ipv4.ProtocolNumber, |
| AddressWithPrefix: tcpip.AddressWithPrefix{ |
| Address: util.Parse("240.240.240.240"), |
| PrefixLen: 24, |
| }, |
| } |
| addAddressAndRoute(t, ns, ifState, addr) |
| |
| wq := new(waiter.Queue) |
| var eventOut struct { |
| mu sync.Mutex |
| returned bool |
| } |
| eventOutReturned := func() bool { |
| eventOut.mu.Lock() |
| defer eventOut.mu.Unlock() |
| return eventOut.returned |
| } |
| cb := func(m waiter.EventMask) { |
| eventOut.mu.Lock() |
| defer eventOut.mu.Unlock() |
| if m&waiter.EventOut != 0 { |
| eventOut.returned = true |
| } |
| } |
| entry := waiter.NewFunctionEntry(waiter.EventOut, cb) |
| wq.EventRegister(&entry) |
| |
| ep, err := ns.stack.NewEndpoint(header.UDPProtocolNumber, header.IPv4ProtocolNumber, wq) |
| if err != nil { |
| t.Fatalf("ns.stack.NewEndpoint(%d, %d, _): %s", header.UDPProtocolNumber, header.IPv4ProtocolNumber, err) |
| } |
| |
| fullAddress := &tcpip.FullAddress{ |
| NIC: ifState.nicid, |
| Addr: addr.AddressWithPrefix.Address, |
| Port: 42, |
| } |
| var r bytes.Reader |
| data := []byte{0, 1, 2, 3, 4} |
| validateWrite := func( |
| expectedCount int64, |
| expectedErr tcpip.Error, |
| ) error { |
| r.Reset(data) |
| writeOpts := tcpip.WriteOptions{ |
| To: fullAddress, |
| } |
| n, err := ep.Write(&r, writeOpts) |
| if diff := cmp.Diff(expectedErr, err); diff != "" { |
| return fmt.Errorf("got ep.Write(_, %#v) = (_, %#v), want (_, %#v)", writeOpts, err, expectedErr) |
| } |
| if n != expectedCount { |
| return fmt.Errorf("got ep.Write(_, %#v) = (%d, _), want (%d, _)", writeOpts, n, expectedCount) |
| } |
| return nil |
| } |
| |
| // Write() succeeds when length(data) > sizeof(send buf) > 0; thereafter, |
| // the send buffer is exhausted. |
| ep.SocketOptions().SetSendBufferSize(int64(len(data))-1, false) |
| if err := validateWrite(int64(len(data)), nil); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Expect Write() blocks when the send buffer is exhausted. |
| if err := validateWrite(0, &tcpip.ErrWouldBlock{}); err != nil { |
| t.Fatal(err) |
| } |
| |
| if eventOutReturned() { |
| t.Fatalf("got waiter.EventOut before endpoint was unblocked") |
| } |
| linkEp.Drain() |
| if !eventOutReturned() { |
| t.Fatalf("expect waiter.EventOut signaled when endpoint is unblocked") |
| } |
| |
| // Expect Write() succeeds now that the send buffer has space. |
| if err := validateWrite(int64(len(data)), nil); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| func cmpAtomicUint32(a, b atomic.Uint32) bool { |
| return a.Load() == b.Load() |
| } |
| |
| func TestNetworkSocketOptionStatsUpdateMax(t *testing.T) { |
| var candidate NetworkSocketOptionStats |
| candidate.SetReuseAddress.Add(1) |
| |
| var max MaxSocketOptionStats |
| max.GetReuseAddress.Add(2) |
| max.updateMax(&candidate) |
| |
| var want MaxSocketOptionStats |
| want.SetReuseAddress.Add(1) |
| want.GetReuseAddress.Add(2) |
| if diff := cmp.Diff(want, max, cmp.Comparer(cmpAtomicUint32)); diff != "" { |
| t.Fatalf("unexpected value after updateMax (-want +got):\n%s", diff) |
| } |
| } |
| |
| func TestStreamSocketOptionStatsUpdateMax(t *testing.T) { |
| candidate := streamSocketOptionStats{ |
| NetworkSocketOptionStats: &NetworkSocketOptionStats{}, |
| } |
| // These two stats are in different embedded types in streamSocketOptionStats. |
| candidate.SetReuseAddress.Add(1) |
| candidate.SetTcpNoDelay.Add(1) |
| |
| var max MaxSocketOptionStats |
| max.GetReuseAddress.Add(2) |
| max.GetTcpNoDelay.Add(2) |
| max.updateMax(&candidate) |
| |
| var want MaxSocketOptionStats |
| want.SetReuseAddress.Add(1) |
| want.SetTcpNoDelay.Add(1) |
| want.GetReuseAddress.Add(2) |
| want.GetTcpNoDelay.Add(2) |
| if diff := cmp.Diff(want, max, cmp.Comparer(cmpAtomicUint32)); diff != "" { |
| t.Fatalf("unexpected value after updateMax (-want +got):\n%s", diff) |
| } |
| } |
| |
| func TestRawSocketOptionStatsUpdateMax(t *testing.T) { |
| candidate := rawSocketOptionStats{ |
| NetworkSocketOptionStats: &NetworkSocketOptionStats{}, |
| } |
| // These two stats are in different embedded types in rawSocketOptionStats. |
| candidate.SetReuseAddress.Add(1) |
| candidate.SetIpHeaderIncluded.Add(1) |
| |
| var max MaxSocketOptionStats |
| max.GetReuseAddress.Add(2) |
| max.GetIpHeaderIncluded.Add(2) |
| max.updateMax(&candidate) |
| |
| var want MaxSocketOptionStats |
| want.SetReuseAddress.Add(1) |
| want.SetIpHeaderIncluded.Add(1) |
| want.GetReuseAddress.Add(2) |
| want.GetIpHeaderIncluded.Add(2) |
| if diff := cmp.Diff(want, max, cmp.Comparer(cmpAtomicUint32)); diff != "" { |
| t.Fatalf("unexpected value after updateMax (-want +got):\n%s", diff) |
| } |
| } |