blob: 263d8f7c53bc6544fdb3af6527546e89267e1d88 [file] [log] [blame]
// Copyright 2019 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"
"fmt"
"testing"
"time"
"go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/dns"
"go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/util"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/faketime"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
const (
shortLifetime = time.Nanosecond
shortLifetimeTimeout = time.Second
middleLifetime = 500 * time.Millisecond
middleLifetimeTimeout = middleLifetime * time.Second
incrementalTimeout = 100 * time.Millisecond
defaultLifetime = time.Hour
)
var (
mask = util.Parse("ffff:ffff::")
subnet1 = newSubnet(util.Parse("abcd:1234::"), tcpip.MaskFromBytes(mask.AsSlice()))
subnet2 = newSubnet(util.Parse("abcd:1236::"), tcpip.MaskFromBytes(mask.AsSlice()))
testProtocolAddr1 = tcpip.ProtocolAddress{
Protocol: ipv6.ProtocolNumber,
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: util.Parse("abcd:ee00::1"),
PrefixLen: 64,
},
}
testProtocolAddr2 = tcpip.ProtocolAddress{
Protocol: ipv6.ProtocolNumber,
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: util.Parse("abcd:ef00::1"),
PrefixLen: 64,
},
}
)
func newSubnet(addr tcpip.Address, mask tcpip.AddressMask) tcpip.Subnet {
subnet, err := tcpip.NewSubnet(addr, mask)
if err != nil {
panic(fmt.Sprintf("NewSubnet(%s, %s): %s", addr, mask, err))
}
return subnet
}
// newNDPDispatcherForTest returns a new ndpDispatcher with a channel used to
// notify tests when its event queue is emptied.
func newNDPDispatcherForTest() *ndpDispatcher {
n := newNDPDispatcher()
n.testNotifyCh = make(chan struct{}, 1)
return n
}
// waitForEmptyQueue returns after the event queue is emptied.
//
// If n's event queue is empty when waitForEmptyQueue is called, then
// waitForEmptyQueue returns immediately.
func waitForEmptyQueue(n *ndpDispatcher) {
// Wait for an empty event queue.
for {
n.mu.Lock()
empty := len(n.mu.events) == 0
n.mu.Unlock()
if empty {
break
}
<-n.testNotifyCh
}
}
// Test that attempting to invalidate an off-link route which we do not have a
// route for is not an issue.
func TestNDPInvalidateUnknownOffLinkRoute(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ndpDisp := newNDPDispatcherForTest()
ns, _ := newNetstack(t, netstackTestOptions{ndpDisp: ndpDisp})
ndpDisp.start(ctx)
ifs := addNoopEndpoint(t, ns, "")
t.Cleanup(ifs.RemoveByUser)
if err := ifs.Up(); err != nil {
t.Fatalf("ifs.Up(): %s", err)
}
// Invalidate a route we do not have in our route table.
rt := tcpip.Route{NIC: ifs.nicid, Destination: subnet1, Gateway: testLinkLocalV6Addr1}
ndpDisp.OnOffLinkRouteInvalidated(rt.NIC, rt.Destination, rt.Gateway)
waitForEmptyQueue(ndpDisp)
if rts := ns.stack.GetRouteTable(); containsRoute(rts, rt) {
t.Fatalf("should not have route = %s in the route table, got = %s", rt, rts)
}
}
func TestNDPIPv6OffLinkRoutePreferences(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ndpDisp := newNDPDispatcherForTest()
ns, _ := newNetstack(t, netstackTestOptions{ndpDisp: ndpDisp})
ndpDisp.start(ctx)
ifs1 := addNoopEndpoint(t, ns, "")
t.Cleanup(ifs1.RemoveByUser)
if err := ifs1.Up(); err != nil {
t.Fatalf("ifs1.Up(): %s", err)
}
ifs2 := addNoopEndpoint(t, ns, "")
t.Cleanup(ifs2.RemoveByUser)
if err := ifs2.Up(); err != nil {
t.Fatalf("ifs2.Up(): %s", err)
}
dest := tcpip.AddressWithPrefix{Address: util.Parse("abcd:ee00::"), PrefixLen: 32}.Subnet()
r1 := tcpip.Route{Destination: dest, NIC: ifs1.nicid, Gateway: testLinkLocalV6Addr1}
r2 := tcpip.Route{Destination: dest, NIC: ifs2.nicid, Gateway: testLinkLocalV6Addr1}
expectedRouteTable := []tcpip.Route{
{Destination: ipv4MulticastSubnet().Subnet(), NIC: ifs1.nicid},
{Destination: ipv4MulticastSubnet().Subnet(), NIC: ifs2.nicid},
ipv6LinkLocalOnLinkRoute(ifs1.nicid),
ipv6LinkLocalOnLinkRoute(ifs2.nicid),
r1,
r2,
{Destination: ipv6MulticastSubnet().Subnet(), NIC: ifs1.nicid},
{Destination: ipv6MulticastSubnet().Subnet(), NIC: ifs2.nicid},
}
ndpDisp.OnOffLinkRouteUpdated(r2.NIC, r2.Destination, r2.Gateway, header.LowRoutePreference)
ndpDisp.OnOffLinkRouteUpdated(r1.NIC, r1.Destination, r1.Gateway, header.HighRoutePreference)
waitForEmptyQueue(ndpDisp)
if diff := cmp.Diff(expectedRouteTable, ns.stack.GetRouteTable()); diff != "" {
t.Errorf("route table mismatch (-want +got):\n%s", diff)
}
// Flip the preferences of r1 and r2.
expectedRouteTable[4], expectedRouteTable[5] = expectedRouteTable[5], expectedRouteTable[4]
ndpDisp.OnOffLinkRouteUpdated(r2.NIC, r2.Destination, r2.Gateway, header.HighRoutePreference)
ndpDisp.OnOffLinkRouteUpdated(r1.NIC, r1.Destination, r1.Gateway, header.LowRoutePreference)
waitForEmptyQueue(ndpDisp)
if diff := cmp.Diff(expectedRouteTable, ns.stack.GetRouteTable()); diff != "" {
t.Errorf("route table mismatch (-want +got):\n%s", diff)
}
}
// Test that ndpDispatcher properly handles the discovery and invalidation of
// off-link routes.
func TestNDPIPv6OffLinkRouteDiscovery(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ndpDisp := newNDPDispatcherForTest()
ns, _ := newNetstack(t, netstackTestOptions{ndpDisp: ndpDisp})
ndpDisp.start(ctx)
ifs1 := addNoopEndpoint(t, ns, "")
t.Cleanup(ifs1.RemoveByUser)
if err := ifs1.Up(); err != nil {
t.Fatalf("ifs1.Up(): %s", err)
}
ifs2 := addNoopEndpoint(t, ns, "")
t.Cleanup(ifs2.RemoveByUser)
if err := ifs2.Up(); err != nil {
t.Fatalf("ifs2.Up(): %s", err)
}
// Test discovering a new default router on eth1.
nic1Rtr1Rt := tcpip.Route{Destination: header.IPv6EmptySubnet, Gateway: testLinkLocalV6Addr1, NIC: ifs1.nicid}
ndpDisp.OnOffLinkRouteUpdated(nic1Rtr1Rt.NIC, nic1Rtr1Rt.Destination, nic1Rtr1Rt.Gateway, header.MediumRoutePreference)
waitForEmptyQueue(ndpDisp)
if rts := ns.stack.GetRouteTable(); !containsRoute(rts, nic1Rtr1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic1Rtr1Rt, rts)
}
// Test discovering a new default router on eth2 (with the same
// link-local IP as the one discovered as eth1).
nic2Rtr1Rt := tcpip.Route{Destination: header.IPv6EmptySubnet, Gateway: testLinkLocalV6Addr1, NIC: ifs2.nicid}
ndpDisp.OnOffLinkRouteUpdated(nic2Rtr1Rt.NIC, nic2Rtr1Rt.Destination, nic2Rtr1Rt.Gateway, header.MediumRoutePreference)
waitForEmptyQueue(ndpDisp)
rts := ns.stack.GetRouteTable()
if !containsRoute(rts, nic2Rtr1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Rtr1Rt, rts)
}
// Should still have the route from before.
if !containsRoute(rts, nic1Rtr1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic1Rtr1Rt, rts)
}
// Test discovering another default router on eth2.
nic2Rtr2Rt := tcpip.Route{Destination: header.IPv6EmptySubnet, Gateway: testLinkLocalV6Addr2, NIC: ifs2.nicid}
ndpDisp.OnOffLinkRouteUpdated(nic2Rtr2Rt.NIC, nic2Rtr2Rt.Destination, nic2Rtr2Rt.Gateway, header.MediumRoutePreference)
waitForEmptyQueue(ndpDisp)
rts = ns.stack.GetRouteTable()
if !containsRoute(rts, nic2Rtr2Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Rtr2Rt, rts)
}
// Should still have the routes from before.
if !containsRoute(rts, nic2Rtr1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Rtr1Rt, rts)
}
if !containsRoute(rts, nic1Rtr1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic1Rtr1Rt, rts)
}
// Test discovering a more specific route on eth2.
dest := tcpip.AddressWithPrefix{Address: util.Parse("abcd:ee00::"), PrefixLen: 64}.Subnet()
ndpDisp.OnOffLinkRouteUpdated(ifs2.nicid, dest, testLinkLocalV6Addr1, header.MediumRoutePreference)
waitForEmptyQueue(ndpDisp)
nic2Rtr2MoreSpecificRt := tcpip.Route{Destination: dest, Gateway: testLinkLocalV6Addr1, NIC: ifs2.nicid}
rts = ns.stack.GetRouteTable()
if !containsRoute(rts, nic2Rtr2MoreSpecificRt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Rtr2MoreSpecificRt, rts)
}
// Should still have the routes from before.
if !containsRoute(rts, nic2Rtr2Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Rtr2Rt, rts)
}
if !containsRoute(rts, nic2Rtr1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Rtr1Rt, rts)
}
if !containsRoute(rts, nic1Rtr1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic1Rtr1Rt, rts)
}
// Invalidate the default router with IP testLinkLocalV6Addr1 from eth2.
ndpDisp.OnOffLinkRouteInvalidated(ifs2.nicid, header.IPv6EmptySubnet, testLinkLocalV6Addr1)
waitForEmptyQueue(ndpDisp)
rts = ns.stack.GetRouteTable()
if containsRoute(rts, nic2Rtr1Rt) {
t.Fatalf("should not have route = %s in the route table, got = %s", nic2Rtr1Rt, rts)
}
// Should still have default routes through the non-invalidated
// routers and the more specific route.
if !containsRoute(rts, nic2Rtr2MoreSpecificRt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Rtr2MoreSpecificRt, rts)
}
if !containsRoute(rts, nic2Rtr2Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Rtr2Rt, rts)
}
if !containsRoute(rts, nic1Rtr1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic1Rtr1Rt, rts)
}
// Invalidate the default router with IP testLinkLocalV6Addr1 from eth1.
ndpDisp.OnOffLinkRouteInvalidated(ifs1.nicid, header.IPv6EmptySubnet, testLinkLocalV6Addr1)
waitForEmptyQueue(ndpDisp)
rts = ns.stack.GetRouteTable()
if containsRoute(rts, nic1Rtr1Rt) {
t.Fatalf("should not have route = %s in the route table, got = %s", nic1Rtr1Rt, rts)
}
// Should still have the more specific route and default routes through the
// non-invalidated router.
if !containsRoute(rts, nic2Rtr2Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Rtr2Rt, rts)
}
if !containsRoute(rts, nic2Rtr2MoreSpecificRt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Rtr2MoreSpecificRt, rts)
}
// Invalidate the default router with IP testLinkLocalV6Addr2 from eth2.
ndpDisp.OnOffLinkRouteInvalidated(ifs2.nicid, header.IPv6EmptySubnet, testLinkLocalV6Addr2)
waitForEmptyQueue(ndpDisp)
rts = ns.stack.GetRouteTable()
if containsRoute(rts, nic2Rtr2Rt) {
t.Fatalf("should not have route = %s in the route table, got = %s", nic2Rtr2Rt, rts)
}
// Should still have the more specific route.
if !containsRoute(rts, nic2Rtr2MoreSpecificRt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Rtr2MoreSpecificRt, rts)
}
// Invalidate the more specific route.
ndpDisp.OnOffLinkRouteInvalidated(ifs2.nicid, dest, testLinkLocalV6Addr1)
waitForEmptyQueue(ndpDisp)
rts = ns.stack.GetRouteTable()
// Should not have the more specific route.
if containsRoute(rts, nic2Rtr2MoreSpecificRt) {
t.Fatalf("should not have route = %s in the route table, got = %s", nic2Rtr2MoreSpecificRt, rts)
}
}
// Test that attempting to invalidate an on-link prefix which we do not have a
// route for is not an issue.
func TestNDPInvalidateUnknownIPv6Prefix(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ndpDisp := newNDPDispatcherForTest()
ns, _ := newNetstack(t, netstackTestOptions{ndpDisp: ndpDisp})
ndpDisp.start(ctx)
ifs := addNoopEndpoint(t, ns, "")
t.Cleanup(ifs.RemoveByUser)
if err := ifs.Up(); err != nil {
t.Fatalf("ifs.Up(): %s", err)
}
// Invalidate the prefix subnet1 from eth (even though we do not yet know
// about it).
ndpDisp.OnOnLinkPrefixInvalidated(ifs.nicid, subnet1)
waitForEmptyQueue(ndpDisp)
if rt, rts := onLinkV6Route(ifs.nicid, subnet1), ns.stack.GetRouteTable(); containsRoute(rts, rt) {
t.Fatalf("should not have route = %s in the route table, got = %s", rt, rts)
}
}
// Test that ndpDispatcher properly handles the discovery and invalidation of
// on-link IPv6 prefixes.
func TestNDPIPv6PrefixDiscovery(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ndpDisp := newNDPDispatcherForTest()
ns, _ := newNetstack(t, netstackTestOptions{ndpDisp: ndpDisp})
ndpDisp.start(ctx)
ifs1 := addNoopEndpoint(t, ns, "")
t.Cleanup(ifs1.RemoveByUser)
if err := ifs1.Up(); err != nil {
t.Fatalf("ifs1.Up(): %s", err)
}
ifs2 := addNoopEndpoint(t, ns, "")
t.Cleanup(ifs2.RemoveByUser)
if err := ifs2.Up(); err != nil {
t.Fatalf("ifs2.Up(): %s", err)
}
// Test discovering a new on-link prefix on eth1.
ndpDisp.OnOnLinkPrefixDiscovered(ifs1.nicid, subnet1)
waitForEmptyQueue(ndpDisp)
nic1Sub1Rt := onLinkV6Route(ifs1.nicid, subnet1)
if rts := ns.stack.GetRouteTable(); !containsRoute(rts, nic1Sub1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic1Sub1Rt, rts)
}
// Test discovering the same on-link prefix on eth2.
ndpDisp.OnOnLinkPrefixDiscovered(ifs2.nicid, subnet1)
waitForEmptyQueue(ndpDisp)
nic2Sub1Rt := onLinkV6Route(ifs2.nicid, subnet1)
rts := ns.stack.GetRouteTable()
if !containsRoute(rts, nic2Sub1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Sub1Rt, rts)
}
// Should still have the route from before.
if !containsRoute(rts, nic1Sub1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic1Sub1Rt, rts)
}
// Test discovering another on-link prefix on eth2.
ndpDisp.OnOnLinkPrefixDiscovered(ifs2.nicid, subnet2)
waitForEmptyQueue(ndpDisp)
nic2Sub2Rt := onLinkV6Route(ifs2.nicid, subnet2)
rts = ns.stack.GetRouteTable()
if !containsRoute(rts, nic2Sub2Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Sub2Rt, rts)
}
// Should still have the routes from before.
if !containsRoute(rts, nic2Sub1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Sub1Rt, rts)
}
if !containsRoute(rts, nic1Sub1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic1Sub1Rt, rts)
}
// Invalidate the prefix subnet1 from eth2.
ndpDisp.OnOnLinkPrefixInvalidated(ifs2.nicid, subnet1)
waitForEmptyQueue(ndpDisp)
rts = ns.stack.GetRouteTable()
if containsRoute(rts, nic2Sub1Rt) {
t.Fatalf("should not have route = %s in the route table, got = %s", nic2Sub1Rt, rts)
}
// Should still have default routes through the non-invalidated
// routers.
if !containsRoute(rts, nic2Sub2Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Sub2Rt, rts)
}
if !containsRoute(rts, nic1Sub1Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic1Sub1Rt, rts)
}
// Invalidate the prefix subnet1 from eth1.
ndpDisp.OnOnLinkPrefixInvalidated(ifs1.nicid, subnet1)
waitForEmptyQueue(ndpDisp)
rts = ns.stack.GetRouteTable()
if containsRoute(rts, nic1Sub1Rt) {
t.Fatalf("should not have route = %s in the route table, got = %s", nic1Sub1Rt, rts)
}
// Should still not have the other invalidated route.
if containsRoute(rts, nic2Sub1Rt) {
t.Fatalf("should not have route = %s in the route table, got = %s", nic2Sub1Rt, rts)
}
// Should still have default route through the non-invalidated router.
if !containsRoute(rts, nic2Sub2Rt) {
t.Fatalf("missing route = %s from route table, got = %s", nic2Sub2Rt, rts)
}
// Invalidate the prefix subnet2 from eth2.
ndpDisp.OnOnLinkPrefixInvalidated(ifs2.nicid, subnet2)
waitForEmptyQueue(ndpDisp)
rts = ns.stack.GetRouteTable()
if containsRoute(rts, nic2Sub2Rt) {
t.Fatalf("should not have route = %s in the route table, got = %s", nic2Sub2Rt, rts)
}
// Should still not have the other invalidated route.
if containsRoute(rts, nic1Sub1Rt) {
t.Fatalf("should not have route = %s in the route table, got = %s", nic1Sub1Rt, rts)
}
if containsRoute(rts, nic2Sub1Rt) {
t.Fatalf("should not have route = %s in the route table, got = %s", nic2Sub1Rt, rts)
}
}
// TestLinkDown tests that Recursive DNS Servers learned from NDP are
// invalidated when a NIC is brought down.
func TestLinkDown(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ndpDisp := newNDPDispatcherForTest()
ns, _ := newNetstack(t, netstackTestOptions{ndpDisp: ndpDisp})
ndpDisp.start(ctx)
ifs1 := addNoopEndpoint(t, ns, "")
t.Cleanup(ifs1.RemoveByUser)
if err := ifs1.Up(); err != nil {
t.Fatalf("ifs1.Up(): %s", err)
}
ifs2 := addNoopEndpoint(t, ns, "")
t.Cleanup(ifs2.RemoveByUser)
if err := ifs2.Up(); err != nil {
t.Fatalf("ifs2.Up(): %s", err)
}
addr1NIC1 := tcpip.FullAddress{
Addr: util.Parse("fe80::1"),
Port: 53,
NIC: ifs1.nicid,
}
addr1NIC2 := tcpip.FullAddress{
Addr: util.Parse("fe80::1"),
Port: 53,
NIC: ifs2.nicid,
}
addr2NIC1 := tcpip.FullAddress{
Addr: util.Parse("fe80::2"),
Port: 53,
NIC: ifs1.nicid,
}
addr3NIC2 := tcpip.FullAddress{
Addr: util.Parse("fe80::3"),
Port: 53,
NIC: ifs2.nicid,
}
ndpDisp.OnRecursiveDNSServerOption(ifs1.nicid, []tcpip.Address{addr1NIC1.Addr, addr2NIC1.Addr}, defaultLifetime)
ndpDisp.OnRecursiveDNSServerOption(ifs2.nicid, []tcpip.Address{addr1NIC2.Addr, addr3NIC2.Addr}, defaultLifetime)
waitForEmptyQueue(ndpDisp)
{
want := []tcpip.FullAddress{addr1NIC1, addr2NIC1, addr1NIC2, addr3NIC2}
got := ns.dnsConfig.GetServersCache()
if diff := cmp.Diff(want, got, dnsServerTcpIpFullAddressOpts...); diff != "" {
t.Fatalf("GetServerCache() mismatch (-want +got):\n%s", diff)
}
}
// Bring eth2 down and make sure the DNS servers learned from that NIC are
// invalidated.
if err := ifs2.Down(); err != nil {
t.Fatalf("ifs2.Down(): %s", err)
}
waitForEmptyQueue(ndpDisp)
{
want := []tcpip.FullAddress{addr1NIC1, addr2NIC1}
got := ns.dnsConfig.GetServersCache()
if diff := cmp.Diff(want, got, dnsServerTcpIpFullAddressOpts...); diff != "" {
t.Fatalf("GetServerCache() mismatch (-want +got):\n%s", diff)
}
}
// Bring eth2 up and make sure the DNS servers learned from that NIC do not
// reappear.
if err := ifs2.Up(); err != nil {
t.Fatalf("ifs2.Up(): %s", err)
}
waitForEmptyQueue(ndpDisp)
{
want := []tcpip.FullAddress{addr1NIC1, addr2NIC1}
got := ns.dnsConfig.GetServersCache()
if diff := cmp.Diff(want, got, dnsServerTcpIpFullAddressOpts...); diff != "" {
t.Errorf("GetServerCache() mismatch (-want +got):\n%s", diff)
}
}
}
var dnsServerTcpIpFullAddressOpts = []cmp.Option{
// Asymmetric application of transformers is not supported in go-cmp. IOW we
// write this asymmetric transformer of dns.Server -> tcpip.FullAddress as a
// symmetric transformer that handles both input types; go-cmp internally
// upcasts invariant operands to interface{}.
cmp.FilterValues(func(x, y interface{}) bool {
for _, v := range []interface{}{x, y} {
switch v.(type) {
case []dns.Server:
case []tcpip.FullAddress:
default:
return false
}
}
return true
}, cmp.Transformer("ToTcpIpAddress", func(v interface{}) []tcpip.FullAddress {
switch v := v.(type) {
case []dns.Server:
if v == nil {
return nil
}
out := make([]tcpip.FullAddress, len(v))
for i := range v {
out[i] = v[i].Address
}
return out
case []tcpip.FullAddress:
return v
default:
panic(fmt.Sprintf("value of unexpected type %#v", v))
}
})),
cmpopts.SortSlices(func(left, right tcpip.FullAddress) bool {
if left, right := left.NIC, right.NIC; left != right {
return left < right
}
if left, right := left.Addr, right.Addr; left != right {
return string(left.AsSlice()) < string(right.AsSlice())
}
if left, right := left.Port, right.Port; left != right {
return left < right
}
return false
}),
}
func TestRecursiveDNSServers(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ndpDisp := newNDPDispatcherForTest()
ns, clock := newNetstack(t, netstackTestOptions{ndpDisp: ndpDisp})
ndpDisp.start(ctx)
ifs1 := addNoopEndpoint(t, ns, "")
t.Cleanup(ifs1.RemoveByUser)
if err := ifs1.Up(); err != nil {
t.Fatalf("ifs1.Up(): %s", err)
}
ifs2 := addNoopEndpoint(t, ns, "")
t.Cleanup(ifs2.RemoveByUser)
if err := ifs2.Up(); err != nil {
t.Fatalf("ifs2.Up(): %s", err)
}
addr1NIC1 := tcpip.FullAddress{
Addr: util.Parse("fe80::1"),
Port: 53,
NIC: ifs1.nicid,
}
addr1NIC2 := tcpip.FullAddress{
Addr: util.Parse("fe80::1"),
Port: 53,
NIC: ifs2.nicid,
}
addr2NIC1 := tcpip.FullAddress{
Addr: util.Parse("fe80::2"),
Port: 53,
NIC: ifs1.nicid,
}
addr3NIC2 := tcpip.FullAddress{
Addr: util.Parse("fe80::3"),
Port: 53,
NIC: ifs2.nicid,
}
ndpDisp.OnRecursiveDNSServerOption(ifs1.nicid, []tcpip.Address{addr1NIC1.Addr, addr2NIC1.Addr}, 0)
waitForEmptyQueue(ndpDisp)
{
want := []tcpip.FullAddress(nil)
got := ns.dnsConfig.GetServersCache()
if diff := cmp.Diff(want, got, dnsServerTcpIpFullAddressOpts...); diff != "" {
t.Errorf("GetServerCache() mismatch (-want +got):\n%s", diff)
}
}
ndpDisp.OnRecursiveDNSServerOption(ifs1.nicid, []tcpip.Address{addr1NIC1.Addr, addr2NIC1.Addr}, defaultLifetime)
waitForEmptyQueue(ndpDisp)
{
want := []tcpip.FullAddress{addr1NIC1, addr2NIC1}
got := ns.dnsConfig.GetServersCache()
if diff := cmp.Diff(want, got, dnsServerTcpIpFullAddressOpts...); diff != "" {
t.Errorf("GetServerCache() mismatch (-want +got):\n%s", diff)
}
}
ndpDisp.OnRecursiveDNSServerOption(ifs2.nicid, []tcpip.Address{addr1NIC2.Addr, addr3NIC2.Addr}, defaultLifetime)
waitForEmptyQueue(ndpDisp)
{
want := []tcpip.FullAddress{addr1NIC1, addr2NIC1, addr1NIC2, addr3NIC2}
got := ns.dnsConfig.GetServersCache()
if diff := cmp.Diff(want, got, dnsServerTcpIpFullAddressOpts...); diff != "" {
t.Errorf("GetServerCache() mismatch (-want +got):\n%s", diff)
}
}
ndpDisp.OnRecursiveDNSServerOption(ifs2.nicid, []tcpip.Address{addr1NIC2.Addr, addr3NIC2.Addr}, 0)
waitForEmptyQueue(ndpDisp)
{
want := []tcpip.FullAddress{addr1NIC1, addr2NIC1}
got := ns.dnsConfig.GetServersCache()
if diff := cmp.Diff(want, got, dnsServerTcpIpFullAddressOpts...); diff != "" {
t.Errorf("GetServerCache() mismatch (-want +got):\n%s", diff)
}
}
ndpDisp.OnRecursiveDNSServerOption(ifs1.nicid, []tcpip.Address{addr1NIC1.Addr, addr2NIC1.Addr}, shortLifetime)
waitForEmptyQueue(ndpDisp)
for elapsedTime := time.Duration(0); elapsedTime <= shortLifetimeTimeout; elapsedTime += incrementalTimeout {
clock.Advance(incrementalTimeout)
want := []tcpip.FullAddress(nil)
got := ns.dnsConfig.GetServersCache()
if diff := cmp.Diff(want, got, dnsServerTcpIpFullAddressOpts...); diff != "" {
if elapsedTime < shortLifetimeTimeout {
continue
}
t.Errorf("GetServerCache() mismatch (-want +got):\n%s", diff)
}
break
}
}
func TestRecursiveDNSServersWithInfiniteLifetime(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ndpDisp := newNDPDispatcherForTest()
ns, clock := newNetstack(t, netstackTestOptions{ndpDisp: ndpDisp})
ndpDisp.start(ctx)
ifs := addNoopEndpoint(t, ns, "")
t.Cleanup(ifs.RemoveByUser)
if err := ifs.Up(); err != nil {
t.Fatalf("ifs.Up(): %s", err)
}
addr1 := tcpip.FullAddress{
Addr: util.Parse("fe80::1"),
Port: 53,
NIC: ifs.nicid,
}
addr2 := tcpip.FullAddress{
Addr: util.Parse("fe80::2"),
Port: 53,
NIC: ifs.nicid,
}
addr3 := tcpip.FullAddress{
Addr: util.Parse("fe80::3"),
Port: 53,
NIC: ifs.nicid,
}
ndpDisp.OnRecursiveDNSServerOption(ifs.nicid, []tcpip.Address{addr1.Addr, addr2.Addr, addr3.Addr}, header.NDPInfiniteLifetime)
waitForEmptyQueue(ndpDisp)
{
want := []tcpip.FullAddress{addr1, addr2, addr3}
got := ns.dnsConfig.GetServersCache()
if diff := cmp.Diff(want, got, dnsServerTcpIpFullAddressOpts...); diff != "" {
t.Fatalf("GetServerCache() mismatch (-want +got):\n%s", diff)
}
}
// All addresses to expire after middleLifetime.
ndpDisp.OnRecursiveDNSServerOption(ifs.nicid, []tcpip.Address{addr1.Addr, addr2.Addr, addr3.Addr}, middleLifetime)
waitForEmptyQueue(ndpDisp)
if diff := cmp.Diff([]tcpip.FullAddress{addr1, addr2, addr3}, ns.dnsConfig.GetServersCache(), dnsServerTcpIpFullAddressOpts...); diff != "" {
t.Fatalf("GetServerCache() mismatch (-want +got):\n%s", diff)
}
// Update addr2 and addr3 to be valid forever.
ndpDisp.OnRecursiveDNSServerOption(ifs.nicid, []tcpip.Address{addr2.Addr, addr3.Addr}, header.NDPInfiniteLifetime)
waitForEmptyQueue(ndpDisp)
if diff := cmp.Diff([]tcpip.FullAddress{addr1, addr2, addr3}, ns.dnsConfig.GetServersCache(), dnsServerTcpIpFullAddressOpts...); diff != "" {
t.Fatalf("GetServerCache() mismatch (-want +got):\n%s", diff)
}
// addr1 should expire after middleLifetime.
clock.Advance(middleLifetimeTimeout)
if diff := cmp.Diff([]tcpip.FullAddress{addr2, addr3}, ns.dnsConfig.GetServersCache(), dnsServerTcpIpFullAddressOpts...); diff != "" {
t.Fatalf("GetServerCache() mismatch (-want +got):\n%s", diff)
}
// addr2 and addr3 should not expire after header.NDPInfiniteLifetime (since
// it represents infinity).
clock.Advance(header.NDPInfiniteLifetime)
if diff := cmp.Diff([]tcpip.FullAddress{addr2, addr3}, ns.dnsConfig.GetServersCache(), dnsServerTcpIpFullAddressOpts...); diff != "" {
t.Fatalf("GetServerCache() mismatch (-want +got):\n%s", diff)
}
}
func TestDHCPv6Stats(t *testing.T) {
type statsSnapshot struct {
NoConfiguration uint64
ManagedAddress uint64
OtherConfiguration uint64
}
type step struct {
run func(*ndpDispatcher)
want statsSnapshot
}
getSnapshot := func(ns *Netstack) statsSnapshot {
d := ns.stats.DHCPv6
return statsSnapshot{
NoConfiguration: d.NoConfiguration.Value(),
ManagedAddress: d.ManagedAddress.Value(),
OtherConfiguration: d.OtherConfiguration.Value(),
}
}
tests := []struct {
name string
steps []step
}{
{
name: "one configuration",
steps: []step{
{
run: func(ndpDisp *ndpDispatcher) {
ndpDisp.OnDHCPv6Configuration(0, ipv6.DHCPv6NoConfiguration)
},
want: statsSnapshot{
NoConfiguration: 1,
ManagedAddress: 0,
OtherConfiguration: 0,
},
},
},
},
{
name: "multiple configurations",
steps: []step{
{
run: func(ndpDisp *ndpDispatcher) {
ndpDisp.OnDHCPv6Configuration(0, ipv6.DHCPv6NoConfiguration)
ndpDisp.OnDHCPv6Configuration(0, ipv6.DHCPv6ManagedAddress)
},
want: statsSnapshot{
NoConfiguration: 1,
ManagedAddress: 1,
OtherConfiguration: 0,
},
},
},
},
{
name: "pull between configurations",
steps: []step{
{
run: func(ndpDisp *ndpDispatcher) {
ndpDisp.OnDHCPv6Configuration(0, ipv6.DHCPv6NoConfiguration)
},
want: statsSnapshot{
NoConfiguration: 1,
ManagedAddress: 0,
OtherConfiguration: 0,
},
},
{
run: func(ndpDisp *ndpDispatcher) {
ndpDisp.OnDHCPv6Configuration(0, ipv6.DHCPv6NoConfiguration)
},
want: statsSnapshot{
NoConfiguration: 2,
ManagedAddress: 0,
OtherConfiguration: 0,
},
},
},
},
{
name: "duplicated configurations",
steps: []step{
{
run: func(ndpDisp *ndpDispatcher) {
ndpDisp.OnDHCPv6Configuration(0, ipv6.DHCPv6NoConfiguration)
ndpDisp.OnDHCPv6Configuration(0, ipv6.DHCPv6NoConfiguration)
},
want: statsSnapshot{
NoConfiguration: 2,
ManagedAddress: 0,
OtherConfiguration: 0,
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ns := &Netstack{stack: stack.New(stack.Options{Clock: &faketime.NullClock{}})}
ndpDisp := newNDPDispatcher()
ndpDisp.ns = ns
ndpDisp.dynamicAddressSourceTracker.init(ns)
for i, step := range test.steps {
step.run(ndpDisp)
if diff := cmp.Diff(step.want, getSnapshot(ns)); diff != "" {
t.Errorf("%d-th step: mismatch (-want +got):\n%s", i, diff)
}
}
})
}
}
func TestIPv6AddressConfigTracker(t *testing.T) {
const (
nicID1 = 1
nicID2 = 2
)
linkLocalAddr := tcpip.AddressWithPrefix{
Address: util.Parse("fe80::1"),
PrefixLen: 64,
}
type statsSnapshot struct {
NoGlobalSLAACOrDHCPv6ManagedAddress uint64
GlobalSLAACOnly uint64
DHCPv6ManagedAddressOnly uint64
GlobalSLAACAndDHCPv6ManagedAddress uint64
}
type step struct {
run func(*ndpDispatcher, *faketime.ManualClock)
want statsSnapshot
}
getSnapshot := func(ns *Netstack) statsSnapshot {
c := ns.stats.IPv6AddressConfig
return statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: c.NoGlobalSLAACOrDHCPv6ManagedAddress.Value(),
GlobalSLAACOnly: c.GlobalSLAACOnly.Value(),
DHCPv6ManagedAddressOnly: c.DHCPv6ManagedAddressOnly.Value(),
GlobalSLAACAndDHCPv6ManagedAddress: c.GlobalSLAACAndDHCPv6ManagedAddress.Value(),
}
}
tests := []struct {
name string
steps []step
}{
{
name: "dynamic address config with no config",
steps: []step{
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
clock.Advance(ipv6AddressConfigTrackerInitialDelay)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 0,
GlobalSLAACOnly: 0,
DHCPv6ManagedAddressOnly: 0,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
// Link local addresses should not increment global SLAAC address
// count.
ndpDisp.OnAutoGenAddress(nicID1, linkLocalAddr)
clock.Advance(ipv6AddressConfigTrackerInterval)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 0,
GlobalSLAACOnly: 0,
DHCPv6ManagedAddressOnly: 0,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
ndpDisp.OnDHCPv6Configuration(nicID1, ipv6.DHCPv6NoConfiguration)
clock.Advance(ipv6AddressConfigTrackerInterval)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 1,
GlobalSLAACOnly: 0,
DHCPv6ManagedAddressOnly: 0,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
ndpDisp.OnDHCPv6Configuration(nicID1, ipv6.DHCPv6OtherConfigurations)
clock.Advance(ipv6AddressConfigTrackerInterval)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 2,
GlobalSLAACOnly: 0,
DHCPv6ManagedAddressOnly: 0,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
ndpDisp.OnAutoGenAddress(nicID1, testProtocolAddr1.AddressWithPrefix)
ndpDisp.OnAutoGenAddressInvalidated(nicID1, testProtocolAddr1.AddressWithPrefix)
clock.Advance(ipv6AddressConfigTrackerInterval)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 3,
GlobalSLAACOnly: 0,
DHCPv6ManagedAddressOnly: 0,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
},
},
{
name: "dynamic address config with slaac only",
steps: []step{
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
ndpDisp.OnAutoGenAddress(nicID1, testProtocolAddr1.AddressWithPrefix)
clock.Advance(ipv6AddressConfigTrackerInitialDelay)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 0,
GlobalSLAACOnly: 1,
DHCPv6ManagedAddressOnly: 0,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
// Link local addresses should not decrement global SLAAC address
// count.
ndpDisp.OnAutoGenAddressInvalidated(nicID1, linkLocalAddr)
clock.Advance(ipv6AddressConfigTrackerInterval)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 0,
GlobalSLAACOnly: 2,
DHCPv6ManagedAddressOnly: 0,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
ndpDisp.OnAutoGenAddress(nicID1, testProtocolAddr2.AddressWithPrefix)
clock.Advance(ipv6AddressConfigTrackerInterval)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 0,
GlobalSLAACOnly: 3,
DHCPv6ManagedAddressOnly: 0,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
ndpDisp.OnAutoGenAddressInvalidated(nicID1, testProtocolAddr2.AddressWithPrefix)
clock.Advance(ipv6AddressConfigTrackerInterval)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 0,
GlobalSLAACOnly: 4,
DHCPv6ManagedAddressOnly: 0,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
},
},
{
name: "dynamic address config with dhcpv6 only",
steps: []step{
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
ndpDisp.OnDHCPv6Configuration(nicID1, ipv6.DHCPv6ManagedAddress)
clock.Advance(ipv6AddressConfigTrackerInitialDelay)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 0,
GlobalSLAACOnly: 0,
DHCPv6ManagedAddressOnly: 1,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
// Only the last configuration should be used.
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
ndpDisp.OnDHCPv6Configuration(nicID1, ipv6.DHCPv6NoConfiguration)
ndpDisp.OnDHCPv6Configuration(nicID1, ipv6.DHCPv6ManagedAddress)
clock.Advance(ipv6AddressConfigTrackerInterval)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 0,
GlobalSLAACOnly: 0,
DHCPv6ManagedAddressOnly: 2,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
},
},
{
name: "dynamic address config with dhcpv6 and slaac",
steps: []step{
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
ndpDisp.OnDHCPv6Configuration(nicID1, ipv6.DHCPv6ManagedAddress)
ndpDisp.OnAutoGenAddress(nicID1, testProtocolAddr2.AddressWithPrefix)
clock.Advance(ipv6AddressConfigTrackerInitialDelay)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 0,
GlobalSLAACOnly: 0,
DHCPv6ManagedAddressOnly: 0,
GlobalSLAACAndDHCPv6ManagedAddress: 1,
},
},
},
},
{
name: "dynamic address config with dhcpv6 and slaac on different NICs",
steps: []step{
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
ndpDisp.OnDHCPv6Configuration(nicID1, ipv6.DHCPv6ManagedAddress)
ndpDisp.OnAutoGenAddress(nicID2, testProtocolAddr2.AddressWithPrefix)
clock.Advance(ipv6AddressConfigTrackerInitialDelay)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 0,
GlobalSLAACOnly: 1,
DHCPv6ManagedAddressOnly: 1,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
ndpDisp.dynamicAddressSourceTracker.RemovedNIC(nicID1)
clock.Advance(ipv6AddressConfigTrackerInterval)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 0,
GlobalSLAACOnly: 2,
DHCPv6ManagedAddressOnly: 1,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
{
run: func(ndpDisp *ndpDispatcher, clock *faketime.ManualClock) {
ndpDisp.dynamicAddressSourceTracker.RemovedNIC(nicID2)
clock.Advance(ipv6AddressConfigTrackerInterval)
},
want: statsSnapshot{
NoGlobalSLAACOrDHCPv6ManagedAddress: 0,
GlobalSLAACOnly: 2,
DHCPv6ManagedAddressOnly: 1,
GlobalSLAACAndDHCPv6ManagedAddress: 0,
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
clock := faketime.NewManualClock()
ns := &Netstack{stack: stack.New(stack.Options{Clock: clock})}
ndpDisp := newNDPDispatcher()
ndpDisp.ns = ns
ndpDisp.dynamicAddressSourceTracker.init(ns)
for i, step := range test.steps {
step.run(ndpDisp, clock)
if diff := cmp.Diff(step.want, getSnapshot(ns)); diff != "" {
t.Errorf("%d-th step: mismatch (-want +got):\n%s", i, diff)
}
}
})
}
}