From 1e09eb0cb6ed08d60fbfc6cd716c11aac2e8a9ad Mon Sep 17 00:00:00 2001 From: Fran Bull Date: Wed, 18 Mar 2026 15:12:11 -0700 Subject: [PATCH] feature/conn25: implement IPMapper Give the datapath hooks the lookup functions they need. Updates tailscale/corp#37144 Updates tailscale/corp#37145 Signed-off-by: Fran Bull --- feature/conn25/conn25.go | 92 ++++++++++++++++-------- feature/conn25/conn25_test.go | 128 +++++++++++++++++++++++++++++++++- 2 files changed, 188 insertions(+), 32 deletions(-) diff --git a/feature/conn25/conn25.go b/feature/conn25/conn25.go index 6c494c313..3c0811713 100644 --- a/feature/conn25/conn25.go +++ b/feature/conn25/conn25.go @@ -340,12 +340,6 @@ func (s *connector) handleTransitIPRequest(n tailcfg.NodeView, peerV4 netip.Addr return TransitIPResponse{} } -func (s *connector) transitIPTarget(peerIP, tip netip.Addr) netip.Addr { - s.mu.Lock() - defer s.mu.Unlock() - return s.transitIPs[peerIP][tip].addr -} - // TransitIPRequest details a single TransitIP allocation request from a client to a // connector. type TransitIPRequest struct { @@ -424,6 +418,8 @@ type config struct { appsByName map[string]appctype.Conn25Attr appNamesByDomain map[dnsname.FQDN][]string selfRoutedDomains set.Set[dnsname.FQDN] + transitIPSet netipx.IPSet + magicIPSet netipx.IPSet } func configFromNodeView(n tailcfg.NodeView) (config, error) { @@ -456,6 +452,21 @@ func configFromNodeView(n tailcfg.NodeView) (config, error) { } mak.Set(&cfg.appsByName, app.Name, app) } + // TODO(fran) 2026-03-18 we don't yet have a proper way to communicate the + // global IP pool config. For now just take it from the first app. + if len(apps) != 0 { + app := apps[0] + mipp, err := ipSetFromIPRanges(app.MagicIPPool) + if err != nil { + return config{}, err + } + tipp, err := ipSetFromIPRanges(app.TransitIPPool) + if err != nil { + return config{}, err + } + cfg.magicIPSet = *mipp + cfg.transitIPSet = *tipp + } return cfg, nil } @@ -474,6 +485,20 @@ type client struct { config config } +// ClientTransitIPForMagicIP is part of the implementation of the IPMapper interface for dataflows lookups. +func (c *client) ClientTransitIPForMagicIP(magicIP netip.Addr) (netip.Addr, error) { + c.mu.Lock() + defer c.mu.Unlock() + v, ok := c.assignments.lookupByMagicIP(magicIP) + if ok { + return v.transit, nil + } + if !c.config.magicIPSet.Contains(magicIP) { + return netip.Addr{}, nil + } + return netip.Addr{}, ErrUnmappedMagicIP +} + func (c *client) isConfigured() bool { c.mu.Lock() defer c.mu.Unlock() @@ -486,31 +511,8 @@ func (c *client) reconfig(newCfg config) error { c.config = newCfg - // TODO(fran) this is not the correct way to manage the pools and changes to the pools. - // We probably want to: - // * check the pools haven't changed - // * reset the whole connector if the pools change? or just if they've changed to exclude - // addresses we have in use? - // * have config separate from the apps for this (rather than multiple potentially conflicting places) - // but this works while we are just getting started here. - for _, app := range c.config.apps { - if c.magicIPPool != nil { // just take the first config and never reconfig - break - } - if app.MagicIPPool == nil { - continue - } - mipp, err := ipSetFromIPRanges(app.MagicIPPool) - if err != nil { - return err - } - tipp, err := ipSetFromIPRanges(app.TransitIPPool) - if err != nil { - return err - } - c.magicIPPool = newIPPool(mipp) - c.transitIPPool = newIPPool(tipp) - } + c.magicIPPool = newIPPool(&(newCfg.magicIPSet)) + c.transitIPPool = newIPPool(&(newCfg.transitIPSet)) return nil } @@ -844,6 +846,29 @@ type connector struct { config config } +// ConnectorRealIPForTransitIPConnection is part of the implementation of the IPMapper interface for dataflows lookups. +func (c *connector) ConnectorRealIPForTransitIPConnection(srcIP netip.Addr, transitIP netip.Addr) (netip.Addr, error) { + c.mu.Lock() + defer c.mu.Unlock() + v, ok := c.lookupBySrcIPAndTransitIP(srcIP, transitIP) + if ok { + return v.addr, nil + } + if !c.config.transitIPSet.Contains(transitIP) { + return netip.Addr{}, nil + } + return netip.Addr{}, ErrUnmappedSrcAndTransitIP +} + +func (c *connector) lookupBySrcIPAndTransitIP(srcIP, transitIP netip.Addr) (appAddr, bool) { + m, ok := c.transitIPs[srcIP] + if !ok || m == nil { + return appAddr{}, false + } + v, ok := m[transitIP] + return v, ok +} + func (s *connector) reconfig(newCfg config) error { s.mu.Lock() defer s.mu.Unlock() @@ -896,3 +921,8 @@ func (a *addrAssignments) lookupByDomainDst(domain dnsname.FQDN, dst netip.Addr) v, ok := a.byDomainDst[domainDst{domain: domain, dst: dst}] return v, ok } + +func (a *addrAssignments) lookupByMagicIP(mip netip.Addr) (addrs, bool) { + v, ok := a.byMagicIP[mip] + return v, ok +} diff --git a/feature/conn25/conn25_test.go b/feature/conn25/conn25_test.go index 08789183d..f91ea98d7 100644 --- a/feature/conn25/conn25_test.go +++ b/feature/conn25/conn25_test.go @@ -369,7 +369,8 @@ func TestHandleConnectorTransitIPRequest(t *testing.T) { i, j, len(wantLookup)) } pip, tip, wantDip := wantLookup[0], wantLookup[1], wantLookup[2] - gotDip := c.connector.transitIPTarget(pip, tip) + aa, _ := c.connector.lookupBySrcIPAndTransitIP(pip, tip) + gotDip := aa.addr if gotDip != wantDip { t.Errorf("wrong result on lookup[%d][%d] ([%v], [%v]): got [%v] expected [%v]", i, j, pip, tip, gotDip, wantDip) @@ -1183,3 +1184,128 @@ func TestMapDNSResponseRewritesResponses(t *testing.T) { }) } } + +func TestClientTransitIPForMagicIP(t *testing.T) { + sn := makeSelfNode(t, appctype.Conn25Attr{ + MagicIPPool: []netipx.IPRange{rangeFrom("0", "10")}, // 100.64.0.0 - 100.64.0.10 + }, []string{}) + mappedMip := netip.MustParseAddr("100.64.0.0") + mappedTip := netip.MustParseAddr("169.0.0.0") + unmappedMip := netip.MustParseAddr("100.64.0.1") + nonMip := netip.MustParseAddr("100.64.0.11") + for _, tt := range []struct { + name string + mip netip.Addr + wantTip netip.Addr + wantErr error + }{ + { + name: "not-a-magic-ip", + mip: nonMip, + wantTip: netip.Addr{}, + wantErr: nil, + }, + { + name: "unmapped-magic-ip", + mip: unmappedMip, + wantTip: netip.Addr{}, + wantErr: ErrUnmappedMagicIP, + }, + { + name: "mapped-magic-ip", + mip: mappedMip, + wantTip: mappedTip, + wantErr: nil, + }, + } { + t.Run(tt.name, func(t *testing.T) { + c := newConn25(t.Logf) + if err := c.reconfig(sn); err != nil { + t.Fatal(err) + } + c.client.assignments.insert(addrs{ + magic: mappedMip, + transit: mappedTip, + }) + tip, err := c.client.ClientTransitIPForMagicIP(tt.mip) + if tip != tt.wantTip { + t.Fatalf("checking transit ip: want %v, got %v", tt.wantTip, tip) + } + if err != tt.wantErr { + t.Fatalf("checking error: want %v, got %v", tt.wantErr, err) + } + }) + } +} + +func TestConnectorRealIPForTransitIPConnection(t *testing.T) { + sn := makeSelfNode(t, appctype.Conn25Attr{ + TransitIPPool: []netipx.IPRange{rangeFrom("40", "50")}, // 100.64.0.40 - 100.64.0.50 + }, []string{}) + mappedSrc := netip.MustParseAddr("100.0.0.1") + unmappedSrc := netip.MustParseAddr("100.0.0.2") + mappedTip := netip.MustParseAddr("100.64.0.41") + unmappedTip := netip.MustParseAddr("100.64.0.42") + nonTip := netip.MustParseAddr("100.0.0.3") + mappedMip := netip.MustParseAddr("100.64.0.1") + for _, tt := range []struct { + name string + src netip.Addr + tip netip.Addr + wantMip netip.Addr + wantErr error + }{ + { + name: "not-a-transit-ip-unmapped-src", + src: unmappedSrc, + tip: nonTip, + wantMip: netip.Addr{}, + wantErr: nil, + }, + { + name: "not-a-transit-ip-mapped-src", + src: mappedSrc, + tip: nonTip, + wantMip: netip.Addr{}, + wantErr: nil, + }, + { + name: "unmapped-src-transit-ip", + src: unmappedSrc, + tip: unmappedTip, + wantMip: netip.Addr{}, + wantErr: ErrUnmappedSrcAndTransitIP, + }, + { + name: "unmapped-tip-transit-ip", + src: mappedSrc, + tip: unmappedTip, + wantMip: netip.Addr{}, + wantErr: ErrUnmappedSrcAndTransitIP, + }, + { + name: "mapped-src-and-transit-ip", + src: mappedSrc, + tip: mappedTip, + wantMip: mappedMip, + wantErr: nil, + }, + } { + t.Run(tt.name, func(t *testing.T) { + c := newConn25(t.Logf) + if err := c.reconfig(sn); err != nil { + t.Fatal(err) + } + c.connector.transitIPs = map[netip.Addr]map[netip.Addr]appAddr{} + c.connector.transitIPs[mappedSrc] = map[netip.Addr]appAddr{} + c.connector.transitIPs[mappedSrc][mappedTip] = appAddr{addr: mappedMip} + mip, err := c.connector.ConnectorRealIPForTransitIPConnection(tt.src, tt.tip) + if mip != tt.wantMip { + t.Fatalf("checking magic ip: want %v, got %v", tt.wantMip, mip) + } + if err != tt.wantErr { + t.Fatalf("checking error: want %v, got %v", tt.wantErr, err) + } + }) + } +}