From a3c265e9be15802df2b49b3595ff0b0e39be0379 Mon Sep 17 00:00:00 2001 From: James Sanderson Date: Fri, 21 Mar 2025 09:30:42 +0000 Subject: [PATCH 1/2] tsnet: add test for packet filter generation from netmap This is an integration test that covers all the code in Direct, Auto, and LocalBackend that processes NetMaps and creates a Filter. The test uses tsnet as a convenient proxy for setting up all the client pieces correctly, but is not actually a test specific to tsnet. Updates tailscale/corp#20514 Signed-off-by: James Sanderson --- tsnet/packet_filter_test.go | 234 ++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 tsnet/packet_filter_test.go diff --git a/tsnet/packet_filter_test.go b/tsnet/packet_filter_test.go new file mode 100644 index 000000000..3b6feacb8 --- /dev/null +++ b/tsnet/packet_filter_test.go @@ -0,0 +1,234 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package tsnet + +import ( + "context" + "fmt" + "net/netip" + "testing" + "time" + + "tailscale.com/ipn" + "tailscale.com/tailcfg" + "tailscale.com/types/ipproto" + "tailscale.com/types/key" + "tailscale.com/types/netmap" + "tailscale.com/util/must" + "tailscale.com/wgengine/filter" +) + +// waitFor blocks until a NetMap is seen on the IPN bus that satisfies the given +// function f. Note: has no timeout, should be called with a ctx that has an +// appropriate timeout set. +func waitFor(t testing.TB, ctx context.Context, s *Server, f func(*netmap.NetworkMap) bool) error { + t.Helper() + t.Log("starting waitFor") + watcher, err := s.localClient.WatchIPNBus(ctx, ipn.NotifyInitialNetMap) + if err != nil { + t.Fatalf("error watching IPN bus: %s", err) + } + defer watcher.Close() + + for { + n, err := watcher.Next() + if err != nil { + return fmt.Errorf("getting next ipn.Notify from IPN bus: %w", err) + } + if n.NetMap != nil { + if f(n.NetMap) { + return nil + } + } + } +} + +// TestPacketFilterFromNetmap tests all of the client code for processing +// netmaps and turning them into packet filters together. Only the control-plane +// side is mocked out. +func TestPacketFilterFromNetmap(t *testing.T) { + t.Parallel() + + var key key.NodePublic + must.Do(key.UnmarshalText([]byte("nodekey:5c8f86d5fc70d924e55f02446165a5dae8f822994ad26bcf4b08fd841f9bf261"))) + + type check struct { + src string + dst string + port uint16 + want filter.Response + } + + tests := []struct { + name string + initialNetMap *tailcfg.MapResponse + initialWaitTest func(*netmap.NetworkMap) bool + + incrementalNetMap *tailcfg.MapResponse // optional + incrementalWaitTest func(*netmap.NetworkMap) bool // optional + + checks []check + }{ + { + name: "IP_based_peers", + initialNetMap: &tailcfg.MapResponse{ + Node: &tailcfg.Node{ + Addresses: []netip.Prefix{netip.MustParsePrefix("1.1.1.1/32")}, + }, + Peers: []*tailcfg.Node{{ + ID: 2, + Name: "foo", + Key: key, + Addresses: []netip.Prefix{netip.MustParsePrefix("2.2.2.2/32")}, + CapMap: nil, + }}, + PacketFilter: []tailcfg.FilterRule{{ + SrcIPs: []string{"2.2.2.2/32"}, + DstPorts: []tailcfg.NetPortRange{{ + IP: "1.1.1.1/32", + Ports: tailcfg.PortRange{ + First: 22, + Last: 22, + }, + }}, + IPProto: []int{int(ipproto.TCP)}, + }}, + }, + initialWaitTest: func(nm *netmap.NetworkMap) bool { + return len(nm.Peers) > 0 + }, + checks: []check{ + {src: "2.2.2.2", dst: "1.1.1.1", port: 22, want: filter.Accept}, + {src: "2.2.2.2", dst: "1.1.1.1", port: 23, want: filter.Drop}, // different port + {src: "3.3.3.3", dst: "1.1.1.1", port: 22, want: filter.Drop}, // different src + {src: "2.2.2.2", dst: "1.1.1.2", port: 22, want: filter.Drop}, // different dst + }, + }, + { + name: "capmap_based_peers", + initialNetMap: &tailcfg.MapResponse{ + Node: &tailcfg.Node{ + Addresses: []netip.Prefix{netip.MustParsePrefix("1.1.1.1/32")}, + }, + Peers: []*tailcfg.Node{{ + ID: 2, + Name: "foo", + Key: key, + Addresses: []netip.Prefix{netip.MustParsePrefix("2.2.2.2/32")}, + CapMap: tailcfg.NodeCapMap{"X": nil}, + }}, + PacketFilter: []tailcfg.FilterRule{{ + SrcIPs: []string{"cap:X"}, + DstPorts: []tailcfg.NetPortRange{{ + IP: "1.1.1.1/32", + Ports: tailcfg.PortRange{ + First: 22, + Last: 22, + }, + }}, + IPProto: []int{int(ipproto.TCP)}, + }}, + }, + initialWaitTest: func(nm *netmap.NetworkMap) bool { + return len(nm.Peers) > 0 + }, + checks: []check{ + {src: "2.2.2.2", dst: "1.1.1.1", port: 22, want: filter.Accept}, + {src: "2.2.2.2", dst: "1.1.1.1", port: 23, want: filter.Drop}, // different port + {src: "3.3.3.3", dst: "1.1.1.1", port: 22, want: filter.Drop}, // different src + {src: "2.2.2.2", dst: "1.1.1.2", port: 22, want: filter.Drop}, // different dst + }, + }, + { + name: "capmap_based_peers_changed", + initialNetMap: &tailcfg.MapResponse{ + Node: &tailcfg.Node{ + Addresses: []netip.Prefix{netip.MustParsePrefix("1.1.1.1/32")}, + CapMap: tailcfg.NodeCapMap{"X-sigil": nil}, + }, + PacketFilter: []tailcfg.FilterRule{{ + SrcIPs: []string{"cap:label-1"}, + DstPorts: []tailcfg.NetPortRange{{ + IP: "1.1.1.1/32", + Ports: tailcfg.PortRange{ + First: 22, + Last: 22, + }, + }}, + IPProto: []int{int(ipproto.TCP)}, + }}, + }, + initialWaitTest: func(nm *netmap.NetworkMap) bool { + return nm.SelfNode.HasCap("X-sigil") + }, + incrementalNetMap: &tailcfg.MapResponse{ + PeersChanged: []*tailcfg.Node{{ + ID: 2, + Name: "foo", + Key: key, + Addresses: []netip.Prefix{netip.MustParsePrefix("2.2.2.2/32")}, + CapMap: tailcfg.NodeCapMap{"label-1": nil}, + }}, + }, + incrementalWaitTest: func(nm *netmap.NetworkMap) bool { + return len(nm.Peers) > 0 + }, + checks: []check{ + {src: "2.2.2.2", dst: "1.1.1.1", port: 22, want: filter.Accept}, + {src: "2.2.2.2", dst: "1.1.1.1", port: 23, want: filter.Drop}, // different port + {src: "3.3.3.3", dst: "1.1.1.1", port: 22, want: filter.Drop}, // different src + {src: "2.2.2.2", dst: "1.1.1.2", port: 22, want: filter.Drop}, // different dst + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) + defer cancel() + + controlURL, c := startControl(t) + s, _, pubKey := startServer(t, ctx, controlURL, "node") + + if test.initialWaitTest(s.lb.NetMap()) { + t.Fatal("initialWaitTest already passes before sending initial netmap: this will be flaky") + } + + if !c.AddRawMapResponse(pubKey, test.initialNetMap) { + t.Fatalf("could not send map response to %s", pubKey) + } + + if err := waitFor(t, ctx, s, test.initialWaitTest); err != nil { + t.Fatalf("waitFor: %s", err) + } + + if test.incrementalNetMap != nil { + if test.incrementalWaitTest == nil { + t.Fatal("incrementalWaitTest must be set if incrementalNetMap is set") + } + + if test.incrementalWaitTest(s.lb.NetMap()) { + t.Fatal("incrementalWaitTest already passes before sending incremental netmap: this will be flaky") + } + + if !c.AddRawMapResponse(pubKey, test.incrementalNetMap) { + t.Fatalf("could not send map response to %s", pubKey) + } + + if err := waitFor(t, ctx, s, test.incrementalWaitTest); err != nil { + t.Fatalf("waitFor: %s", err) + } + } + + pf := s.lb.GetFilterForTest() + + for _, check := range test.checks { + got := pf.Check(netip.MustParseAddr(check.src), netip.MustParseAddr(check.dst), check.port, ipproto.TCP) + if got != check.want { + t.Errorf("check %s -> %s:%d, got: %s, want: %s", check.src, check.dst, check.port, got, check.want) + } + } + }) + } +} From 4cd24de606d2b1668a1e585d5562118909e33bb5 Mon Sep 17 00:00:00 2001 From: James Sanderson Date: Fri, 21 Mar 2025 12:27:22 +0000 Subject: [PATCH 2/2] review feedback Signed-off-by: James Sanderson --- tsnet/packet_filter_test.go | 88 +++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/tsnet/packet_filter_test.go b/tsnet/packet_filter_test.go index 3b6feacb8..462234222 100644 --- a/tsnet/packet_filter_test.go +++ b/tsnet/packet_filter_test.go @@ -24,7 +24,6 @@ import ( // appropriate timeout set. func waitFor(t testing.TB, ctx context.Context, s *Server, f func(*netmap.NetworkMap) bool) error { t.Helper() - t.Log("starting waitFor") watcher, err := s.localClient.WatchIPNBus(ctx, ipn.NotifyInitialNetMap) if err != nil { t.Fatalf("error watching IPN bus: %s", err) @@ -61,18 +60,18 @@ func TestPacketFilterFromNetmap(t *testing.T) { } tests := []struct { - name string - initialNetMap *tailcfg.MapResponse - initialWaitTest func(*netmap.NetworkMap) bool + name string + mapResponse *tailcfg.MapResponse + waitTest func(*netmap.NetworkMap) bool - incrementalNetMap *tailcfg.MapResponse // optional - incrementalWaitTest func(*netmap.NetworkMap) bool // optional + incrementalMapResponse *tailcfg.MapResponse // optional + incrementalWaitTest func(*netmap.NetworkMap) bool // optional checks []check }{ { name: "IP_based_peers", - initialNetMap: &tailcfg.MapResponse{ + mapResponse: &tailcfg.MapResponse{ Node: &tailcfg.Node{ Addresses: []netip.Prefix{netip.MustParsePrefix("1.1.1.1/32")}, }, @@ -95,7 +94,7 @@ func TestPacketFilterFromNetmap(t *testing.T) { IPProto: []int{int(ipproto.TCP)}, }}, }, - initialWaitTest: func(nm *netmap.NetworkMap) bool { + waitTest: func(nm *netmap.NetworkMap) bool { return len(nm.Peers) > 0 }, checks: []check{ @@ -107,7 +106,7 @@ func TestPacketFilterFromNetmap(t *testing.T) { }, { name: "capmap_based_peers", - initialNetMap: &tailcfg.MapResponse{ + mapResponse: &tailcfg.MapResponse{ Node: &tailcfg.Node{ Addresses: []netip.Prefix{netip.MustParsePrefix("1.1.1.1/32")}, }, @@ -130,7 +129,7 @@ func TestPacketFilterFromNetmap(t *testing.T) { IPProto: []int{int(ipproto.TCP)}, }}, }, - initialWaitTest: func(nm *netmap.NetworkMap) bool { + waitTest: func(nm *netmap.NetworkMap) bool { return len(nm.Peers) > 0 }, checks: []check{ @@ -142,7 +141,7 @@ func TestPacketFilterFromNetmap(t *testing.T) { }, { name: "capmap_based_peers_changed", - initialNetMap: &tailcfg.MapResponse{ + mapResponse: &tailcfg.MapResponse{ Node: &tailcfg.Node{ Addresses: []netip.Prefix{netip.MustParsePrefix("1.1.1.1/32")}, CapMap: tailcfg.NodeCapMap{"X-sigil": nil}, @@ -159,10 +158,10 @@ func TestPacketFilterFromNetmap(t *testing.T) { IPProto: []int{int(ipproto.TCP)}, }}, }, - initialWaitTest: func(nm *netmap.NetworkMap) bool { + waitTest: func(nm *netmap.NetworkMap) bool { return nm.SelfNode.HasCap("X-sigil") }, - incrementalNetMap: &tailcfg.MapResponse{ + incrementalMapResponse: &tailcfg.MapResponse{ PeersChanged: []*tailcfg.Node{{ ID: 2, Name: "foo", @@ -191,44 +190,59 @@ func TestPacketFilterFromNetmap(t *testing.T) { controlURL, c := startControl(t) s, _, pubKey := startServer(t, ctx, controlURL, "node") - if test.initialWaitTest(s.lb.NetMap()) { - t.Fatal("initialWaitTest already passes before sending initial netmap: this will be flaky") + if test.waitTest(s.lb.NetMap()) { + t.Fatal("waitTest already passes before sending initial netmap: this will be flaky") } - if !c.AddRawMapResponse(pubKey, test.initialNetMap) { + if !c.AddRawMapResponse(pubKey, test.mapResponse) { t.Fatalf("could not send map response to %s", pubKey) } - if err := waitFor(t, ctx, s, test.initialWaitTest); err != nil { + if err := waitFor(t, ctx, s, test.waitTest); err != nil { t.Fatalf("waitFor: %s", err) } - if test.incrementalNetMap != nil { - if test.incrementalWaitTest == nil { - t.Fatal("incrementalWaitTest must be set if incrementalNetMap is set") - } - - if test.incrementalWaitTest(s.lb.NetMap()) { - t.Fatal("incrementalWaitTest already passes before sending incremental netmap: this will be flaky") - } - - if !c.AddRawMapResponse(pubKey, test.incrementalNetMap) { - t.Fatalf("could not send map response to %s", pubKey) - } - - if err := waitFor(t, ctx, s, test.incrementalWaitTest); err != nil { - t.Fatalf("waitFor: %s", err) - } - } - pf := s.lb.GetFilterForTest() for _, check := range test.checks { got := pf.Check(netip.MustParseAddr(check.src), netip.MustParseAddr(check.dst), check.port, ipproto.TCP) - if got != check.want { - t.Errorf("check %s -> %s:%d, got: %s, want: %s", check.src, check.dst, check.port, got, check.want) + + want := check.want + if test.incrementalMapResponse != nil { + want = filter.Drop + } + if got != want { + t.Errorf("check %s -> %s:%d, got: %s, want: %s", check.src, check.dst, check.port, got, want) } } + + if test.incrementalMapResponse != nil { + if test.incrementalWaitTest == nil { + t.Fatal("incrementalWaitTest must be set if incrementalMapResponse is set") + } + + if test.incrementalWaitTest(s.lb.NetMap()) { + t.Fatal("incrementalWaitTest already passes before sending incremental netmap: this will be flaky") + } + + if !c.AddRawMapResponse(pubKey, test.incrementalMapResponse) { + t.Fatalf("could not send map response to %s", pubKey) + } + + if err := waitFor(t, ctx, s, test.incrementalWaitTest); err != nil { + t.Fatalf("waitFor: %s", err) + } + + pf := s.lb.GetFilterForTest() + + for _, check := range test.checks { + got := pf.Check(netip.MustParseAddr(check.src), netip.MustParseAddr(check.dst), check.port, ipproto.TCP) + if got != check.want { + t.Errorf("check %s -> %s:%d, got: %s, want: %s", check.src, check.dst, check.port, got, check.want) + } + } + } + }) } }