From d3626c51f1d1bef01f6b5fbe6ce4a01c08a22ff6 Mon Sep 17 00:00:00 2001 From: Fran Bull Date: Fri, 20 Mar 2026 06:58:22 -0700 Subject: [PATCH] feature/conn25: add packet filter allow functions That will be able to be plugged into the hooks in wgengine/filter/filter.go to let connector packets flow. Fixes tailscale/corp#37144 Fixes tailscale/corp#37145 Signed-off-by: Fran Bull --- feature/conn25/conn25.go | 47 ++++++++++++++++++++++- feature/conn25/conn25_test.go | 71 +++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/feature/conn25/conn25.go b/feature/conn25/conn25.go index d1ee4f952..4ab5dafd7 100644 --- a/feature/conn25/conn25.go +++ b/feature/conn25/conn25.go @@ -27,6 +27,7 @@ import ( "tailscale.com/ipn/ipnext" "tailscale.com/ipn/ipnlocal" "tailscale.com/net/dns" + "tailscale.com/net/packet" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/appctype" @@ -499,6 +500,26 @@ func (c *client) ClientTransitIPForMagicIP(magicIP netip.Addr) (netip.Addr, erro return netip.Addr{}, ErrUnmappedMagicIP } +// linkLocalAllow returns true if the provided packet with a link-local Dst address has a +// Dst that is one of our transit IPs, and false otherwise. +// Tailscale's wireguard filters drop link-local unicast packets (see [wgengine/filter/filter.go]) +// but conn25 uses link-local addresses for transit IPs. +// Let the filter know if this is one of our addresses and should be allowed. +func (c *client) linkLocalAllow(p packet.Parsed) (bool, string) { + c.mu.Lock() + defer c.mu.Unlock() + ok := c.isKnownTransitIP(p.Dst.Addr()) + if ok { + return true, packetFilterAllowReason + } + return false, "" +} + +func (c *client) isKnownTransitIP(tip netip.Addr) bool { + _, ok := c.assignments.lookupByTransitIP(tip) + return ok +} + func (c *client) isConfigured() bool { c.mu.Lock() defer c.mu.Unlock() @@ -863,6 +884,20 @@ func (c *connector) ConnectorRealIPForTransitIPConnection(srcIP netip.Addr, tran return netip.Addr{}, ErrUnmappedSrcAndTransitIP } +const packetFilterAllowReason = "app connector transit IP" + +// packetFilterAllow returns true if the provided packet has a Src that maps to a peer +// that has a transit IP with us that is the packet Dst, and false otherwise. +func (c *connector) packetFilterAllow(p packet.Parsed) (bool, string) { + c.mu.Lock() + defer c.mu.Unlock() + _, ok := c.lookupBySrcIPAndTransitIP(p.Src.Addr(), p.Dst.Addr()) + if ok { + return true, packetFilterAllowReason + } + return false, "" +} + func (c *connector) lookupBySrcIPAndTransitIP(srcIP, transitIP netip.Addr) (appAddr, bool) { m, ok := c.transitIPs[srcIP] if !ok || m == nil { @@ -899,9 +934,10 @@ type domainDst struct { } // addrAssignments is the collection of addrs assigned by this client -// supporting lookup by magicip or domain+dst +// supporting lookup by magic IP, transit IP or domain+dst type addrAssignments struct { byMagicIP map[netip.Addr]addrs + byTransitIP map[netip.Addr]addrs byDomainDst map[domainDst]addrs } @@ -915,7 +951,11 @@ func (a *addrAssignments) insert(as addrs) error { if _, ok := a.byDomainDst[ddst]; ok { return errors.New("byDomainDst key exists") } + if _, ok := a.byTransitIP[as.transit]; ok { + return errors.New("byTransitIP key exists") + } mak.Set(&a.byMagicIP, as.magic, as) + mak.Set(&a.byTransitIP, as.transit, as) mak.Set(&a.byDomainDst, ddst, as) return nil } @@ -929,3 +969,8 @@ func (a *addrAssignments) lookupByMagicIP(mip netip.Addr) (addrs, bool) { v, ok := a.byMagicIP[mip] return v, ok } + +func (a *addrAssignments) lookupByTransitIP(tip netip.Addr) (addrs, bool) { + v, ok := a.byTransitIP[tip] + return v, ok +} diff --git a/feature/conn25/conn25_test.go b/feature/conn25/conn25_test.go index f91ea98d7..c8dcef817 100644 --- a/feature/conn25/conn25_test.go +++ b/feature/conn25/conn25_test.go @@ -16,6 +16,7 @@ import ( "go4.org/netipx" "golang.org/x/net/dns/dnsmessage" "tailscale.com/ipn/ipnext" + "tailscale.com/net/packet" "tailscale.com/net/tsdial" "tailscale.com/tailcfg" "tailscale.com/tsd" @@ -1309,3 +1310,73 @@ func TestConnectorRealIPForTransitIPConnection(t *testing.T) { }) } } + +func TestIsKnownTransitIP(t *testing.T) { + knownTip := netip.MustParseAddr("100.64.0.41") + unknownTip := netip.MustParseAddr("100.64.0.42") + + c := newConn25(t.Logf) + c.client.assignments.insert(addrs{ + transit: knownTip, + }) + + if !c.client.isKnownTransitIP(knownTip) { + t.Fatal("knownTip: should have been known") + } + if c.client.isKnownTransitIP(unknownTip) { + t.Fatal("unknownTip: should not have been known") + } +} + +func TestLinkLocalAllow(t *testing.T) { + knownTip := netip.MustParseAddr("100.64.0.41") + + c := newConn25(t.Logf) + c.client.assignments.insert(addrs{ + transit: knownTip, + }) + + if allow, _ := c.client.linkLocalAllow(packet.Parsed{ + Dst: netip.AddrPortFrom(knownTip, 1234), + }); !allow { + t.Fatal("knownTip: should have been allowed") + } + + if allow, _ := c.client.linkLocalAllow(packet.Parsed{ + Dst: netip.AddrPort{}, + }); allow { + t.Fatal("unknownTip: should not have been allowed") + } +} + +func TestConnectorPacketFilterAllow(t *testing.T) { + knownTip := netip.MustParseAddr("100.64.0.41") + knownSrc := netip.MustParseAddr("100.64.0.1") + unknownTip := netip.MustParseAddr("100.64.0.42") + unknownSrc := netip.MustParseAddr("100.64.0.42") + + c := newConn25(t.Logf) + c.connector.transitIPs = map[netip.Addr]map[netip.Addr]appAddr{} + c.connector.transitIPs[knownSrc] = map[netip.Addr]appAddr{} + c.connector.transitIPs[knownSrc][knownTip] = appAddr{} + + if allow, _ := c.connector.packetFilterAllow(packet.Parsed{ + Src: netip.AddrPortFrom(knownSrc, 1234), + Dst: netip.AddrPortFrom(knownTip, 1234), + }); !allow { + t.Fatal("knownTip: should have been allowed") + } + + if allow, _ := c.connector.packetFilterAllow(packet.Parsed{ + Src: netip.AddrPortFrom(unknownSrc, 1234), + Dst: netip.AddrPortFrom(knownTip, 1234), + }); allow { + t.Fatal("unknownSrc: should not have been allowed") + } + if allow, _ := c.connector.packetFilterAllow(packet.Parsed{ + Src: netip.AddrPortFrom(knownSrc, 1234), + Dst: netip.AddrPortFrom(unknownTip, 1234), + }); allow { + t.Fatal("unknownTip: should not have been allowed") + } +}