feature/conn25: centralize config on Conn25 with atomic access

We have two sources of truth for configuration state: the node view
(from the netmap/policy) and prefs (the --advertise-connector option).
These come with two independent update paths: onSelfChange for node view
changes and profileStateChange for pref changes.

Centralize config on Conn25 so that onSelfChange and profileStateChange
can update their independent parts without bundling changes together.
The old bundled approach required read-modify-write, which opened the
door to potential TOCTOU bugs. The node view config is
stored as an atomic.Pointer[config] and the prefs-derived field
(advertise-connector) becomes an independent atomic.Bool. onSelfChange
creates a fresh config and stores it atomically. profileStateChange sets
the bool.

This also establishes clearer lines of responsibility:

 - Configuration state lives on Conn25. Methods that need to read
   config (isConnectorDomain, mapDNSResponse, the IPMapper methods)
   are on Conn25, and use the atomics for synchronization.

 - "Active" state (address allocations, transit IP mappings) lives on
   client and connector, and use a mutex for synchronization on that
   state, without conflicting with configuration synchronization.
   It's fine for active state to be out of sync with config — e.g. a
   transit IP allocated for an app should still be tracked, and gracefully
   expired, even if the app is removed from the node view.
   Removing config responsibility from client/connector makes these
   cases clearer to handle.

 - In cases where the client or connector does need access to
   config-derived state, e.g. a client reconfiguring its IP pools from
   the IPSets in the config, we can use closures for the
   client or connector to get just the latest state it needs from the
   config. See getIPSets() in this commit.

 - As of this commit, the connector doesn't need config-derived state at
   all.

Fixes tailscale/corp#40872

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
This commit is contained in:
Michael Ben-Ami
2026-04-27 11:27:32 -04:00
committed by mzbenami
parent 159cf8707a
commit 822299642b
2 changed files with 226 additions and 270 deletions
+176 -169
View File
@@ -19,6 +19,7 @@ import (
"slices" "slices"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"go4.org/netipx" "go4.org/netipx"
@@ -123,11 +124,12 @@ func (e *extension) Init(host ipnext.Host) error {
} }
e.host = host e.host = host
dph := newDatapathHandler(e.conn25, e.conn25.client.logf) dph := newDatapathHandler(e.conn25, e.conn25.logf)
if err := e.installHooks(dph); err != nil { if err := e.installHooks(dph); err != nil {
return err return err
} }
e.seedPrefsConfig() profile, prefs := e.host.Profiles().CurrentProfileState()
e.profileStateChange(profile, prefs, false)
ctx, cancel := context.WithCancelCause(context.Background()) ctx, cancel := context.WithCancelCause(context.Background())
e.ctxCancel = cancel e.ctxCancel = cancel
@@ -224,28 +226,42 @@ func (e *extension) installHooks(dph *datapathHandler) error {
return nil return nil
} }
// seedPrefsConfig provides an initial prefs config before
// any hooks fire. The OnSelfChange hook needs to fire
// for Conn25 to be fully configured and ready to use.
func (e *extension) seedPrefsConfig() {
var cfg config
cfg.prefs = configFromPrefs(e.host.Profiles().CurrentPrefs())
e.conn25.reconfig(cfg)
}
// ClientTransitIPForMagicIP implements [IPMapper]. // ClientTransitIPForMagicIP implements [IPMapper].
func (c *Conn25) ClientTransitIPForMagicIP(m netip.Addr) (netip.Addr, error) { func (c *Conn25) ClientTransitIPForMagicIP(m netip.Addr) (netip.Addr, error) {
return c.client.transitIPForMagicIP(m) if addr, ok := c.client.transitIPForMagicIP(m); ok {
return addr, nil
}
cfg, ok := c.getConfig()
if !ok {
return netip.Addr{}, nil
}
if !cfg.ipSets.v4Magic.Contains(m) && !cfg.ipSets.v6Magic.Contains(m) {
return netip.Addr{}, nil
}
return netip.Addr{}, ErrUnmappedMagicIP
} }
// ConnectorRealIPForTransitIPConnection implements [IPMapper]. // ConnectorRealIPForTransitIPConnection implements [IPMapper].
func (c *Conn25) ConnectorRealIPForTransitIPConnection(src, transit netip.Addr) (netip.Addr, error) { func (c *Conn25) ConnectorRealIPForTransitIPConnection(src, transit netip.Addr) (netip.Addr, error) {
return c.connector.realIPForTransitIPConnection(src, transit) if addr, ok := c.connector.realIPForTransitIPConnection(src, transit); ok {
return addr, nil
}
cfg, ok := c.getConfig()
if !ok {
return netip.Addr{}, nil
}
if !cfg.ipSets.v4Transit.Contains(transit) && !cfg.ipSets.v6Transit.Contains(transit) {
return netip.Addr{}, nil
}
return netip.Addr{}, ErrUnmappedSrcAndTransitIP
} }
func (e *extension) getMagicRange() views.Slice[netip.Prefix] { func (e *extension) getMagicRange() views.Slice[netip.Prefix] {
cfg := e.conn25.client.getConfig() cfg, ok := e.conn25.getConfig()
return views.SliceOf(slices.Concat(cfg.nv.v4MagicIPSet.Prefixes(), cfg.nv.v6MagicIPSet.Prefixes())) if !ok {
return views.Slice[netip.Prefix]{}
}
return views.SliceOf(slices.Concat(cfg.ipSets.v4Magic.Prefixes(), cfg.ipSets.v6Magic.Prefixes()))
} }
// Shutdown implements [ipnlocal.Extension]. // Shutdown implements [ipnlocal.Extension].
@@ -281,29 +297,20 @@ func (e *extension) handleConnectorTransitIP(h ipnlocal.PeerAPIHandler, w http.R
} }
// onSelfChange implements the [ipnext.Hooks.OnSelfChange] hook. // onSelfChange implements the [ipnext.Hooks.OnSelfChange] hook.
// It reads then modifies the current config. We expect that OnSelfChange
// is not called concurrently with itself or with ProfileStateChange to
// prevent TOCTOU errors.
func (e *extension) onSelfChange(selfNode tailcfg.NodeView) { func (e *extension) onSelfChange(selfNode tailcfg.NodeView) {
cfg := e.conn25.client.getConfig() cfg, err := configFromNodeView(selfNode)
nvCfg, err := configFromNodeView(selfNode)
if err != nil { if err != nil {
e.conn25.client.logf("error generating config from self node view: %v", err) e.conn25.logf("error generating config from self node view: %v", err)
return return
} }
cfg.nv = nvCfg
e.conn25.reconfig(cfg) e.conn25.reconfig(cfg)
} }
// profileStateChange implements the [ipnext.Hooks.ProfileStateChange] hook. // profileStateChange implements the [ipnext.Hooks.ProfileStateChange] hook.
// It reads then modifies the current config. We expect that ProfileStateChange
// is not called concurrently with itself or with OnSelfChange to
// prevent TOCTOU errors.
func (e *extension) profileStateChange(loginProfile ipn.LoginProfileView, prefs ipn.PrefsView, sameNode bool) { func (e *extension) profileStateChange(loginProfile ipn.LoginProfileView, prefs ipn.PrefsView, sameNode bool) {
// TODO(mzb): Handle node changes. Wipe out all config? // TODO(mzb): Handle node changes. Wipe out all config?
cfg := e.conn25.client.getConfig() // We'll need to look at the ordering of this hook and onSelfChange.
cfg.prefs = configFromPrefs(prefs) e.conn25.prefsAdvertiseConnector.Store(prefs.AppConnector().Advertise)
e.conn25.reconfig(cfg)
} }
func (e *extension) extraWireGuardAllowedIPs(k key.NodePublic) views.Slice[netip.Prefix] { func (e *extension) extraWireGuardAllowedIPs(k key.NodePublic) views.Slice[netip.Prefix] {
@@ -317,24 +324,41 @@ type appAddr struct {
// Conn25 holds state for routing traffic for a domain via a connector. // Conn25 holds state for routing traffic for a domain via a connector.
type Conn25 struct { type Conn25 struct {
mu sync.Mutex // mu protects reconfiguration of client and connector config atomic.Pointer[config]
client *client prefsAdvertiseConnector atomic.Bool
connector *connector logf logger.Logf
client *client
connector *connector
}
func (c *Conn25) getConfig() (*config, bool) {
cfg := c.config.Load()
return cfg, cfg.isConfigured
} }
func (c *Conn25) isConfigured() bool { func (c *Conn25) isConfigured() bool {
return c.client.isConfigured() _, ok := c.getConfig()
return ok
} }
func newConn25(logf logger.Logf) *Conn25 { func newConn25(logf logger.Logf) *Conn25 {
c := &Conn25{ c := &Conn25{
client: &client{ logf: logf,
logf: logf,
addrsCh: make(chan addrs, 64),
assignments: addrAssignments{clock: tstime.StdClock{}},
},
connector: &connector{logf: logf}, connector: &connector{logf: logf},
} }
c.config.Store(&config{}) // initialize with empty to avoid nil checks
c.client = &client{
logf: logf,
addrsCh: make(chan addrs, 64),
assignments: addrAssignments{clock: tstime.StdClock{}},
getIPSets: func() ipSets {
cfg, ok := c.getConfig()
if !ok {
return emptyIPSets()
}
return cfg.ipSets
},
}
return c return c
} }
@@ -346,18 +370,9 @@ func ipSetFromIPRanges(rs []netipx.IPRange) (*netipx.IPSet, error) {
return b.IPSet() return b.IPSet()
} }
func (c *Conn25) reconfig(cfg config) { func (c *Conn25) reconfig(cfg *config) {
c.mu.Lock() c.config.Store(cfg)
defer c.mu.Unlock() c.client.reconfig()
c.client.reconfig(cfg)
c.connector.reconfig(cfg)
}
// mapDNSResponse parses and inspects the DNS response, and uses the
// contents to assign addresses for connecting.
func (c *Conn25) mapDNSResponse(buf []byte) []byte {
return c.client.mapDNSResponse(buf)
} }
const dupeTransitIPMessage = "Duplicate transit address in ConnectorTransitIPRequest" const dupeTransitIPMessage = "Duplicate transit address in ConnectorTransitIPRequest"
@@ -371,6 +386,21 @@ const unknownAppNameMessage = "The App name in the request does not match a conf
// family of the transitIP). If a peer has stored this mapping in the connector, // family of the transitIP). If a peer has stored this mapping in the connector,
// Conn25 will route traffic to TransitIPs to DestinationIPs for that peer. // Conn25 will route traffic to TransitIPs to DestinationIPs for that peer.
func (c *Conn25) handleConnectorTransitIPRequest(n tailcfg.NodeView, ctipr ConnectorTransitIPRequest) ConnectorTransitIPResponse { func (c *Conn25) handleConnectorTransitIPRequest(n tailcfg.NodeView, ctipr ConnectorTransitIPRequest) ConnectorTransitIPResponse {
resp := ConnectorTransitIPResponse{}
cfg, ok := c.getConfig()
if !ok {
// TODO(mzb): If this node is no longer configured at the
// the time of this call, perhaps there should be a top-level
// error, instead of error-per-TransitIP?
for range ctipr.TransitIPs {
resp.TransitIPs = append(resp.TransitIPs, TransitIPResponse{
Code: UnknownAppName,
Message: unknownAppNameMessage,
})
}
return resp
}
var peerIPv4, peerIPv6 netip.Addr var peerIPv4, peerIPv6 netip.Addr
for _, ip := range n.Addresses().All() { for _, ip := range n.Addresses().All() {
if !ip.IsSingleIP() || !tsaddr.IsTailscaleIP(ip.Addr()) { if !ip.IsSingleIP() || !tsaddr.IsTailscaleIP(ip.Addr()) {
@@ -383,7 +413,6 @@ func (c *Conn25) handleConnectorTransitIPRequest(n tailcfg.NodeView, ctipr Conne
} }
} }
resp := ConnectorTransitIPResponse{}
seen := map[netip.Addr]bool{} seen := map[netip.Addr]bool{}
for _, each := range ctipr.TransitIPs { for _, each := range ctipr.TransitIPs {
if seen[each.TransitIP] { if seen[each.TransitIP] {
@@ -391,10 +420,20 @@ func (c *Conn25) handleConnectorTransitIPRequest(n tailcfg.NodeView, ctipr Conne
Code: DuplicateTransitIP, Code: DuplicateTransitIP,
Message: dupeTransitIPMessage, Message: dupeTransitIPMessage,
}) })
c.connector.logf("[Unexpected] peer attempt to map a transit IP reused a transitIP: node: %s, IP: %v", c.logf("[Unexpected] peer attempt to map a transit IP reused a transitIP: node: %s, IP: %v",
n.StableID(), each.TransitIP) n.StableID(), each.TransitIP)
continue continue
} }
if _, ok := cfg.appsByName[each.App]; !ok {
resp.TransitIPs = append(resp.TransitIPs, TransitIPResponse{
Code: UnknownAppName,
Message: unknownAppNameMessage,
})
c.logf("[Unexpected] peer attempt to map a transit IP referenced unknown app: node: %s, app: %q",
n.StableID(), each.App)
continue
}
tipresp := c.connector.handleTransitIPRequest(n, peerIPv4, peerIPv6, each) tipresp := c.connector.handleTransitIPRequest(n, peerIPv4, peerIPv6, each)
seen[each.TransitIP] = true seen[each.TransitIP] = true
resp.TransitIPs = append(resp.TransitIPs, tipresp) resp.TransitIPs = append(resp.TransitIPs, tipresp)
@@ -427,12 +466,6 @@ func (c *connector) handleTransitIPRequest(n tailcfg.NodeView, peerV4 netip.Addr
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if _, ok := c.config.nv.appsByName[tipr.App]; !ok {
c.logf("[Unexpected] peer attempt to map a transit IP referenced unknown app: node: %s, app: %q",
n.StableID(), tipr.App)
return TransitIPResponse{Code: UnknownAppName, Message: unknownAppNameMessage}
}
if c.transitIPs == nil { if c.transitIPs == nil {
c.transitIPs = make(map[netip.Addr]map[netip.Addr]appAddr) c.transitIPs = make(map[netip.Addr]map[netip.Addr]appAddr)
} }
@@ -515,43 +548,58 @@ type ConnectorTransitIPResponse struct {
const AppConnectorsExperimentalAttrName = "tailscale.com/app-connectors-experimental" const AppConnectorsExperimentalAttrName = "tailscale.com/app-connectors-experimental"
// nodeViewConfig holds the config derived from the self node view, // ipSets wraps all the IPSets the config needs.
type ipSets struct {
v4Transit *netipx.IPSet
v4Magic *netipx.IPSet
v6Transit *netipx.IPSet
v6Magic *netipx.IPSet
}
func emptyIPSets() ipSets {
return ipSets{
v4Transit: &netipx.IPSet{},
v4Magic: &netipx.IPSet{},
v6Transit: &netipx.IPSet{},
v6Magic: &netipx.IPSet{},
}
}
// config holds the config derived from the self node view,
// which includes the policy. // which includes the policy.
// nodeViewConfig is not safe for concurrent use. // config is not safe for concurrent use.
type nodeViewConfig struct { type config struct {
isConfigured bool isConfigured bool
apps []appctype.Conn25Attr apps []appctype.Conn25Attr
appsByName map[string]appctype.Conn25Attr appsByName map[string]appctype.Conn25Attr
appNamesByDomain map[dnsname.FQDN][]string appNamesByDomain map[dnsname.FQDN][]string
selfDomains set.Set[dnsname.FQDN] selfDomains set.Set[dnsname.FQDN]
v4TransitIPSet netipx.IPSet ipSets ipSets
v4MagicIPSet netipx.IPSet
v6TransitIPSet netipx.IPSet
v6MagicIPSet netipx.IPSet
} }
func configFromNodeView(n tailcfg.NodeView) (nodeViewConfig, error) { func configFromNodeView(n tailcfg.NodeView) (*config, error) {
apps, err := tailcfg.UnmarshalNodeCapViewJSON[appctype.Conn25Attr](n.CapMap(), AppConnectorsExperimentalAttrName) apps, err := tailcfg.UnmarshalNodeCapViewJSON[appctype.Conn25Attr](n.CapMap(), AppConnectorsExperimentalAttrName)
if err != nil { if err != nil {
return nodeViewConfig{}, err return &config{}, err
} }
if len(apps) == 0 { if len(apps) == 0 {
return nodeViewConfig{}, nil return &config{}, nil
} }
selfTags := set.SetOf(n.Tags().AsSlice()) selfTags := set.SetOf(n.Tags().AsSlice())
cfg := nodeViewConfig{ cfg := &config{
isConfigured: true, isConfigured: true,
apps: apps, apps: apps,
appsByName: map[string]appctype.Conn25Attr{}, appsByName: map[string]appctype.Conn25Attr{},
appNamesByDomain: map[dnsname.FQDN][]string{}, appNamesByDomain: map[dnsname.FQDN][]string{},
selfDomains: set.Set[dnsname.FQDN]{}, selfDomains: set.Set[dnsname.FQDN]{},
ipSets: emptyIPSets(),
} }
for _, app := range apps { for _, app := range apps {
selfMatchesTags := slices.ContainsFunc(app.Connectors, selfTags.Contains) selfMatchesTags := slices.ContainsFunc(app.Connectors, selfTags.Contains)
for _, d := range app.Domains { for _, d := range app.Domains {
fqdn, err := normalizeDNSName(d) fqdn, err := normalizeDNSName(d)
if err != nil { if err != nil {
return nodeViewConfig{}, err return &config{}, err
} }
mak.Set(&cfg.appNamesByDomain, fqdn, append(cfg.appNamesByDomain[fqdn], app.Name)) mak.Set(&cfg.appNamesByDomain, fqdn, append(cfg.appNamesByDomain[fqdn], app.Name))
if selfMatchesTags { if selfMatchesTags {
@@ -567,62 +615,39 @@ func configFromNodeView(n tailcfg.NodeView) (nodeViewConfig, error) {
app := apps[0] app := apps[0]
v4Mipp, err := ipSetFromIPRanges(app.V4MagicIPPool) v4Mipp, err := ipSetFromIPRanges(app.V4MagicIPPool)
if err != nil { if err != nil {
return nodeViewConfig{}, err return &config{}, err
} }
v4Tipp, err := ipSetFromIPRanges(app.V4TransitIPPool) v4Tipp, err := ipSetFromIPRanges(app.V4TransitIPPool)
if err != nil { if err != nil {
return nodeViewConfig{}, err return &config{}, err
} }
v6Mipp, err := ipSetFromIPRanges(app.V6MagicIPPool) v6Mipp, err := ipSetFromIPRanges(app.V6MagicIPPool)
if err != nil { if err != nil {
return nodeViewConfig{}, err return &config{}, err
} }
v6Tipp, err := ipSetFromIPRanges(app.V6TransitIPPool) v6Tipp, err := ipSetFromIPRanges(app.V6TransitIPPool)
if err != nil { if err != nil {
return nodeViewConfig{}, err return &config{}, err
} }
cfg.v4MagicIPSet = *v4Mipp ipSets := ipSets{
cfg.v4TransitIPSet = *v4Tipp v4Magic: v4Mipp,
cfg.v6MagicIPSet = *v6Mipp v4Transit: v4Tipp,
cfg.v6TransitIPSet = *v6Tipp v6Magic: v6Mipp,
v6Transit: v6Tipp,
}
cfg.ipSets = ipSets
} }
return cfg, nil return cfg, nil
} }
// prefsConfig holds the config derived from the current prefs.
// prefsConfig is not safe for concurrent use.
type prefsConfig struct {
isConfigured bool
isEligibleConnector bool
}
func configFromPrefs(prefs ipn.PrefsView) prefsConfig {
return prefsConfig{
isConfigured: true,
isEligibleConnector: prefs.AppConnector().Advertise,
}
}
// config wraps the config from disparate sources.
// config is not safe for concurrent use.
type config struct {
nv nodeViewConfig
prefs prefsConfig
}
// isSelfRoutedDomain reports whether the self node is currently
// acting as a connector for the given domain.
func (c config) isSelfRoutedDomain(d dnsname.FQDN) bool {
return c.prefs.isEligibleConnector && c.nv.selfDomains.Contains(d)
}
// client performs the conn25 functionality for clients of connectors // client performs the conn25 functionality for clients of connectors
// It allocates magic and transit IP addresses and communicates them with // It allocates magic and transit IP addresses and communicates them with
// connectors. // connectors.
// It's safe for concurrent use. // It's safe for concurrent use.
type client struct { type client struct {
logf logger.Logf logf logger.Logf
addrsCh chan addrs addrsCh chan addrs
getIPSets func() ipSets
mu sync.Mutex // protects the fields below mu sync.Mutex // protects the fields below
v4MagicIPPool *ippool v4MagicIPPool *ippool
@@ -631,28 +656,18 @@ type client struct {
v6TransitIPPool *ippool v6TransitIPPool *ippool
assignments addrAssignments assignments addrAssignments
byConnKey map[key.NodePublic]set.Set[netip.Prefix] byConnKey map[key.NodePublic]set.Set[netip.Prefix]
config config
}
func (c *client) getConfig() config {
c.mu.Lock()
defer c.mu.Unlock()
return c.config
} }
// transitIPForMagicIP is part of the implementation of the IPMapper interface for dataflows lookups. // transitIPForMagicIP is part of the implementation of the IPMapper interface for dataflows lookups.
// See also [IPMapper.ClientTransitIPForMagicIP]. // See also [IPMapper.ClientTransitIPForMagicIP].
func (c *client) transitIPForMagicIP(magicIP netip.Addr) (netip.Addr, error) { func (c *client) transitIPForMagicIP(magicIP netip.Addr) (netip.Addr, bool) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
v, ok := c.assignments.lookupByMagicIP(magicIP) v, ok := c.assignments.lookupByMagicIP(magicIP)
if ok { if ok {
return v.transit, nil return v.transit, true
} }
if !c.config.nv.v4MagicIPSet.Contains(magicIP) && !c.config.nv.v6MagicIPSet.Contains(magicIP) { return netip.Addr{}, false
return netip.Addr{}, nil
}
return netip.Addr{}, ErrUnmappedMagicIP
} }
// linkLocalAllow returns true if the provided packet with a link-local Dst address has a // linkLocalAllow returns true if the provided packet with a link-local Dst address has a
@@ -675,40 +690,28 @@ func (c *client) isKnownTransitIP(tip netip.Addr) bool {
return ok return ok
} }
// isConfigured reports whether the client has received both a node view func (c *client) reconfig() {
// config (from the netmap/policy) and a prefs config. Both are required
// before the feature is active.
func (c *client) isConfigured() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.config.prefs.isConfigured && c.config.nv.isConfigured
}
func (c *client) reconfig(newCfg config) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.config = newCfg ipSets := c.getIPSets()
c.v4MagicIPPool = newIPPool(&(newCfg.nv.v4MagicIPSet)) c.v4MagicIPPool = newIPPool(ipSets.v4Magic)
c.v4TransitIPPool = newIPPool(&(newCfg.nv.v4TransitIPSet)) c.v4TransitIPPool = newIPPool(ipSets.v4Transit)
c.v6MagicIPPool = newIPPool(&(newCfg.nv.v6MagicIPSet)) c.v6MagicIPPool = newIPPool(ipSets.v6Magic)
c.v6TransitIPPool = newIPPool(&(newCfg.nv.v6TransitIPSet)) c.v6TransitIPPool = newIPPool(ipSets.v6Transit)
} }
// isConnectorDomain returns true if the domain is expected // isConnectorDomain returns true if the domain is expected
// to be routed through a peer connector, but returns false // to be routed through a peer connector, but returns false
// if the self node is a connector responsible for routing the // if the self node is a connector responsible for routing the
// domain, and false in all other cases. // domain, and false in all other cases.
func (c *client) isConnectorDomain(domain dnsname.FQDN) bool { func (cfg *config) isConnectorDomain(domain dnsname.FQDN, prefsAdvertiseConnector bool) bool {
c.mu.Lock() if prefsAdvertiseConnector && cfg.selfDomains.Contains(domain) {
defer c.mu.Unlock()
if c.config.isSelfRoutedDomain(domain) {
return false return false
} }
appNames, ok := c.config.nv.appNamesByDomain[domain] appNames, ok := cfg.appNamesByDomain[domain]
return ok && len(appNames) > 0 return ok && len(appNames) > 0
} }
@@ -716,7 +719,7 @@ func (c *client) isConnectorDomain(domain dnsname.FQDN) bool {
// for this domain+dst address, so that this client can use conn25 connectors. // for this domain+dst address, so that this client can use conn25 connectors.
// It checks that this domain should be routed and that this client is not itself a connector for the domain // It checks that this domain should be routed and that this client is not itself a connector for the domain
// and generally if it is valid to make the assignment. // and generally if it is valid to make the assignment.
func (c *client) reserveAddresses(domain dnsname.FQDN, dst netip.Addr) (addrs, error) { func (c *client) reserveAddresses(app string, domain dnsname.FQDN, dst netip.Addr) (addrs, error) {
if !dst.IsValid() { if !dst.IsValid() {
return addrs{}, errors.New("dst is not valid") return addrs{}, errors.New("dst is not valid")
} }
@@ -725,12 +728,6 @@ func (c *client) reserveAddresses(domain dnsname.FQDN, dst netip.Addr) (addrs, e
if existing, ok := c.assignments.lookupByDomainDst(domain, dst); ok { if existing, ok := c.assignments.lookupByDomainDst(domain, dst); ok {
return existing, nil return existing, nil
} }
appNames, _ := c.config.nv.appNamesByDomain[domain]
if len(appNames) == 0 {
return addrs{}, fmt.Errorf("no app names found for domain %q", domain)
}
// only reserve for first app
app := appNames[0]
var mip, tip netip.Addr var mip, tip netip.Addr
var err error var err error
@@ -789,7 +786,7 @@ func (e *extension) sendLoop(ctx context.Context) {
return return
case as := <-e.conn25.client.addrsCh: case as := <-e.conn25.client.addrsCh:
if err := e.handleAddressAssignment(ctx, as); err != nil { if err := e.handleAddressAssignment(ctx, as); err != nil {
e.conn25.client.logf("error handling transit IP assignment (app: %s, mip: %v, src: %v): %v", as.app, as.magic, as.dst, err) e.conn25.logf("error handling transit IP assignment (app: %s, mip: %v, src: %v): %v", as.app, as.magic, as.dst, err)
} }
} }
} }
@@ -873,9 +870,13 @@ func makePeerAPIReq(ctx context.Context, httpClient *http.Client, urlBase string
} }
func (e *extension) sendAddressAssignment(ctx context.Context, as addrs) (tailcfg.NodeView, error) { func (e *extension) sendAddressAssignment(ctx context.Context, as addrs) (tailcfg.NodeView, error) {
app, ok := e.conn25.client.getConfig().nv.appsByName[as.app] cfg, ok := e.conn25.getConfig()
if !ok { if !ok {
e.conn25.client.logf("App not found for app: %s (domain: %s)", as.app, as.domain) return tailcfg.NodeView{}, errors.New("not configured")
}
app, ok := cfg.appsByName[as.app]
if !ok {
e.conn25.logf("App not found for app: %s (domain: %s)", as.app, as.domain)
return tailcfg.NodeView{}, errors.New("app not found") return tailcfg.NodeView{}, errors.New("app not found")
} }
@@ -927,7 +928,10 @@ func makeServFail(logf logger.Logf, h dnsmessage.Header, q dnsmessage.Question)
return bs return bs
} }
func (c *client) mapDNSResponse(buf []byte) []byte { // mapDNSResponse parses and inspects the DNS response. If the domain
// is determined to belong to app this node is client for, it assigns addresses
// for connecting and rewrites the response to contain Magic IPs.
func (c *Conn25) mapDNSResponse(buf []byte) []byte {
var p dnsmessage.Parser var p dnsmessage.Parser
hdr, err := p.Start(buf) hdr, err := p.Start(buf)
if err != nil { if err != nil {
@@ -952,10 +956,23 @@ func (c *client) mapDNSResponse(buf []byte) []byte {
if err != nil { if err != nil {
return buf return buf
} }
if !c.isConnectorDomain(queriedDomain) {
cfg, ok := c.getConfig()
if !ok {
return buf return buf
} }
if !cfg.isConnectorDomain(queriedDomain, c.prefsAdvertiseConnector.Load()) {
return buf
}
appNames, _ := cfg.appNamesByDomain[queriedDomain]
if len(appNames) == 0 {
return buf
}
// only reserve for first app
app := appNames[0]
// Now we know this is a dns response we think we should rewrite, we're going to provide our response which // Now we know this is a dns response we think we should rewrite, we're going to provide our response which
// currently means we will: // currently means we will:
// * write the questions through as they are // * write the questions through as they are
@@ -964,7 +981,7 @@ func (c *client) mapDNSResponse(buf []byte) []byte {
var answers []dnsResponseRewrite var answers []dnsResponseRewrite
if question.Type != dnsmessage.TypeA && question.Type != dnsmessage.TypeAAAA { if question.Type != dnsmessage.TypeA && question.Type != dnsmessage.TypeAAAA {
c.logf("mapping dns response for connector domain, unsupported type: %v", question.Type) c.logf("mapping dns response for connector domain, unsupported type: %v", question.Type)
newBuf, err := c.rewriteDNSResponse(hdr, questions, answers) newBuf, err := c.client.rewriteDNSResponse(app, hdr, questions, answers)
if err != nil { if err != nil {
c.logf("error writing empty response for unsupported type: %v", err) c.logf("error writing empty response for unsupported type: %v", err)
return makeServFail(c.logf, hdr, question) return makeServFail(c.logf, hdr, question)
@@ -1049,7 +1066,7 @@ func (c *client) mapDNSResponse(buf []byte) []byte {
} }
} }
} }
newBuf, err := c.rewriteDNSResponse(hdr, questions, answers) newBuf, err := c.client.rewriteDNSResponse(app, hdr, questions, answers)
if err != nil { if err != nil {
c.logf("error rewriting dns response: %v", err) c.logf("error rewriting dns response: %v", err)
return makeServFail(c.logf, hdr, question) return makeServFail(c.logf, hdr, question)
@@ -1057,7 +1074,7 @@ func (c *client) mapDNSResponse(buf []byte) []byte {
return newBuf return newBuf
} }
func (c *client) rewriteDNSResponse(hdr dnsmessage.Header, questions []dnsmessage.Question, answers []dnsResponseRewrite) ([]byte, error) { func (c *client) rewriteDNSResponse(app string, hdr dnsmessage.Header, questions []dnsmessage.Question, answers []dnsResponseRewrite) ([]byte, error) {
b := dnsmessage.NewBuilder(nil, hdr) b := dnsmessage.NewBuilder(nil, hdr)
b.EnableCompression() b.EnableCompression()
if err := b.StartQuestions(); err != nil { if err := b.StartQuestions(); err != nil {
@@ -1074,7 +1091,7 @@ func (c *client) rewriteDNSResponse(hdr dnsmessage.Header, questions []dnsmessag
// make an answer for each rewrite // make an answer for each rewrite
for _, rw := range answers { for _, rw := range answers {
as, err := c.reserveAddresses(rw.domain, rw.dst) as, err := c.reserveAddresses(app, rw.domain, rw.dst)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1115,22 +1132,18 @@ type connector struct {
// transitIPs is a map of connector client peer IP -> client transitIPs that we update as connector client peers instruct us to, and then use to route traffic to its destination on behalf of connector clients. // transitIPs is a map of connector client peer IP -> client transitIPs that we update as connector client peers instruct us to, and then use to route traffic to its destination on behalf of connector clients.
// Note that each peer could potentially have two maps: one for its IPv4 address, and one for its IPv6 address. The transit IPs map for a given peer IP will contain transit IPs of the same family as the peer's IP. // Note that each peer could potentially have two maps: one for its IPv4 address, and one for its IPv6 address. The transit IPs map for a given peer IP will contain transit IPs of the same family as the peer's IP.
transitIPs map[netip.Addr]map[netip.Addr]appAddr transitIPs map[netip.Addr]map[netip.Addr]appAddr
config config
} }
// realIPForTransitIPConnection is part of the implementation of the IPMapper interface for dataflows lookups. // realIPForTransitIPConnection is part of the implementation of the IPMapper interface for dataflows lookups.
// See also [IPMapper.ConnectorRealIPForTransitIPConnection]. // See also [IPMapper.ConnectorRealIPForTransitIPConnection].
func (c *connector) realIPForTransitIPConnection(srcIP netip.Addr, transitIP netip.Addr) (netip.Addr, error) { func (c *connector) realIPForTransitIPConnection(srcIP netip.Addr, transitIP netip.Addr) (netip.Addr, bool) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
v, ok := c.lookupBySrcIPAndTransitIP(srcIP, transitIP) v, ok := c.lookupBySrcIPAndTransitIP(srcIP, transitIP)
if ok { if ok {
return v.addr, nil return v.addr, true
} }
if !c.config.nv.v4TransitIPSet.Contains(transitIP) && !c.config.nv.v6TransitIPSet.Contains(transitIP) { return netip.Addr{}, false
return netip.Addr{}, nil
}
return netip.Addr{}, ErrUnmappedSrcAndTransitIP
} }
const packetFilterAllowReason = "app connector transit IP" const packetFilterAllowReason = "app connector transit IP"
@@ -1156,12 +1169,6 @@ func (c *connector) lookupBySrcIPAndTransitIP(srcIP, transitIP netip.Addr) (appA
return v, ok return v, ok
} }
func (c *connector) reconfig(newCfg config) {
c.mu.Lock()
defer c.mu.Unlock()
c.config = newCfg
}
type addrs struct { type addrs struct {
dst netip.Addr dst netip.Addr
magic netip.Addr magic netip.Addr
+50 -101
View File
@@ -8,7 +8,6 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip" "net/netip"
"reflect"
"slices" "slices"
"testing" "testing"
"time" "time"
@@ -341,11 +340,10 @@ func TestHandleConnectorTransitIPRequest(t *testing.T) {
// Use the same Conn25 for each request in the test and seed it with a test app name. // Use the same Conn25 for each request in the test and seed it with a test app name.
c := newConn25(logger.Discard) c := newConn25(logger.Discard)
c.connector.config = config{ c.reconfig(&config{
nv: nodeViewConfig{ isConfigured: true,
appsByName: map[string]appctype.Conn25Attr{appName: {}}, appsByName: map[string]appctype.Conn25Attr{appName: {}},
}, })
}
for i, peer := range tt.ctipReqPeers { for i, peer := range tt.ctipReqPeers {
req := tt.ctipReqs[i] req := tt.ctipReqs[i]
@@ -395,15 +393,21 @@ func TestHandleConnectorTransitIPRequest(t *testing.T) {
func TestReserveIPs(t *testing.T) { func TestReserveIPs(t *testing.T) {
c := newConn25(logger.Discard) c := newConn25(logger.Discard)
c.client.v4MagicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24"))
c.client.v6MagicIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0100::/80"))
c.client.v4TransitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24"))
c.client.v6TransitIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80"))
app := "a" app := "a"
domainStr := "example.com." domainStr := "example.com."
mbd := map[dnsname.FQDN][]string{} mbd := map[dnsname.FQDN][]string{}
mbd["example.com."] = []string{app} mbd["example.com."] = []string{app}
c.client.config.nv.appNamesByDomain = mbd cfg := &config{
isConfigured: true,
appNamesByDomain: mbd,
ipSets: ipSets{
v4Magic: mustIPSetFromPrefix("100.64.0.0/24"),
v6Magic: mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0100::/80"),
v4Transit: mustIPSetFromPrefix("169.254.0.0/24"),
v6Transit: mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80"),
},
}
c.reconfig(cfg)
domain := must.Get(dnsname.ToFQDN(domainStr)) domain := must.Get(dnsname.ToFQDN(domainStr))
for _, tt := range []struct { for _, tt := range []struct {
@@ -426,7 +430,7 @@ func TestReserveIPs(t *testing.T) {
}, },
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
addrs, err := c.client.reserveAddresses(domain, tt.dst) addrs, err := c.client.reserveAddresses(app, domain, tt.dst)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -459,74 +463,26 @@ func TestReconfig(t *testing.T) {
c := newConn25(logger.Discard) c := newConn25(logger.Discard)
if c.isConfigured() { if c.isConfigured() {
t.Fatal("expected Conn25 to report unconfigured before reconfig") t.Fatal("expected Conn25 isConfigured() to report unconfigured before reconfig")
} }
sn := (&tailcfg.Node{ sn := (&tailcfg.Node{
CapMap: capMap, CapMap: capMap,
}).View() }).View()
cfg := mustConfig(t, sn, testPrefsIsConnector) cfg := mustConfig(t, sn)
c.reconfig(cfg) c.reconfig(cfg)
if !c.isConfigured() { if !c.isConfigured() {
t.Fatal("expected Conn25 to report configured after reconfig") t.Fatal("expected Conn25 isConfigured() to report configured after reconfig")
} }
if !reflect.DeepEqual(c.client.config, c.connector.config) { cfg, ok := c.getConfig()
t.Fatal("client and connector have different configs") if !ok {
t.Fatal("expected Conn25 getConfig() to report configured after reconfig")
} }
if len(c.client.config.nv.apps) != 1 || c.client.config.nv.apps[0].Name != "app1" { if len(cfg.apps) != 1 || cfg.apps[0].Name != "app1" {
t.Fatalf("want apps to have one entry 'app1', got %v", c.client.config.nv.apps) t.Fatalf("want apps to have one entry 'app1', got %v", cfg.apps)
}
if c.client.config.prefs.isEligibleConnector != true {
t.Fatal("want prefs config to have isEligibleConnector=true")
}
c.reconfig(config{
prefs: prefsConfig{isConfigured: true},
})
if c.isConfigured() {
t.Fatal("expected Conn25 to report unconfigured with only prefs config")
}
c.reconfig(config{
nv: nodeViewConfig{isConfigured: true},
})
if c.isConfigured() {
t.Fatal("expected Conn25 to report unconfigured with only nodeview config")
}
}
func TestConfigFromPrefs(t *testing.T) {
for _, tt := range []struct {
name string
prefs ipn.PrefsView
expected prefsConfig
}{
{
name: "is-eligible-connector",
prefs: testPrefsIsConnector,
expected: prefsConfig{
isConfigured: true,
isEligibleConnector: true,
},
},
{
name: "not-eligible-connector",
prefs: testPrefsNotConnector,
expected: prefsConfig{
isConfigured: true,
isEligibleConnector: false,
},
},
} {
t.Run(tt.name, func(t *testing.T) {
if diff := cmp.Diff(tt.expected, configFromPrefs(tt.prefs), cmp.AllowUnexported(prefsConfig{})); diff != "" {
t.Errorf("unexpected prefsConfig (-want,+got): %s", diff)
}
})
} }
} }
@@ -646,23 +602,16 @@ func makeSelfNode(t *testing.T, attrs []appctype.Conn25Attr, tags []string) tail
} }
var ( var (
testPrefsIsConnector = (&ipn.Prefs{AppConnector: ipn.AppConnectorPrefs{Advertise: true}}).View()
testPrefsNotConnector = (&ipn.Prefs{AppConnector: ipn.AppConnectorPrefs{Advertise: false}}).View() testPrefsNotConnector = (&ipn.Prefs{AppConnector: ipn.AppConnectorPrefs{Advertise: false}}).View()
) )
func mustConfig(t *testing.T, selfNode tailcfg.NodeView, prefs ipn.PrefsView) config { func mustConfig(t *testing.T, selfNode tailcfg.NodeView) *config {
t.Helper() t.Helper()
nvCfg, err := configFromNodeView(selfNode) cfg, err := configFromNodeView(selfNode)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return cfg
prefsCfg := configFromPrefs(prefs)
return config{
nv: nvCfg,
prefs: prefsCfg,
}
} }
func v4RangeFrom(from, to string) netipx.IPRange { func v4RangeFrom(from, to string) netipx.IPRange {
@@ -941,14 +890,11 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")}, V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")},
V6TransitIPPool: []netipx.IPRange{v6RangeFrom("40", "50")}, V6TransitIPPool: []netipx.IPRange{v6RangeFrom("40", "50")},
}}, tt.selfTags) }}, tt.selfTags)
prefs := testPrefsNotConnector
if tt.isEligibleConnector {
prefs = testPrefsIsConnector
}
c := newConn25(logger.Discard) c := newConn25(logger.Discard)
cfg := mustConfig(t, sn, prefs) cfg := mustConfig(t, sn)
c.reconfig(cfg) c.reconfig(cfg)
c.prefsAdvertiseConnector.Store(tt.isEligibleConnector)
c.mapDNSResponse(dnsResp) c.mapDNSResponse(dnsResp)
if diff := cmp.Diff( if diff := cmp.Diff(
@@ -979,19 +925,19 @@ func TestReserveAddressesDeduplicated(t *testing.T) {
}, },
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := newConn25(logger.Discard) conn25 := newConn25(t.Logf)
c.client.v4MagicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24")) c := conn25.client
c.client.v6MagicIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0100::/80")) c.v4MagicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24"))
c.client.v4TransitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24")) c.v6MagicIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0100::/80"))
c.client.v6TransitIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80")) c.v4TransitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24"))
c.client.config.nv.appNamesByDomain = map[dnsname.FQDN][]string{"example.com.": {"a"}} c.v6TransitIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80"))
first, err := c.client.reserveAddresses("example.com.", tt.dst) first, err := c.reserveAddresses("a", "example.com.", tt.dst)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
second, err := c.client.reserveAddresses("example.com.", tt.dst) second, err := c.reserveAddresses("a", "example.com.", tt.dst)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -999,10 +945,10 @@ func TestReserveAddressesDeduplicated(t *testing.T) {
if first.magic != second.magic { if first.magic != second.magic {
t.Errorf("expected same magic addrs on repeated call, got first=%v second=%v", first.magic, second.magic) t.Errorf("expected same magic addrs on repeated call, got first=%v second=%v", first.magic, second.magic)
} }
if got := len(c.client.assignments.byMagicIP); got != 1 { if got := len(c.assignments.byMagicIP); got != 1 {
t.Errorf("want 1 entry in byMagicIP, got %d", got) t.Errorf("want 1 entry in byMagicIP, got %d", got)
} }
if got := len(c.client.assignments.byDomainDst); got != 1 { if got := len(c.assignments.byDomainDst); got != 1 {
t.Errorf("want 1 entry in byDomainDst, got %d", got) t.Errorf("want 1 entry in byDomainDst, got %d", got)
} }
@@ -1039,6 +985,9 @@ type testProfileServices struct {
} }
func (p *testProfileServices) CurrentPrefs() ipn.PrefsView { return p.prefs } func (p *testProfileServices) CurrentPrefs() ipn.PrefsView { return p.prefs }
func (p *testProfileServices) CurrentProfileState() (ipn.LoginProfileView, ipn.PrefsView) {
return ipn.LoginProfileView{}, p.prefs
}
type testHost struct { type testHost struct {
ipnext.Host ipnext.Host
@@ -1127,7 +1076,7 @@ func TestAddressAssignmentIsHandled(t *testing.T) {
Domains: []string{"example.com"}, Domains: []string{"example.com"},
}}, []string{}) }}, []string{})
cfg := mustConfig(t, sn, testPrefsNotConnector) cfg := mustConfig(t, sn)
ext.conn25.reconfig(cfg) ext.conn25.reconfig(cfg)
as := addrs{ as := addrs{
@@ -1208,7 +1157,7 @@ func TestMapDNSResponseRewritesResponses(t *testing.T) {
V6TransitIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("2606:4700::6813:100"), netip.MustParseAddr("2606:4700::6813:1ff"))}, V6TransitIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("2606:4700::6813:100"), netip.MustParseAddr("2606:4700::6813:1ff"))},
}}, []string{}) }}, []string{})
cfg := mustConfig(t, sn, testPrefsNotConnector) cfg := mustConfig(t, sn)
compareToRecords := func(t *testing.T, resources []dnsmessage.Resource, want []netip.Addr) { compareToRecords := func(t *testing.T, resources []dnsmessage.Resource, want []netip.Addr) {
t.Helper() t.Helper()
@@ -1560,7 +1509,7 @@ func TestHandleAddressAssignmentStoresTransitIPs(t *testing.T) {
}, },
}, []string{}) }, []string{})
cfg := mustConfig(t, sn, testPrefsNotConnector) cfg := mustConfig(t, sn)
ext.conn25.reconfig(cfg) ext.conn25.reconfig(cfg)
type lookup struct { type lookup struct {
@@ -1738,7 +1687,7 @@ func TestClientTransitIPForMagicIP(t *testing.T) {
V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10")}, // 100.64.0.0 - 100.64.0.10 V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10")}, // 100.64.0.0 - 100.64.0.10
V6MagicIPPool: []netipx.IPRange{v6RangeFrom("0", "10")}, V6MagicIPPool: []netipx.IPRange{v6RangeFrom("0", "10")},
}}, []string{}) }}, []string{})
cfg := mustConfig(t, sn, testPrefsNotConnector) cfg := mustConfig(t, sn)
mappedMip := netip.MustParseAddr("100.64.0.0") mappedMip := netip.MustParseAddr("100.64.0.0")
mappedTip := netip.MustParseAddr("169.0.0.0") mappedTip := netip.MustParseAddr("169.0.0.0")
@@ -1813,7 +1762,7 @@ func TestClientTransitIPForMagicIP(t *testing.T) {
}); err != nil { }); err != nil {
t.Fatal(err) t.Fatal(err)
} }
tip, err := c.client.transitIPForMagicIP(tt.mip) tip, err := c.ClientTransitIPForMagicIP(tt.mip)
if tip != tt.wantTip { if tip != tt.wantTip {
t.Fatalf("checking transit ip: want %v, got %v", tt.wantTip, tip) t.Fatalf("checking transit ip: want %v, got %v", tt.wantTip, tip)
} }
@@ -1828,7 +1777,7 @@ func TestConnectorRealIPForTransitIPConnection(t *testing.T) {
sn := makeSelfNode(t, []appctype.Conn25Attr{{ sn := makeSelfNode(t, []appctype.Conn25Attr{{
V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")}, // 100.64.0.40 - 100.64.0.50 V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")}, // 100.64.0.40 - 100.64.0.50
}}, []string{}) }}, []string{})
cfg := mustConfig(t, sn, testPrefsIsConnector) cfg := mustConfig(t, sn)
mappedSrc := netip.MustParseAddr("100.0.0.1") mappedSrc := netip.MustParseAddr("100.0.0.1")
unmappedSrc := netip.MustParseAddr("100.0.0.2") unmappedSrc := netip.MustParseAddr("100.0.0.2")
@@ -1885,7 +1834,7 @@ func TestConnectorRealIPForTransitIPConnection(t *testing.T) {
c.connector.transitIPs = map[netip.Addr]map[netip.Addr]appAddr{} c.connector.transitIPs = map[netip.Addr]map[netip.Addr]appAddr{}
c.connector.transitIPs[mappedSrc] = map[netip.Addr]appAddr{} c.connector.transitIPs[mappedSrc] = map[netip.Addr]appAddr{}
c.connector.transitIPs[mappedSrc][mappedTip] = appAddr{addr: mappedMip} c.connector.transitIPs[mappedSrc][mappedTip] = appAddr{addr: mappedMip}
mip, err := c.connector.realIPForTransitIPConnection(tt.src, tt.tip) mip, err := c.ConnectorRealIPForTransitIPConnection(tt.src, tt.tip)
if mip != tt.wantMip { if mip != tt.wantMip {
t.Fatalf("checking magic ip: want %v, got %v", tt.wantMip, mip) t.Fatalf("checking magic ip: want %v, got %v", tt.wantMip, mip)
} }
@@ -1974,7 +1923,7 @@ func TestGetMagicRange(t *testing.T) {
V4MagicIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("0.0.0.1"), netip.MustParseAddr("0.0.0.3"))}, V4MagicIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("0.0.0.1"), netip.MustParseAddr("0.0.0.3"))},
V6MagicIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("::1"), netip.MustParseAddr("::3"))}, V6MagicIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("::1"), netip.MustParseAddr("::3"))},
}}, []string{}) }}, []string{})
cfg := mustConfig(t, sn, testPrefsNotConnector) cfg := mustConfig(t, sn)
c := newConn25(t.Logf) c := newConn25(t.Logf)
c.reconfig(cfg) c.reconfig(cfg)
ext := &extension{ ext := &extension{