blob: fd97727f530478a8ac21124581e9b5739ead38cd [file] [log] [blame]
// Copyright 2020 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 (
"context"
"errors"
"fmt"
"syscall/zx"
"testing"
"time"
"go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/dhcp"
"go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/fidlconv"
"go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/routetypes"
"go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/sync"
zxtime "go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/time"
"go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/util"
fnet "fidl/fuchsia/net"
"fidl/fuchsia/net/interfaces"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
const testId = 1
const negativeTimeout = 50 * time.Millisecond
func testIpv4Subnet() fnet.Subnet {
return fnet.Subnet{
Addr: fnet.IpAddressWithIpv4(fnet.Ipv4Address{Addr: [4]uint8{1, 2, 3, 4}}),
PrefixLen: 16,
}
}
func testIpv4Address() interfaces.Address {
var addr interfaces.Address
addr.SetAddr(testIpv4Subnet())
addr.SetValidUntil(int64(zx.TimensecInfinite))
addr.SetPreferredLifetimeInfo(interfaces.PreferredLifetimeInfoWithPreferredUntil(int64(zx.TimensecInfinite)))
addr.SetAssignmentState(interfaces.AddressAssignmentStateAssigned)
return addr
}
func testProperties() interfaces.Properties {
var properties interfaces.Properties
properties.SetId(testId)
properties.SetName("testif01")
properties.SetDeviceClass(interfaces.DeviceClassWithLoopback(interfaces.Empty{}))
properties.SetOnline(true)
properties.SetHasDefaultIpv4Route(true)
properties.SetHasDefaultIpv6Route(true)
properties.SetAddresses([]interfaces.Address{})
return properties
}
// Starts the interface watcher event loop.
//
// Note that this function registers a cleanup function to stop the event
// loop, so tests which need newNetstack to construct a netstack must
// call this function first as the netstack cleanup tasks rely on the
// interface watcher event loop to still be running.
func startEventLoop(t *testing.T) (chan interfaceEvent, chan interfaceWatcherRequest) {
eventChan := make(chan interfaceEvent)
watcherChan := make(chan interfaceWatcherRequest)
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
t.Cleanup(func() {
cancel()
wg.Wait()
})
wg.Add(1)
go func() {
defer wg.Done()
interfaceWatcherEventLoop(ctx, eventChan, watcherChan, &fidlInterfaceWatcherStats{})
}()
return eventChan, watcherChan
}
func assertWatchResult(gotEvent interfaces.Event, gotErr error, wantEvent interfaces.Event) error {
if gotErr != nil {
return fmt.Errorf("Watch failed: %w", gotErr)
}
if diff := cmp.Diff(wantEvent, gotEvent, cmpopts.IgnoreTypes(struct{}{}), cmpopts.EquateEmpty()); diff != "" {
return fmt.Errorf("(-want +got)\n%s", diff)
}
return nil
}
type watchResult struct {
event interfaces.Event
err error
}
type watcherHelper struct {
*interfaces.WatcherWithCtxInterface
}
func optionsWithFullInterest() interfaces.WatcherOptions {
var options interfaces.WatcherOptions
options.SetAddressPropertiesInterest(interfaces.AddressPropertiesInterestValidUntil | interfaces.AddressPropertiesInterestPreferredLifetimeInfo)
return options
}
func initWatcher(t *testing.T, si *interfaceStateImpl, options interfaces.WatcherOptions) watcherHelper {
request, watcher, err := interfaces.NewWatcherWithCtxInterfaceRequest()
if err != nil {
t.Fatalf("failed to create Watcher protocol channel pair: %s", err)
}
if err := si.GetWatcher(context.Background(), options, request); err != nil {
t.Fatalf("failed to call GetWatcher: %s", err)
}
return watcherHelper{
WatcherWithCtxInterface: watcher,
}
}
func (w *watcherHelper) expectIdleEvent(t *testing.T) {
t.Helper()
event, err := w.Watch(context.Background())
if err := assertWatchResult(event, err, interfaces.EventWithIdle(interfaces.Empty{})); err != nil {
t.Fatal(err)
}
}
// Call `Watch` on the provided watcher, expecting the call to block because
// no `watchResult` is immediately ready.
//
// Note: This function makes a best effort attempt to ensure `Watch` has
// been called before it returning, but it cannot guarantee it. In certain
// execution contexts (i.e. Fuchsia's CQ) it's possible for `negativeTimeout` to
// expire, without the watch goroutine having been scheduled. As such, this
// function should only be used in negative checks (e.g. verifying an event did
// not occur).
func (w *watcherHelper) blockingWatch(t *testing.T, ch chan watchResult) {
go func() {
event, err := w.Watch(context.Background())
ch <- watchResult{
event: event,
err: err,
}
}()
select {
case got := <-ch:
t.Fatalf("Watch did not block and completed with: %#v", got)
case <-zxtime.After(zxtime.Duration(negativeTimeout)):
}
}
func TestInterfacesWatcherDisallowMultiplePending(t *testing.T) {
_, watcherChan := startEventLoop(t)
si := &interfaceStateImpl{watcherChan: watcherChan}
watcher := initWatcher(t, si, optionsWithFullInterest())
watcher.expectIdleEvent(t)
var wg sync.WaitGroup
defer wg.Wait()
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, err := watcher.Watch(context.Background())
var gotErr *zx.Error
if !(errors.As(err, &gotErr) && gotErr.Status == zx.ErrPeerClosed) {
t.Errorf("got watcher.Watch() = (_, %s), want %s", err, zx.ErrPeerClosed)
}
}()
}
}
func TestInterfacesWatcherExisting(t *testing.T) {
eventChan, watcherChan := startEventLoop(t)
ns, _ := newNetstack(t, netstackTestOptions{interfaceEventChan: eventChan})
si := &interfaceStateImpl{watcherChan: watcherChan}
ifs := addNoopEndpoint(t, ns, "")
watcher := initWatcher(t, si, optionsWithFullInterest())
defer func() {
if err := watcher.Close(); err != nil {
t.Errorf("failed to close watcher: %s", err)
}
}()
event, err := watcher.Watch(context.Background())
if err := assertWatchResult(event, err, interfaces.EventWithExisting(initialProperties(ifs, ns.name(ifs.nicid)))); err != nil {
t.Fatal(err)
}
watcher.expectIdleEvent(t)
}
func TestInterfacesWatcher(t *testing.T) {
eventChan, watcherChan := startEventLoop(t)
ndpDisp := newNDPDispatcher()
ns, _ := newNetstack(t, netstackTestOptions{
interfaceEventChan: eventChan,
ndpDisp: ndpDisp,
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ndpDisp.start(ctx)
si := &interfaceStateImpl{watcherChan: watcherChan}
// The first watcher will always block, while the second watcher should never block.
blockingWatcher, nonBlockingWatcher := initWatcher(t, si, optionsWithFullInterest()), initWatcher(t, si, optionsWithFullInterest())
ch := make(chan watchResult)
defer func() {
// NB: The blocking watcher closed at the end of the test instead of deferred as
// additional assertions are made with it.
if err := nonBlockingWatcher.Close(); err != nil {
t.Errorf("failed to close non-blocking watcher: %s", err)
}
close(ch)
}()
blockingWatcher.expectIdleEvent(t)
nonBlockingWatcher.expectIdleEvent(t)
blockingWatcher.blockingWatch(t, ch)
// Add an interface.
ifs := addNoopEndpoint(t, ns, "")
verifyWatchResults := func(wantEvent interfaces.Event) error {
event, err := nonBlockingWatcher.Watch(context.Background())
if err := assertWatchResult(event, err, wantEvent); err != nil {
return fmt.Errorf("non-blocking watcher error: %w", err)
}
got := <-ch
if err := assertWatchResult(got.event, got.err, wantEvent); err != nil {
return fmt.Errorf("blocking watcher error: %w", err)
}
return nil
}
if err := verifyWatchResults(interfaces.EventWithAdded(initialProperties(ifs, ns.name(ifs.nicid)))); err != nil {
t.Fatal(err)
}
// Set interface up.
blockingWatcher.blockingWatch(t, ch)
if err := ifs.Up(); err != nil {
t.Fatalf("failed to set interface up: %s", err)
}
var id interfaces.Properties
id.SetId(uint64(ifs.nicid))
online := id
online.SetOnline(true)
if err := verifyWatchResults(interfaces.EventWithChanged(online)); err != nil {
t.Fatal(err)
}
// Add and remove addresses.
for _, protocolAddr := range []tcpip.ProtocolAddress{
{
Protocol: header.IPv4ProtocolNumber,
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: util.Parse("1.2.3.4"),
PrefixLen: 16,
},
},
{
Protocol: header.IPv6ProtocolNumber,
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: util.Parse("abcd::1"),
PrefixLen: 64,
},
},
} {
blockingWatcher.blockingWatch(t, ch)
if ok, reason := ifs.addAddress(protocolAddr, stack.AddressProperties{}); !ok {
t.Fatalf("ifs.addAddress(%s, {}): %s", protocolAddr.AddressWithPrefix, reason)
}
addressAdded := id
var address interfaces.Address
address.SetAddr(fidlconv.ToNetSubnet(protocolAddr.AddressWithPrefix))
address.SetValidUntil(int64(zx.TimensecInfinite))
address.SetPreferredLifetimeInfo(interfaces.PreferredLifetimeInfoWithPreferredUntil(int64(zx.TimensecInfinite)))
address.SetAssignmentState(interfaces.AddressAssignmentStateAssigned)
addressAdded.SetAddresses([]interfaces.Address{address})
if err := verifyWatchResults(interfaces.EventWithChanged(addressAdded)); err != nil {
t.Fatal(err)
}
blockingWatcher.blockingWatch(t, ch)
if zxStatus := ifs.removeAddress(protocolAddr); zxStatus != zx.ErrOk {
t.Fatalf("ifs.removeAddress(%s): %s", protocolAddr.AddressWithPrefix, zxStatus)
}
addressRemoved := id
addressRemoved.SetAddresses([]interfaces.Address{})
if err := verifyWatchResults(interfaces.EventWithChanged(addressRemoved)); err != nil {
t.Fatal(err)
}
}
// Add a default route.
blockingWatcher.blockingWatch(t, ch)
r := defaultV4Route(ifs.nicid, util.Parse("1.2.3.5"))
if _, err := ns.AddRoute(r, nil /* metric */, false, true /* replaceMatchingGvisorRoutes */, routetypes.GlobalRouteSet()); err != nil {
t.Fatalf("failed to add default route: %s", err)
}
defaultIpv4RouteAdded := id
defaultIpv4RouteAdded.SetHasDefaultIpv4Route(true)
if err := verifyWatchResults(interfaces.EventWithChanged(defaultIpv4RouteAdded)); err != nil {
t.Fatal(err)
}
// Remove the default route.
blockingWatcher.blockingWatch(t, ch)
_ = ns.DelRoute(r, routetypes.GlobalRouteSet())
defaultIpv4RouteRemoved := id
defaultIpv4RouteRemoved.SetHasDefaultIpv4Route(false)
if err := verifyWatchResults(interfaces.EventWithChanged(defaultIpv4RouteRemoved)); err != nil {
t.Fatal(err)
}
// DHCP Acquired on the interface.
blockingWatcher.blockingWatch(t, ch)
addr := fnet.Ipv4Address{Addr: [4]uint8{192, 168, 0, 4}}
acquiredAddr := tcpip.AddressWithPrefix{Address: tcpip.AddrFrom4Slice(addr.Addr[:]), PrefixLen: 24}
leaseLength := dhcp.Seconds(10)
initUpdatedAt := zxtime.Monotonic(42)
ifs.dhcpAcquired(context.Background(), tcpip.AddressWithPrefix{}, acquiredAddr, dhcp.Config{UpdatedAt: initUpdatedAt, LeaseLength: leaseLength})
dhcpAddressAdded := id
var address interfaces.Address
address.SetAddr(fnet.Subnet{
Addr: fnet.IpAddressWithIpv4(addr),
PrefixLen: uint8(acquiredAddr.PrefixLen),
})
address.SetValidUntil(initUpdatedAt.Add(leaseLength.Duration()).MonotonicNano())
address.SetPreferredLifetimeInfo(interfaces.PreferredLifetimeInfoWithPreferredUntil(int64(zx.TimensecInfinite)))
address.SetAssignmentState(interfaces.AddressAssignmentStateAssigned)
dhcpAddressAdded.SetAddresses([]interfaces.Address{address})
if err := verifyWatchResults(interfaces.EventWithChanged(dhcpAddressAdded)); err != nil {
t.Fatal(err)
}
// DHCP Acquired with same valid_until does not produce event.
ifs.dhcpAcquired(context.Background(), acquiredAddr, acquiredAddr, dhcp.Config{UpdatedAt: initUpdatedAt, LeaseLength: leaseLength})
blockingWatcher.blockingWatch(t, ch)
// DHCP Acquired with different valid_until.
updatedAt := zxtime.Monotonic(100)
ifs.dhcpAcquired(context.Background(), acquiredAddr, acquiredAddr, dhcp.Config{UpdatedAt: updatedAt, LeaseLength: leaseLength})
dhcpAddressRenewed := id
address.SetValidUntil(updatedAt.Add(leaseLength.Duration()).MonotonicNano())
dhcpAddressRenewed.SetAddresses([]interfaces.Address{address})
if err := verifyWatchResults(interfaces.EventWithChanged(dhcpAddressRenewed)); err != nil {
t.Fatal(err)
}
// DHCP Acquired on empty address signaling end of lease.
blockingWatcher.blockingWatch(t, ch)
ifs.dhcpAcquired(context.Background(), acquiredAddr, tcpip.AddressWithPrefix{}, dhcp.Config{})
dhcpExpired := id
dhcpExpired.SetAddresses([]interfaces.Address{})
if err := verifyWatchResults(interfaces.EventWithChanged(dhcpExpired)); err != nil {
t.Fatal(err)
}
// Set interface down.
blockingWatcher.blockingWatch(t, ch)
if err := ifs.Down(); err != nil {
t.Fatalf("failed to set interface down: %s", err)
}
offline := id
offline.SetOnline(false)
if err := verifyWatchResults(interfaces.EventWithChanged(offline)); err != nil {
t.Fatal(err)
}
// Remove the interface.
blockingWatcher.blockingWatch(t, ch)
ifs.RemoveByUser()
if err := verifyWatchResults(interfaces.EventWithRemoved(uint64(ifs.nicid))); err != nil {
t.Fatal(err)
}
}
// TestInterfacesWatcherExistingDeepCopyAddresses ensures that changes to address
// properties do not get retroactively applied to Existing events enqueued in the past.
func TestInterfacesWatcherExistingDeepCopyAddresses(t *testing.T) {
eventChan, watcherChan := startEventLoop(t)
si := &interfaceStateImpl{watcherChan: watcherChan}
initialProperties := testProperties()
{
e := interfaceAdded(initialProperties)
eventChan <- &e
}
fakeAddressChanged := func(validUntil tcpip.MonotonicTime) addressChanged {
return addressChanged{
nicid: tcpip.NICID(testId),
protocolAddr: fidlconv.ToTCPIPProtocolAddress(testIpv4Subnet()),
lifetimes: stack.AddressLifetimes{
Deprecated: false,
PreferredUntil: tcpip.MonotonicTimeInfinite(),
ValidUntil: validUntil,
},
state: stack.AddressAssigned,
}
}
{
e := fakeAddressChanged(tcpip.MonotonicTimeInfinite())
eventChan <- &e
}
// Initialize a watcher so that there is a queued Existing event with the
// address.
watcher := initWatcher(t, si, optionsWithFullInterest())
defer func() {
if err := watcher.Close(); err != nil {
t.Errorf("failed to close watcher: %s", err)
}
}()
validUntil := []time.Duration{
time.Hour,
time.Hour * 2,
}
for _, validUntil := range validUntil {
e := fakeAddressChanged(tcpip.MonotonicTime{}.Add(validUntil))
eventChan <- &e
}
// Read all the queued events.
wantProperties := initialProperties
wantProperties.SetAddresses([]interfaces.Address{testIpv4Address()})
event, err := watcher.Watch(context.Background())
if err := assertWatchResult(event, err, interfaces.EventWithExisting(wantProperties)); err != nil {
t.Fatal(err)
}
watcher.expectIdleEvent(t)
for i, validUntil := range validUntil {
var wantChange interfaces.Properties
wantChange.SetId(testId)
addr := testIpv4Address()
addr.SetValidUntil(validUntil.Nanoseconds())
wantChange.SetAddresses([]interfaces.Address{addr})
event, err = watcher.Watch(context.Background())
if err := assertWatchResult(event, err, interfaces.EventWithChanged(wantChange)); err != nil {
t.Fatalf("valid-until change index %d mismatch: %s", i, err)
}
}
}
func TestInterfacesWatcherInterest(t *testing.T) {
for _, tc := range []struct {
name string
disinterest interfaces.AddressPropertiesInterest
}{
{
name: "validUntil",
disinterest: interfaces.AddressPropertiesInterestValidUntil,
},
{
name: "preferredLifetimeInfo",
disinterest: interfaces.AddressPropertiesInterestPreferredLifetimeInfo,
},
} {
t.Run(tc.name, func(t *testing.T) {
eventChan, watcherChan := startEventLoop(t)
ns, _ := newNetstack(t, netstackTestOptions{
interfaceEventChan: eventChan,
})
si := &interfaceStateImpl{watcherChan: watcherChan}
interestedWatcher := initWatcher(t, si, optionsWithFullInterest())
disinterestedWatcher := func() watcherHelper {
var options interfaces.WatcherOptions
options.SetAddressPropertiesInterest(tc.disinterest.InvertBits())
return initWatcher(t, si, options)
}()
watchers := []*watcherHelper{&interestedWatcher, &disinterestedWatcher}
defer func() {
for i, watcher := range watchers {
if err := watcher.Close(); err != nil {
t.Errorf("failed to close watcher %d: %s", i, err)
}
}
}()
ifs := addNoopEndpoint(t, ns, "")
// Must bring up the interface so that addresses
// can be observed via the watcher.
if err := ifs.Up(); err != nil {
t.Fatalf("ifs.Up() = %s", err)
}
for i, watcher := range watchers {
watcher.expectIdleEvent(t)
event, err := watcher.Watch(context.Background())
if err := assertWatchResult(event, err, interfaces.EventWithAdded(initialProperties(ifs, ns.name(ifs.nicid)))); err != nil {
t.Fatalf("watcher index %d error: %s", i, err)
}
{
var onlineChanged interfaces.Properties
onlineChanged.SetId(testId)
onlineChanged.SetOnline(true)
event, err := watcher.Watch(context.Background())
if err := assertWatchResult(event, err, interfaces.EventWithChanged(onlineChanged)); err != nil {
t.Fatalf("watcher index %d error: %s", i, err)
}
}
}
ifs.addAddress(fidlconv.ToTCPIPProtocolAddress(testIpv4Subnet()), stack.AddressProperties{
Lifetimes: stack.AddressLifetimes{
Deprecated: false,
PreferredUntil: tcpip.MonotonicTimeInfinite(),
ValidUntil: tcpip.MonotonicTimeInfinite(),
},
})
// Interested watcher receives all fields; disinterested watcher does
// not observe the field it isn't interested in.
{
event, err := interestedWatcher.Watch(context.Background())
var want interfaces.Properties
want.SetId(testId)
want.SetAddresses([]interfaces.Address{testIpv4Address()})
if err := assertWatchResult(event, err, interfaces.EventWithChanged(want)); err != nil {
t.Fatalf("interested watcher should receive all fields when address is added: %s", err)
}
}
{
event, err := disinterestedWatcher.Watch(context.Background())
want := func() interfaces.Properties {
var want interfaces.Properties
want.SetId(testId)
addr := testIpv4Address()
switch tc.disinterest {
case interfaces.AddressPropertiesInterestValidUntil:
addr.SetValidUntil(0)
addr.ClearValidUntil()
case interfaces.AddressPropertiesInterestPreferredLifetimeInfo:
addr.SetPreferredLifetimeInfo(interfaces.PreferredLifetimeInfo{})
addr.ClearPreferredLifetimeInfo()
default:
t.Fatalf("unexpected disinterest: %s", tc.disinterest)
}
want.SetAddresses([]interfaces.Address{addr})
return want
}()
if err := assertWatchResult(event, err, interfaces.EventWithChanged(want)); err != nil {
t.Fatalf("disinterested watcher should not receive %s when address is added: %s", tc.name, err)
}
}
lifetimes := stack.AddressLifetimes{
Deprecated: false,
PreferredUntil: tcpip.MonotonicTimeInfinite(),
ValidUntil: tcpip.MonotonicTimeInfinite(),
}
const changedLifetime = time.Hour
switch tc.disinterest {
case interfaces.AddressPropertiesInterestValidUntil:
lifetimes.ValidUntil = tcpip.MonotonicTime{}.Add(changedLifetime)
case interfaces.AddressPropertiesInterestPreferredLifetimeInfo:
lifetimes.PreferredUntil = tcpip.MonotonicTime{}.Add(changedLifetime)
default:
t.Fatalf("unexpected disinterest: %s", tc.disinterest)
}
ifs.ns.stack.SetAddressLifetimes(tcpip.NICID(testId), fidlconv.ToTCPIPAddressWithPrefix(testIpv4Subnet()).Address, lifetimes)
ifs.removeAddress(fidlconv.ToTCPIPProtocolAddress(testIpv4Subnet()))
// Interested watcher receives the changed event and then the
// address-removed event; disinterested watcher only receives the
// address-removed event.
{
event, err := interestedWatcher.Watch(context.Background())
want := func() interfaces.Properties {
var properties interfaces.Properties
properties.SetId(testId)
addr := testIpv4Address()
switch tc.disinterest {
case interfaces.AddressPropertiesInterestValidUntil:
addr.SetValidUntil(changedLifetime.Nanoseconds())
case interfaces.AddressPropertiesInterestPreferredLifetimeInfo:
addr.SetPreferredLifetimeInfo(interfaces.PreferredLifetimeInfoWithPreferredUntil(changedLifetime.Nanoseconds()))
default:
t.Fatalf("unexpected disinterest: %s", tc.disinterest)
}
properties.SetAddresses([]interfaces.Address{addr})
return properties
}()
if err := assertWatchResult(event, err, interfaces.EventWithChanged(want)); err != nil {
t.Fatalf("interested watcher receives %s change: %s", tc.name, err)
}
}
for i, watcher := range watchers {
event, err := watcher.Watch(context.Background())
var want interfaces.Properties
want.SetId(testId)
want.SetAddresses(nil)
if err := assertWatchResult(event, err, interfaces.EventWithChanged(want)); err != nil {
t.Fatalf("failed to observe address-removed event on watcher index %d: %s", i, err)
}
}
})
}
}
// TestInterfacesWatcherAddressState tests that the interface watcher event
// loop keeps track of address state correctly by emitting fake state change
// events and ensuring the address appears or disappears as appropriate.
func TestInterfacesWatcherAddressState(t *testing.T) {
states := []stack.AddressAssignmentState{
stack.AddressAssigned,
stack.AddressTentative,
stack.AddressDisabled,
}
for _, fromState := range states {
for _, toState := range states {
if fromState != toState {
t.Run(fmt.Sprintf("%s_to_%s", fromState, toState), func(t *testing.T) {
protocolAddr := tcpip.ProtocolAddress{
Protocol: header.IPv6ProtocolNumber,
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: util.Parse("abcd::1"),
PrefixLen: 64,
},
}
eventChan, watcherChan := startEventLoop(t)
ns, _ := newNetstack(t, netstackTestOptions{
interfaceEventChan: eventChan,
})
si := &interfaceStateImpl{watcherChan: watcherChan}
ifs := addNoopEndpoint(t, ns, "")
// Must bring up the interface as otherwise IPv6 addresses
// in Tentative or Disabled are not observed.
if err := ifs.Up(); err != nil {
t.Fatalf("ifs.Up() = %s", err)
}
watcher := initWatcher(t, si, optionsWithFullInterest())
defer func() {
if err := watcher.Close(); err != nil {
t.Fatalf("failed to close watcher: %s", err)
}
}()
event, err := watcher.Watch(context.Background())
properties := initialProperties(ifs, ns.name(ifs.nicid))
properties.SetOnline(true)
if err := assertWatchResult(event, err, interfaces.EventWithExisting(properties)); err != nil {
t.Fatal(err)
}
watcher.expectIdleEvent(t)
// Add an IPv6 address, since DAD is disabled should
// immediately observe it as assigned.
ifs.addAddress(protocolAddr, stack.AddressProperties{})
var wantAddress interfaces.Address
wantAddress.SetAddr(fidlconv.ToNetSubnet(protocolAddr.AddressWithPrefix))
wantAddress.SetValidUntil(int64(zx.TimensecInfinite))
wantAddress.SetPreferredLifetimeInfo(interfaces.PreferredLifetimeInfoWithPreferredUntil(int64(zx.TimensecInfinite)))
wantAddress.SetAssignmentState(interfaces.AddressAssignmentStateAssigned)
var propertiesWithAddress interfaces.Properties
propertiesWithAddress.SetId(uint64(ifs.nicid))
propertiesWithAddress.SetAddresses([]interfaces.Address{wantAddress})
var propertiesWithoutAddress interfaces.Properties
propertiesWithoutAddress.SetId(uint64(ifs.nicid))
propertiesWithoutAddress.SetAddresses(nil)
event, err = watcher.Watch(context.Background())
if err := assertWatchResult(event, err, interfaces.EventWithChanged(propertiesWithAddress)); err != nil {
t.Fatal(err)
}
var states []stack.AddressAssignmentState
if fromState != stack.AddressAssigned {
states = append(states, fromState)
}
states = append(states, toState)
if toState != stack.AddressAssigned {
states = append(states, stack.AddressAssigned)
}
currentState := stack.AddressAssigned
for _, nextState := range states {
// Fake an event changing the assignment state.
ns.interfaceEventChan <- &addressChanged{
nicid: ifs.nicid,
protocolAddr: protocolAddr,
state: nextState,
}
change, changed := func() (interfaces.Properties, bool) {
if currentState == stack.AddressAssigned &&
(nextState == stack.AddressDisabled || nextState == stack.AddressTentative) {
return propertiesWithoutAddress, true
} else if (currentState == stack.AddressDisabled || currentState == stack.AddressTentative) &&
nextState == stack.AddressAssigned {
return propertiesWithAddress, true
}
return interfaces.Properties{}, false
}()
if changed {
event, err := watcher.Watch(context.Background())
if err := assertWatchResult(event, err, interfaces.EventWithChanged(change)); err != nil {
t.Fatalf("state %s to %s: %s", currentState, nextState, err)
}
}
currentState = nextState
}
// Remove the address and observe removal.
if status := ifs.removeAddress(protocolAddr); status != zx.ErrOk {
t.Fatalf("ifs.removeAddress(%#v) = %s", protocolAddr, status)
}
{
event, err := watcher.Watch(context.Background())
if err := assertWatchResult(event, err, interfaces.EventWithChanged(propertiesWithoutAddress)); err != nil {
t.Fatal(err)
}
}
})
}
}
}
}
func TestAddressesChangeType(t *testing.T) {
for _, tc := range []struct {
name string
previouslyKnown bool
prevProperties, nextProperties addressProperties
wantChanges changedAddressProperties
}{
{
name: "not previously known and visible",
previouslyKnown: false,
prevProperties: addressProperties{},
nextProperties: addressProperties{
state: stack.AddressAssigned,
},
wantChanges: changedAddressProperties{
properties: interfaces.AddressPropertiesInterest(0),
assignmentState: assignmentStateChangeInvolvingAssigned,
},
},
{
name: "not previously known and not visible",
previouslyKnown: false,
prevProperties: addressProperties{},
nextProperties: addressProperties{
state: stack.AddressDisabled,
},
wantChanges: changedAddressProperties{
properties: interfaces.AddressPropertiesInterest(0),
assignmentState: assignmentStateChangeNotInvolvingAssigned,
},
},
{
name: "invisible to visible",
previouslyKnown: true,
prevProperties: addressProperties{
state: stack.AddressDisabled,
},
nextProperties: addressProperties{
state: stack.AddressAssigned,
},
wantChanges: changedAddressProperties{
properties: interfaces.AddressPropertiesInterest(0),
assignmentState: assignmentStateChangeInvolvingAssigned,
},
},
{
name: "visible to invisible",
previouslyKnown: true,
prevProperties: addressProperties{
state: stack.AddressAssigned,
},
nextProperties: addressProperties{
state: stack.AddressDisabled,
},
wantChanges: changedAddressProperties{
properties: interfaces.AddressPropertiesInterest(0),
assignmentState: assignmentStateChangeInvolvingAssigned,
},
},
{
name: "invisible property change",
previouslyKnown: true,
prevProperties: addressProperties{
state: stack.AddressDisabled,
lifetimes: stack.AddressLifetimes{
Deprecated: true,
},
},
nextProperties: addressProperties{
state: stack.AddressDisabled,
lifetimes: stack.AddressLifetimes{
Deprecated: false,
},
},
wantChanges: changedAddressProperties{
properties: interfaces.AddressPropertiesInterestPreferredLifetimeInfo,
assignmentState: assignmentStateUnchangedAtNonassigned,
},
},
{
name: "visible property change",
previouslyKnown: true,
prevProperties: addressProperties{
state: stack.AddressAssigned,
lifetimes: stack.AddressLifetimes{
Deprecated: true,
},
},
nextProperties: addressProperties{
state: stack.AddressAssigned,
lifetimes: stack.AddressLifetimes{
Deprecated: false,
},
},
wantChanges: changedAddressProperties{
properties: interfaces.AddressPropertiesInterestPreferredLifetimeInfo,
assignmentState: assignmentStateUnchangedAtAssigned,
},
},
} {
t.Run(tc.name, func(t *testing.T) {
if gotChanges := addressesChangeType(
tc.previouslyKnown,
tc.prevProperties,
tc.nextProperties,
); gotChanges != tc.wantChanges {
t.Errorf("got addressChangeType(%t, %#v, %#v) = %#v, want = %#v",
tc.previouslyKnown,
tc.prevProperties,
tc.nextProperties,
gotChanges,
tc.wantChanges,
)
}
})
}
}
func TestAddressToString(t *testing.T) {
subnet := testIpv4Subnet()
for _, tc := range []struct {
name string
fn func(*interfaces.Address)
want string
}{
{
name: "no lifetimes",
fn: func(a *interfaces.Address) {},
want: "{Addr:1.2.3.4/16}",
},
{
name: "infinite valid-until and preferred-until",
fn: func(a *interfaces.Address) {
a.SetValidUntil(int64(zx.TimensecInfinite))
a.SetPreferredLifetimeInfo(interfaces.PreferredLifetimeInfoWithPreferredUntil(int64(zx.TimensecInfinite)))
},
want: "{Addr:1.2.3.4/16, ValidUntil:boot+2562047h47m16.854775807s, PreferredLifetimeInfo:boot+2562047h47m16.854775807s}",
},
{
name: "finite valid-until and deprecated",
fn: func(a *interfaces.Address) {
a.SetValidUntil(int64(60_000_000_000))
a.SetPreferredLifetimeInfo(interfaces.PreferredLifetimeInfoWithDeprecated(interfaces.Empty{}))
},
want: "{Addr:1.2.3.4/16, ValidUntil:boot+1m0s, PreferredLifetimeInfo:deprecated}",
},
} {
t.Run(tc.name, func(t *testing.T) {
var a interfaces.Address
a.SetAddr(subnet)
tc.fn(&a)
if got := addressToString(a); got != tc.want {
t.Fatalf("got \"%s\", want \"%s\"", got, tc.want)
}
})
}
}
func TestInterfaceAddedStringer(t *testing.T) {
want := "{Id:1, Name:testif01, DeviceClass:loopback, Online:true, HasDefaultIpv4Route:true, HasDefaultIpv6Route:true}"
if got := interfaceAdded(testProperties()).String(); got != want {
t.Fatalf("got \"%s\", want \"%s\"", got, want)
}
}
func TestDefaultRouteChangedStringer(t *testing.T) {
ipv4, ipv6 := true, true
for _, tc := range []struct {
name string
event defaultRouteChanged
want string
}{
{
name: "neither present",
event: defaultRouteChanged{
nicid: tcpip.NICID(1),
},
want: "{nicid:1}",
},
{
name: "both present",
event: defaultRouteChanged{
nicid: tcpip.NICID(1),
hasDefaultIPv4Route: &ipv4,
hasDefaultIPv6Route: &ipv6,
},
want: "{nicid:1, hasDefaultIPv4Route:true, hasDefaultIPv6Route:true}",
},
} {
t.Run(tc.name, func(t *testing.T) {
if got := tc.event.String(); got != tc.want {
t.Fatalf("got = \"%s\", want = \"%s\"", got, tc.want)
}
})
}
}
func TestAddressChangedStringer(t *testing.T) {
a := addressChanged{
nicid: tcpip.NICID(1),
protocolAddr: tcpip.ProtocolAddress{
AddressWithPrefix: tcpip.AddressWithPrefix{Address: util.Parse("1.2.3.4"), PrefixLen: 16},
},
lifetimes: stack.AddressLifetimes{
Deprecated: false,
PreferredUntil: (tcpip.MonotonicTime{}).Add(time.Minute),
ValidUntil: (tcpip.MonotonicTime{}).Add(time.Minute),
},
state: stack.AddressAssigned,
}
want := "{nicid:1 addr:1.2.3.4/16 lifetimes:{Deprecated:false PreferredUntil:boot+1m0s ValidUntil:boot+1m0s} state:Assigned}"
if got := a.String(); got != want {
t.Fatalf("got = \"%s\", want = \"%s\"", got, want)
}
}
func TestAddressRemovedStringer(t *testing.T) {
a := addressRemoved{
nicid: tcpip.NICID(1),
protocolAddr: tcpip.ProtocolAddress{
AddressWithPrefix: tcpip.AddressWithPrefix{Address: util.Parse("1.2.3.4"), PrefixLen: 16},
},
reason: stack.AddressRemovalManualAction,
}
want := "{nicid:1 addr:1.2.3.4/16 reason:ManualAction}"
if got := a.String(); got != want {
t.Fatalf("got = \"%s\", want = \"%s\"", got, want)
}
}