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 <fran@tailscale.com>
This commit is contained in:
+61
-31
@@ -340,12 +340,6 @@ func (s *connector) handleTransitIPRequest(n tailcfg.NodeView, peerV4 netip.Addr
|
|||||||
return TransitIPResponse{}
|
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
|
// TransitIPRequest details a single TransitIP allocation request from a client to a
|
||||||
// connector.
|
// connector.
|
||||||
type TransitIPRequest struct {
|
type TransitIPRequest struct {
|
||||||
@@ -424,6 +418,8 @@ type config struct {
|
|||||||
appsByName map[string]appctype.Conn25Attr
|
appsByName map[string]appctype.Conn25Attr
|
||||||
appNamesByDomain map[dnsname.FQDN][]string
|
appNamesByDomain map[dnsname.FQDN][]string
|
||||||
selfRoutedDomains set.Set[dnsname.FQDN]
|
selfRoutedDomains set.Set[dnsname.FQDN]
|
||||||
|
transitIPSet netipx.IPSet
|
||||||
|
magicIPSet netipx.IPSet
|
||||||
}
|
}
|
||||||
|
|
||||||
func configFromNodeView(n tailcfg.NodeView) (config, error) {
|
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)
|
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
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,6 +485,20 @@ type client struct {
|
|||||||
config config
|
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 {
|
func (c *client) isConfigured() bool {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
@@ -486,31 +511,8 @@ func (c *client) reconfig(newCfg config) error {
|
|||||||
|
|
||||||
c.config = newCfg
|
c.config = newCfg
|
||||||
|
|
||||||
// TODO(fran) this is not the correct way to manage the pools and changes to the pools.
|
c.magicIPPool = newIPPool(&(newCfg.magicIPSet))
|
||||||
// We probably want to:
|
c.transitIPPool = newIPPool(&(newCfg.transitIPSet))
|
||||||
// * 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)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,6 +846,29 @@ type connector struct {
|
|||||||
config config
|
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 {
|
func (s *connector) reconfig(newCfg config) error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
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}]
|
v, ok := a.byDomainDst[domainDst{domain: domain, dst: dst}]
|
||||||
return v, ok
|
return v, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *addrAssignments) lookupByMagicIP(mip netip.Addr) (addrs, bool) {
|
||||||
|
v, ok := a.byMagicIP[mip]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|||||||
@@ -369,7 +369,8 @@ func TestHandleConnectorTransitIPRequest(t *testing.T) {
|
|||||||
i, j, len(wantLookup))
|
i, j, len(wantLookup))
|
||||||
}
|
}
|
||||||
pip, tip, wantDip := wantLookup[0], wantLookup[1], wantLookup[2]
|
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 {
|
if gotDip != wantDip {
|
||||||
t.Errorf("wrong result on lookup[%d][%d] ([%v], [%v]): got [%v] expected [%v]",
|
t.Errorf("wrong result on lookup[%d][%d] ([%v], [%v]): got [%v] expected [%v]",
|
||||||
i, j, pip, tip, gotDip, wantDip)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user