appc,feature/conn25: prevent clients from forwarding DNS requests and

modifying DNS responses for domains they are also connectors for

For Connectors 2025, determine if a client is configured as a
connector and what domains it is a connector for. When acting as a
client, don't install Split DNS routes to other connectors for those
domains, and don't alter DNS responses for those domains. The responses
are forwarded back to the original client, which in turn does the alteration,
swapping the real IP for a Magic IP.

A client is also a connector for a domain if it has tags that overlap
with tags in the configured policy, and --advertise-connector=true
in the prefs (not in the self-node Hostinfo from the netmap). We use the prefs
as the source of truth because control only gets a copy from the prefs, and
may drift. And the AppConnector field is currently zeroed out in the
self-node Hostinfo from control.

The extension adds a ProfileStateChange hook to process prefs changes,
and the config type is split into prefs and nodeview sub-configs.

Fixes tailscale/corp#39317

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
This commit is contained in:
Michael Ben-Ami
2026-04-07 15:54:52 -04:00
committed by mzbenami
parent 4f47c3c93d
commit 1dc08f4d41
5 changed files with 437 additions and 121 deletions
+18 -5
View File
@@ -16,7 +16,7 @@ import (
const AppConnectorsExperimentalAttrName = "tailscale.com/app-connectors-experimental" const AppConnectorsExperimentalAttrName = "tailscale.com/app-connectors-experimental"
func isEligibleConnector(peer tailcfg.NodeView) bool { func isPeerEligibleConnector(peer tailcfg.NodeView) bool {
if !peer.Valid() || !peer.Hostinfo().Valid() { if !peer.Valid() || !peer.Hostinfo().Valid() {
return false return false
} }
@@ -39,7 +39,7 @@ func sortByPreference(ns []tailcfg.NodeView) {
func PickConnector(nb ipnext.NodeBackend, app appctype.Conn25Attr) []tailcfg.NodeView { func PickConnector(nb ipnext.NodeBackend, app appctype.Conn25Attr) []tailcfg.NodeView {
appTagsSet := set.SetOf(app.Connectors) appTagsSet := set.SetOf(app.Connectors)
matches := nb.AppendMatchingPeers(nil, func(n tailcfg.NodeView) bool { matches := nb.AppendMatchingPeers(nil, func(n tailcfg.NodeView) bool {
if !isEligibleConnector(n) { if !isPeerEligibleConnector(n) {
return false return false
} }
for _, t := range n.Tags().All() { for _, t := range n.Tags().All() {
@@ -55,7 +55,7 @@ func PickConnector(nb ipnext.NodeBackend, app appctype.Conn25Attr) []tailcfg.Nod
// PickSplitDNSPeers looks at the netmap peers capabilities and finds which peers // PickSplitDNSPeers looks at the netmap peers capabilities and finds which peers
// want to be connectors for which domains. // want to be connectors for which domains.
func PickSplitDNSPeers(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.NodeView, peers map[tailcfg.NodeID]tailcfg.NodeView) map[string][]tailcfg.NodeView { func PickSplitDNSPeers(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.NodeView, peers map[tailcfg.NodeID]tailcfg.NodeView, isSelfEligibleConnector bool) map[string][]tailcfg.NodeView {
var m map[string][]tailcfg.NodeView var m map[string][]tailcfg.NodeView
if !hasCap(AppConnectorsExperimentalAttrName) { if !hasCap(AppConnectorsExperimentalAttrName) {
return m return m
@@ -65,21 +65,34 @@ func PickSplitDNSPeers(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.
return m return m
} }
tagToDomain := make(map[string][]string) tagToDomain := make(map[string][]string)
selfTags := set.SetOf(self.Tags().AsSlice())
selfRoutedDomains := set.Set[string]{}
for _, app := range apps { for _, app := range apps {
for _, tag := range app.Connectors { for _, tag := range app.Connectors {
tagToDomain[tag] = append(tagToDomain[tag], app.Domains...) domains := tagToDomain[tag]
domains = slices.Grow(domains, len(app.Domains))
for _, d := range app.Domains {
if isSelfEligibleConnector && selfTags.Contains(tag) {
selfRoutedDomains.Add(d)
}
domains = append(domains, d)
}
tagToDomain[tag] = domains
} }
} }
// NodeIDs are Comparable, and we have a map of NodeID to NodeView anyway, so // NodeIDs are Comparable, and we have a map of NodeID to NodeView anyway, so
// use a Set of NodeIDs to deduplicate, and populate into a []NodeView later. // use a Set of NodeIDs to deduplicate, and populate into a []NodeView later.
var work map[string]set.Set[tailcfg.NodeID] var work map[string]set.Set[tailcfg.NodeID]
for _, peer := range peers { for _, peer := range peers {
if !isEligibleConnector(peer) { if !isPeerEligibleConnector(peer) {
continue continue
} }
for _, t := range peer.Tags().All() { for _, t := range peer.Tags().All() {
domains := tagToDomain[t] domains := tagToDomain[t]
for _, domain := range domains { for _, domain := range domains {
if selfRoutedDomains.Contains(domain) {
continue
}
if work[domain] == nil { if work[domain] == nil {
mak.Set(&work, domain, set.Set[tailcfg.NodeID]{}) mak.Set(&work, domain, set.Set[tailcfg.NodeID]{})
} }
+88 -5
View File
@@ -47,10 +47,12 @@ func TestPickSplitDNSPeers(t *testing.T) {
nvp4 := makeNodeView(4, "p4", []string{"tag:two", "tag:three2", "tag:four2"}) nvp4 := makeNodeView(4, "p4", []string{"tag:two", "tag:three2", "tag:four2"})
for _, tt := range []struct { for _, tt := range []struct {
name string name string
want map[string][]tailcfg.NodeView peers []tailcfg.NodeView
peers []tailcfg.NodeView config []tailcfg.RawMessage
config []tailcfg.RawMessage isEligibleConnector bool
selfTags []string
want map[string][]tailcfg.NodeView
}{ }{
{ {
name: "empty", name: "empty",
@@ -111,6 +113,85 @@ func TestPickSplitDNSPeers(t *testing.T) {
"c.example.com": {nvp2, nvp4}, "c.example.com": {nvp2, nvp4},
}, },
}, },
{
name: "self-connector-exclude-self-domains",
config: []tailcfg.RawMessage{
tailcfg.RawMessage(appOneBytes),
tailcfg.RawMessage(appTwoBytes),
tailcfg.RawMessage(appThreeBytes),
tailcfg.RawMessage(appFourBytes),
},
peers: []tailcfg.NodeView{
nvp1,
nvp2,
nvp3,
nvp4,
},
isEligibleConnector: true,
selfTags: []string{"tag:three1"},
want: map[string][]tailcfg.NodeView{
// woo.b.example.com and hoo.b.example.com are covered
// by tag:three1, and so is this self-node.
// So those domains should not be routed to peers.
// woo.b.example.com is also covered by another tag,
// but still not included since this connector can route to it.
"example.com": {nvp1},
"a.example.com": {nvp3, nvp4},
"c.example.com": {nvp2, nvp4},
},
},
{
name: "self-eligible-connector-no-matching-tag-include-all-domains",
config: []tailcfg.RawMessage{
tailcfg.RawMessage(appOneBytes),
tailcfg.RawMessage(appTwoBytes),
tailcfg.RawMessage(appThreeBytes),
tailcfg.RawMessage(appFourBytes),
},
peers: []tailcfg.NodeView{
nvp1,
nvp2,
nvp3,
nvp4,
},
isEligibleConnector: true,
selfTags: []string{"tag:unrelated"},
want: map[string][]tailcfg.NodeView{
// Self has prefs set but no tags matching any app,
// so no domains are self-routed and all appear.
"example.com": {nvp1},
"a.example.com": {nvp3, nvp4},
"woo.b.example.com": {nvp2, nvp3, nvp4},
"hoo.b.example.com": {nvp3, nvp4},
"c.example.com": {nvp2, nvp4},
},
},
{
name: "self-not-eligible-connector-but-tagged-include-all-domains",
config: []tailcfg.RawMessage{
tailcfg.RawMessage(appOneBytes),
tailcfg.RawMessage(appTwoBytes),
tailcfg.RawMessage(appThreeBytes),
tailcfg.RawMessage(appFourBytes),
},
peers: []tailcfg.NodeView{
nvp1,
nvp2,
nvp3,
nvp4,
},
selfTags: []string{"tag:three1"},
want: map[string][]tailcfg.NodeView{
// Even though this self node has a tag for an app
// the prefs don't advertise as connector, so
// should still route through other connectors.
"example.com": {nvp1},
"a.example.com": {nvp3, nvp4},
"woo.b.example.com": {nvp2, nvp3, nvp4},
"hoo.b.example.com": {nvp3, nvp4},
"c.example.com": {nvp2, nvp4},
},
},
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
selfNode := &tailcfg.Node{} selfNode := &tailcfg.Node{}
@@ -119,6 +200,7 @@ func TestPickSplitDNSPeers(t *testing.T) {
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): tt.config, tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): tt.config,
} }
} }
selfNode.Tags = append(selfNode.Tags, tt.selfTags...)
selfView := selfNode.View() selfView := selfNode.View()
peers := map[tailcfg.NodeID]tailcfg.NodeView{} peers := map[tailcfg.NodeID]tailcfg.NodeView{}
for _, p := range tt.peers { for _, p := range tt.peers {
@@ -126,7 +208,8 @@ func TestPickSplitDNSPeers(t *testing.T) {
} }
got := PickSplitDNSPeers(func(_ tailcfg.NodeCapability) bool { got := PickSplitDNSPeers(func(_ tailcfg.NodeCapability) bool {
return true return true
}, selfView, peers) }, selfView, peers, tt.isEligibleConnector)
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("got %v, want %v", got, tt.want) t.Fatalf("got %v, want %v", got, tt.want)
} }
+125 -59
View File
@@ -25,6 +25,7 @@ import (
"tailscale.com/appc" "tailscale.com/appc"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/feature" "tailscale.com/feature"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnext" "tailscale.com/ipn/ipnext"
"tailscale.com/ipn/ipnlocal" "tailscale.com/ipn/ipnlocal"
"tailscale.com/net/packet" "tailscale.com/net/packet"
@@ -124,6 +125,7 @@ func (e *extension) Init(host ipnext.Host) error {
if err := e.installHooks(dph); err != nil { if err := e.installHooks(dph); err != nil {
return err return err
} }
e.seedPrefsConfig()
ctx, cancel := context.WithCancelCause(context.Background()) ctx, cancel := context.WithCancelCause(context.Background())
e.ctxCancel = cancel e.ctxCancel = cancel
@@ -167,10 +169,13 @@ func (e *extension) installHooks(dph *datapathHandler) error {
} }
// Manage how we react to changes to the current node, // Manage how we react to changes to the current node,
// including property changes (e.g. HostInfo, Capabilities, CapMap) // including property changes (e.g. HostInfo, Capabilities, CapMap).
// and profile switches.
e.host.Hooks().OnSelfChange.Add(e.onSelfChange) e.host.Hooks().OnSelfChange.Add(e.onSelfChange)
// Manage how we react profile state changes, which include
// prefs changes.
e.host.Hooks().ProfileStateChange.Add(e.profileStateChange)
// Allow the client to send packets with Transit IP destinations // Allow the client to send packets with Transit IP destinations
// in the link-local space. // in the link-local space.
e.host.Hooks().Filter.LinkLocalAllowHooks.Add(func(p packet.Parsed) (bool, string) { e.host.Hooks().Filter.LinkLocalAllowHooks.Add(func(p packet.Parsed) (bool, string) {
@@ -217,6 +222,15 @@ 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) return c.client.transitIPForMagicIP(m)
@@ -229,7 +243,7 @@ func (c *Conn25) ConnectorRealIPForTransitIPConnection(src, transit netip.Addr)
func (e *extension) getMagicRange() views.Slice[netip.Prefix] { func (e *extension) getMagicRange() views.Slice[netip.Prefix] {
cfg := e.conn25.client.getConfig() cfg := e.conn25.client.getConfig()
return views.SliceOf(slices.Concat(cfg.v4MagicIPSet.Prefixes(), cfg.v6MagicIPSet.Prefixes())) return views.SliceOf(slices.Concat(cfg.nv.v4MagicIPSet.Prefixes(), cfg.nv.v6MagicIPSet.Prefixes()))
} }
// Shutdown implements [ipnlocal.Extension]. // Shutdown implements [ipnlocal.Extension].
@@ -264,12 +278,30 @@ func (e *extension) handleConnectorTransitIP(h ipnlocal.PeerAPIHandler, w http.R
w.Write(bs) w.Write(bs)
} }
// 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) {
err := e.conn25.reconfig(selfNode) cfg := e.conn25.client.getConfig()
nvCfg, err := configFromNodeView(selfNode)
if err != nil { if err != nil {
e.conn25.client.logf("error during Reconfig onSelfChange: %v", err) e.conn25.client.logf("error generating config from self node view: %v", err)
return return
} }
cfg.nv = nvCfg
e.conn25.reconfig(cfg)
}
// 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) {
// TODO(mzb): Handle node changes. Wipe out all config?
cfg := e.conn25.client.getConfig()
cfg.prefs = configFromPrefs(prefs)
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] {
@@ -283,6 +315,7 @@ 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
client *client client *client
connector *connector connector *connector
} }
@@ -310,18 +343,12 @@ func ipSetFromIPRanges(rs []netipx.IPRange) (*netipx.IPSet, error) {
return b.IPSet() return b.IPSet()
} }
func (c *Conn25) reconfig(selfNode tailcfg.NodeView) error { func (c *Conn25) reconfig(cfg config) {
cfg, err := configFromNodeView(selfNode) c.mu.Lock()
if err != nil { defer c.mu.Unlock()
return err
} c.client.reconfig(cfg)
if err := c.client.reconfig(cfg); err != nil { c.connector.reconfig(cfg)
return err
}
if err := c.connector.reconfig(cfg); err != nil {
return err
}
return nil
} }
// mapDNSResponse parses and inspects the DNS response, and uses the // mapDNSResponse parses and inspects the DNS response, and uses the
@@ -397,7 +424,7 @@ 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.appsByName[tipr.App]; !ok { 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", c.logf("[Unexpected] peer attempt to map a transit IP referenced unknown app: node: %s, app: %q",
n.StableID(), tipr.App) n.StableID(), tipr.App)
return TransitIPResponse{Code: UnknownAppName, Message: unknownAppNameMessage} return TransitIPResponse{Code: UnknownAppName, Message: unknownAppNameMessage}
@@ -485,69 +512,71 @@ type ConnectorTransitIPResponse struct {
const AppConnectorsExperimentalAttrName = "tailscale.com/app-connectors-experimental" const AppConnectorsExperimentalAttrName = "tailscale.com/app-connectors-experimental"
// config holds the config from the policy and lookups derived from that. // nodeViewConfig holds the config derived from the self node view,
// config is not safe for concurrent use. // which includes the policy.
type config struct { // nodeViewConfig is not safe for concurrent use.
isConfigured bool type nodeViewConfig struct {
apps []appctype.Conn25Attr isConfigured bool
appsByName map[string]appctype.Conn25Attr apps []appctype.Conn25Attr
appNamesByDomain map[dnsname.FQDN][]string appsByName map[string]appctype.Conn25Attr
selfRoutedDomains set.Set[dnsname.FQDN] appNamesByDomain map[dnsname.FQDN][]string
v4TransitIPSet netipx.IPSet selfDomains set.Set[dnsname.FQDN]
v4MagicIPSet netipx.IPSet v4TransitIPSet netipx.IPSet
v6TransitIPSet netipx.IPSet v4MagicIPSet netipx.IPSet
v6MagicIPSet netipx.IPSet v6TransitIPSet netipx.IPSet
v6MagicIPSet netipx.IPSet
} }
func configFromNodeView(n tailcfg.NodeView) (config, error) { func configFromNodeView(n tailcfg.NodeView) (nodeViewConfig, 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 config{}, err return nodeViewConfig{}, err
} }
if len(apps) == 0 { if len(apps) == 0 {
return config{}, nil return nodeViewConfig{}, nil
} }
selfTags := set.SetOf(n.Tags().AsSlice()) selfTags := set.SetOf(n.Tags().AsSlice())
cfg := config{ cfg := nodeViewConfig{
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{},
selfRoutedDomains: set.Set[dnsname.FQDN]{}, selfDomains: set.Set[dnsname.FQDN]{},
} }
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 config{}, err return nodeViewConfig{}, 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 {
cfg.selfRoutedDomains.Add(fqdn) cfg.selfDomains.Add(fqdn)
} }
} }
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 // 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. // global IP pool config. For now just take it from the first app.
if len(apps) != 0 { if len(apps) != 0 {
app := apps[0] app := apps[0]
v4Mipp, err := ipSetFromIPRanges(app.V4MagicIPPool) v4Mipp, err := ipSetFromIPRanges(app.V4MagicIPPool)
if err != nil { if err != nil {
return config{}, err return nodeViewConfig{}, err
} }
v4Tipp, err := ipSetFromIPRanges(app.V4TransitIPPool) v4Tipp, err := ipSetFromIPRanges(app.V4TransitIPPool)
if err != nil { if err != nil {
return config{}, err return nodeViewConfig{}, err
} }
v6Mipp, err := ipSetFromIPRanges(app.V6MagicIPPool) v6Mipp, err := ipSetFromIPRanges(app.V6MagicIPPool)
if err != nil { if err != nil {
return config{}, err return nodeViewConfig{}, err
} }
v6Tipp, err := ipSetFromIPRanges(app.V6TransitIPPool) v6Tipp, err := ipSetFromIPRanges(app.V6TransitIPPool)
if err != nil { if err != nil {
return config{}, err return nodeViewConfig{}, err
} }
cfg.v4MagicIPSet = *v4Mipp cfg.v4MagicIPSet = *v4Mipp
cfg.v4TransitIPSet = *v4Tipp cfg.v4TransitIPSet = *v4Tipp
@@ -557,6 +586,33 @@ func configFromNodeView(n tailcfg.NodeView) (config, error) {
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.
@@ -589,7 +645,7 @@ func (c *client) transitIPForMagicIP(magicIP netip.Addr) (netip.Addr, error) {
if ok { if ok {
return v.transit, nil return v.transit, nil
} }
if !c.config.v4MagicIPSet.Contains(magicIP) && !c.config.v6MagicIPSet.Contains(magicIP) { if !c.config.nv.v4MagicIPSet.Contains(magicIP) && !c.config.nv.v6MagicIPSet.Contains(magicIP) {
return netip.Addr{}, nil return netip.Addr{}, nil
} }
return netip.Addr{}, ErrUnmappedMagicIP return netip.Addr{}, ErrUnmappedMagicIP
@@ -615,29 +671,40 @@ func (c *client) isKnownTransitIP(tip netip.Addr) bool {
return ok return ok
} }
// isConfigured reports whether the client has received both a node view
// config (from the netmap/policy) and a prefs config. Both are required
// before the feature is active.
func (c *client) isConfigured() bool { func (c *client) isConfigured() bool {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
return c.config.isConfigured return c.config.prefs.isConfigured && c.config.nv.isConfigured
} }
func (c *client) reconfig(newCfg config) error { func (c *client) reconfig(newCfg config) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.config = newCfg c.config = newCfg
c.v4MagicIPPool = newIPPool(&(newCfg.v4MagicIPSet)) c.v4MagicIPPool = newIPPool(&(newCfg.nv.v4MagicIPSet))
c.v4TransitIPPool = newIPPool(&(newCfg.v4TransitIPSet)) c.v4TransitIPPool = newIPPool(&(newCfg.nv.v4TransitIPSet))
c.v6MagicIPPool = newIPPool(&(newCfg.v6MagicIPSet)) c.v6MagicIPPool = newIPPool(&(newCfg.nv.v6MagicIPSet))
c.v6TransitIPPool = newIPPool(&(newCfg.v6TransitIPSet)) c.v6TransitIPPool = newIPPool(&(newCfg.nv.v6TransitIPSet))
return nil
} }
// isConnectorDomain returns true if the domain is expected
// to be routed through a peer connector, but returns false
// if the self node is a connector responsible for routing the
// domain, and false in all other cases.
func (c *client) isConnectorDomain(domain dnsname.FQDN) bool { func (c *client) isConnectorDomain(domain dnsname.FQDN) bool {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
appNames, ok := c.config.appNamesByDomain[domain]
if c.config.isSelfRoutedDomain(domain) {
return false
}
appNames, ok := c.config.nv.appNamesByDomain[domain]
return ok && len(appNames) > 0 return ok && len(appNames) > 0
} }
@@ -654,7 +721,7 @@ 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.appNamesByDomain[domain] appNames, _ := c.config.nv.appNamesByDomain[domain]
if len(appNames) == 0 { if len(appNames) == 0 {
return addrs{}, fmt.Errorf("no app names found for domain %q", domain) return addrs{}, fmt.Errorf("no app names found for domain %q", domain)
} }
@@ -802,7 +869,7 @@ 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().appsByName[as.app] app, ok := e.conn25.client.getConfig().nv.appsByName[as.app]
if !ok { if !ok {
e.conn25.client.logf("App not found for app: %s (domain: %s)", as.app, as.domain) e.conn25.client.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")
@@ -1056,7 +1123,7 @@ func (c *connector) realIPForTransitIPConnection(srcIP netip.Addr, transitIP net
if ok { if ok {
return v.addr, nil return v.addr, nil
} }
if !c.config.v4TransitIPSet.Contains(transitIP) && !c.config.v6TransitIPSet.Contains(transitIP) { if !c.config.nv.v4TransitIPSet.Contains(transitIP) && !c.config.nv.v6TransitIPSet.Contains(transitIP) {
return netip.Addr{}, nil return netip.Addr{}, nil
} }
return netip.Addr{}, ErrUnmappedSrcAndTransitIP return netip.Addr{}, ErrUnmappedSrcAndTransitIP
@@ -1085,11 +1152,10 @@ func (c *connector) lookupBySrcIPAndTransitIP(srcIP, transitIP netip.Addr) (appA
return v, ok return v, ok
} }
func (c *connector) reconfig(newCfg config) error { func (c *connector) reconfig(newCfg config) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.config = newCfg c.config = newCfg
return nil
} }
type addrs struct { type addrs struct {
+205 -51
View File
@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip" "net/netip"
"reflect"
"slices" "slices"
"testing" "testing"
"time" "time"
@@ -17,6 +18,7 @@ import (
"go4.org/mem" "go4.org/mem"
"go4.org/netipx" "go4.org/netipx"
"golang.org/x/net/dns/dnsmessage" "golang.org/x/net/dns/dnsmessage"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnext" "tailscale.com/ipn/ipnext"
"tailscale.com/net/dns" "tailscale.com/net/dns"
"tailscale.com/net/packet" "tailscale.com/net/packet"
@@ -339,7 +341,9 @@ 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.connector.config = config{
appsByName: map[string]appctype.Conn25Attr{appName: {}}, nv: nodeViewConfig{
appsByName: map[string]appctype.Conn25Attr{appName: {}},
},
} }
for i, peer := range tt.ctipReqPeers { for i, peer := range tt.ctipReqPeers {
@@ -398,7 +402,7 @@ func TestReserveIPs(t *testing.T) {
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.appNamesByDomain = mbd c.client.config.nv.appNamesByDomain = mbd
domain := must.Get(dnsname.ToFQDN(domainStr)) domain := must.Get(dnsname.ToFQDN(domainStr))
for _, tt := range []struct { for _, tt := range []struct {
@@ -453,29 +457,87 @@ func TestReconfig(t *testing.T) {
} }
c := newConn25(logger.Discard) c := newConn25(logger.Discard)
if c.isConfigured() {
t.Fatal("expected Conn25 to report unconfigured before reconfig")
}
sn := (&tailcfg.Node{ sn := (&tailcfg.Node{
CapMap: capMap, CapMap: capMap,
}).View() }).View()
cfg := mustConfig(t, sn, testPrefsIsConnector)
c.reconfig(cfg)
err := c.reconfig(sn) if !c.isConfigured() {
if err != nil { t.Fatal("expected Conn25 to report configured after reconfig")
t.Fatal(err)
} }
if len(c.client.config.apps) != 1 || c.client.config.apps[0].Name != "app1" { if !reflect.DeepEqual(c.client.config, c.connector.config) {
t.Fatalf("want apps to have one entry 'app1', got %v", c.client.config.apps) t.Fatal("client and connector have different configs")
}
if len(c.client.config.nv.apps) != 1 || c.client.config.nv.apps[0].Name != "app1" {
t.Fatalf("want apps to have one entry 'app1', got %v", c.client.config.nv.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 TestConfigReconfig(t *testing.T) { func TestConfigFromPrefs(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string name string
rawCfg string prefs ipn.PrefsView
cfg []appctype.Conn25Attr expected prefsConfig
tags []string }{
wantErr bool {
wantAppsByDomain map[dnsname.FQDN][]string name: "is-eligible-connector",
wantSelfRoutedDomains set.Set[dnsname.FQDN] 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)
}
})
}
}
func TestConfigFromNodeView(t *testing.T) {
for _, tt := range []struct {
name string
rawCfg string
cfg []appctype.Conn25Attr
tags []string
wantErr bool
wantAppsByDomain map[dnsname.FQDN][]string
wantSelfDomains set.Set[dnsname.FQDN]
}{ }{
{ {
name: "bad-config", name: "bad-config",
@@ -493,10 +555,10 @@ func TestConfigReconfig(t *testing.T) {
"a.example.com.": {"one"}, "a.example.com.": {"one"},
"b.example.com.": {"two"}, "b.example.com.": {"two"},
}, },
wantSelfRoutedDomains: set.SetOf([]dnsname.FQDN{"a.example.com."}), wantSelfDomains: set.SetOf([]dnsname.FQDN{"a.example.com."}),
}, },
{ {
name: "more-complex", name: "more-complex-with-connector-self-domains",
cfg: []appctype.Conn25Attr{ cfg: []appctype.Conn25Attr{
{Name: "one", Domains: []string{"1.a.example.com", "1.b.example.com"}, Connectors: []string{"tag:one", "tag:onea"}}, {Name: "one", Domains: []string{"1.a.example.com", "1.b.example.com"}, Connectors: []string{"tag:one", "tag:onea"}},
{Name: "two", Domains: []string{"2.b.example.com", "2.c.example.com"}, Connectors: []string{"tag:two", "tag:twoa"}}, {Name: "two", Domains: []string{"2.b.example.com", "2.c.example.com"}, Connectors: []string{"tag:two", "tag:twoa"}},
@@ -513,7 +575,19 @@ func TestConfigReconfig(t *testing.T) {
"4.b.example.com.": {"four"}, "4.b.example.com.": {"four"},
"4.d.example.com.": {"four"}, "4.d.example.com.": {"four"},
}, },
wantSelfRoutedDomains: set.SetOf([]dnsname.FQDN{"1.a.example.com.", "1.b.example.com.", "4.b.example.com.", "4.d.example.com."}), wantSelfDomains: set.SetOf([]dnsname.FQDN{"1.a.example.com.", "1.b.example.com.", "4.b.example.com.", "4.d.example.com."}),
},
{
name: "eligible-connector-no-matching-tag-no-self-domains",
cfg: []appctype.Conn25Attr{
{Name: "one", Domains: []string{"a.example.com"}, Connectors: []string{"tag:one"}},
{Name: "two", Domains: []string{"b.example.com"}, Connectors: []string{"tag:two"}},
},
tags: []string{"tag:unrelated"},
wantAppsByDomain: map[dnsname.FQDN][]string{
"a.example.com.": {"one"},
"b.example.com.": {"two"},
},
}, },
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -535,6 +609,7 @@ func TestConfigReconfig(t *testing.T) {
CapMap: capMap, CapMap: capMap,
Tags: tt.tags, Tags: tt.tags,
}).View() }).View()
c, err := configFromNodeView(sn) c, err := configFromNodeView(sn)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Fatalf("wantErr: %t, err: %v", tt.wantErr, err) t.Fatalf("wantErr: %t, err: %v", tt.wantErr, err)
@@ -542,8 +617,8 @@ func TestConfigReconfig(t *testing.T) {
if diff := cmp.Diff(tt.wantAppsByDomain, c.appNamesByDomain); diff != "" { if diff := cmp.Diff(tt.wantAppsByDomain, c.appNamesByDomain); diff != "" {
t.Errorf("appsByDomain diff (-want, +got):\n%s", diff) t.Errorf("appsByDomain diff (-want, +got):\n%s", diff)
} }
if diff := cmp.Diff(tt.wantSelfRoutedDomains, c.selfRoutedDomains); diff != "" { if diff := cmp.Diff(tt.wantSelfDomains, c.selfDomains); diff != "" {
t.Errorf("selfRoutedDomains diff (-want, +got):\n%s", diff) t.Errorf("selfDomains diff (-want, +got):\n%s", diff)
} }
}) })
} }
@@ -562,12 +637,33 @@ func makeSelfNode(t *testing.T, attrs []appctype.Conn25Attr, tags []string) tail
capMap := tailcfg.NodeCapMap{ capMap := tailcfg.NodeCapMap{
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): cfg, tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): cfg,
} }
return (&tailcfg.Node{ return (&tailcfg.Node{
CapMap: capMap, CapMap: capMap,
Tags: tags, Tags: tags,
}).View() }).View()
} }
var (
testPrefsIsConnector = (&ipn.Prefs{AppConnector: ipn.AppConnectorPrefs{Advertise: true}}).View()
testPrefsNotConnector = (&ipn.Prefs{AppConnector: ipn.AppConnectorPrefs{Advertise: false}}).View()
)
func mustConfig(t *testing.T, selfNode tailcfg.NodeView, prefs ipn.PrefsView) config {
t.Helper()
nvCfg, err := configFromNodeView(selfNode)
if err != nil {
t.Fatal(err)
}
prefsCfg := configFromPrefs(prefs)
return config{
nv: nvCfg,
prefs: prefsCfg,
}
}
func v4RangeFrom(from, to string) netipx.IPRange { func v4RangeFrom(from, to string) netipx.IPRange {
return netipx.IPRangeFrom( return netipx.IPRangeFrom(
netip.MustParseAddr("100.64.0."+from), netip.MustParseAddr("100.64.0."+from),
@@ -706,11 +802,13 @@ func makeDNSResponseForSections(t *testing.T, questions []dnsmessage.Question, a
func TestMapDNSResponseAssignsAddrs(t *testing.T) { func TestMapDNSResponseAssignsAddrs(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string name string
domain string domain string
v4Addrs []*dnsmessage.AResource v4Addrs []*dnsmessage.AResource
v6Addrs []*dnsmessage.AAAAResource v6Addrs []*dnsmessage.AAAAResource
wantByMagicIP map[netip.Addr]addrs selfTags []string
isEligibleConnector bool
wantByMagicIP map[netip.Addr]addrs
}{ }{
{ {
name: "one-ip-matches", name: "one-ip-matches",
@@ -783,6 +881,48 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
{A: [4]byte{2, 0, 0, 0}}, {A: [4]byte{2, 0, 0, 0}},
}, },
}, },
{
name: "no-rewrite-self-routed-domain",
domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
selfTags: []string{"tag:woo"},
isEligibleConnector: true,
},
{
name: "rewrite-tagged-but-not-eligible-connector",
domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
selfTags: []string{"tag:woo"},
// isEligibleConnector is false: tag matches but prefs not set,
// so DNS response should be rewritten normally.
wantByMagicIP: map[netip.Addr]addrs{
netip.MustParseAddr("100.64.0.0"): {
domain: "example.com.",
dst: netip.MustParseAddr("1.0.0.0"),
magic: netip.MustParseAddr("100.64.0.0"),
transit: netip.MustParseAddr("100.64.0.40"),
app: "app1",
},
},
},
{
name: "rewrite-eligible-connector-no-matching-tag",
domain: "example.com.",
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
selfTags: []string{"tag:unrelated"},
isEligibleConnector: true,
// isEligibleConnector is true but tag doesn't match the app,
// so DNS response should be rewritten normally.
wantByMagicIP: map[netip.Addr]addrs{
netip.MustParseAddr("100.64.0.0"): {
domain: "example.com.",
dst: netip.MustParseAddr("1.0.0.0"),
magic: netip.MustParseAddr("100.64.0.0"),
transit: netip.MustParseAddr("100.64.0.40"),
app: "app1",
},
},
},
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var dnsResp []byte var dnsResp []byte
@@ -799,9 +939,15 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
V6MagicIPPool: []netipx.IPRange{v6RangeFrom("0", "10"), v6RangeFrom("20", "30")}, V6MagicIPPool: []netipx.IPRange{v6RangeFrom("0", "10"), v6RangeFrom("20", "30")},
V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")}, V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")},
V6TransitIPPool: []netipx.IPRange{v6RangeFrom("40", "50")}, V6TransitIPPool: []netipx.IPRange{v6RangeFrom("40", "50")},
}}, []string{}) }}, tt.selfTags)
prefs := testPrefsNotConnector
if tt.isEligibleConnector {
prefs = testPrefsIsConnector
}
c := newConn25(logger.Discard) c := newConn25(logger.Discard)
c.reconfig(sn) cfg := mustConfig(t, sn, prefs)
c.reconfig(cfg)
c.mapDNSResponse(dnsResp) c.mapDNSResponse(dnsResp)
if diff := cmp.Diff(tt.wantByMagicIP, c.client.assignments.byMagicIP, cmpopts.EquateComparable(addrs{}, netip.Addr{})); diff != "" { if diff := cmp.Diff(tt.wantByMagicIP, c.client.assignments.byMagicIP, cmpopts.EquateComparable(addrs{}, netip.Addr{})); diff != "" {
@@ -831,7 +977,7 @@ func TestReserveAddressesDeduplicated(t *testing.T) {
c.client.v6MagicIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0100::/80")) c.client.v6MagicIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0100::/80"))
c.client.v4TransitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24")) c.client.v4TransitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24"))
c.client.v6TransitIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80")) c.client.v6TransitIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80"))
c.client.config.appNamesByDomain = map[dnsname.FQDN][]string{"example.com.": {"a"}} c.client.config.nv.appNamesByDomain = map[dnsname.FQDN][]string{"example.com.": {"a"}}
first, err := c.client.reserveAddresses("example.com.", tt.dst) first, err := c.client.reserveAddresses("example.com.", tt.dst)
if err != nil { if err != nil {
@@ -880,16 +1026,25 @@ func (nb *testNodeBackend) PeerAPIBase(p tailcfg.NodeView) string {
return nb.peerAPIURL return nb.peerAPIURL
} }
type testProfileServices struct {
ipnext.ProfileServices
prefs ipn.PrefsView
}
func (p *testProfileServices) CurrentPrefs() ipn.PrefsView { return p.prefs }
type testHost struct { type testHost struct {
ipnext.Host ipnext.Host
nb ipnext.NodeBackend nb ipnext.NodeBackend
hooks ipnext.Hooks hooks ipnext.Hooks
prefs ipn.PrefsView
authReconfigAsync func() authReconfigAsync func()
} }
func (h *testHost) NodeBackend() ipnext.NodeBackend { return h.nb } func (h *testHost) NodeBackend() ipnext.NodeBackend { return h.nb }
func (h *testHost) Hooks() *ipnext.Hooks { return &h.hooks } func (h *testHost) Hooks() *ipnext.Hooks { return &h.hooks }
func (h *testHost) AuthReconfigAsync() { h.authReconfigAsync() } func (h *testHost) Profiles() ipnext.ProfileServices { return &testProfileServices{prefs: h.prefs} }
func (h *testHost) AuthReconfigAsync() { h.authReconfigAsync() }
type testSafeBackend struct { type testSafeBackend struct {
ipnext.SafeBackend ipnext.SafeBackend
@@ -950,6 +1105,7 @@ func TestAddressAssignmentIsHandled(t *testing.T) {
peers: []tailcfg.NodeView{connectorPeer}, peers: []tailcfg.NodeView{connectorPeer},
peerAPIURL: peersAPI.URL, peerAPIURL: peersAPI.URL,
}, },
prefs: testPrefsNotConnector,
authReconfigAsync: func() { authReconfigAsync: func() {
authReconfigAsyncCalled <- struct{}{} authReconfigAsyncCalled <- struct{}{}
}, },
@@ -963,10 +1119,9 @@ func TestAddressAssignmentIsHandled(t *testing.T) {
Connectors: []string{"tag:woo"}, Connectors: []string{"tag:woo"},
Domains: []string{"example.com"}, Domains: []string{"example.com"},
}}, []string{}) }}, []string{})
err := ext.conn25.reconfig(sn)
if err != nil { cfg := mustConfig(t, sn, testPrefsNotConnector)
t.Fatal(err) ext.conn25.reconfig(cfg)
}
as := addrs{ as := addrs{
dst: netip.MustParseAddr("1.2.3.4"), dst: netip.MustParseAddr("1.2.3.4"),
@@ -1046,6 +1201,8 @@ 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)
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()
var got []netip.Addr var got []netip.Addr
@@ -1323,9 +1480,7 @@ func TestMapDNSResponseRewritesResponses(t *testing.T) {
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := newConn25(logger.Discard) c := newConn25(logger.Discard)
if err := c.reconfig(sn); err != nil { c.reconfig(cfg)
t.Fatal(err)
}
bs := c.mapDNSResponse(tt.toMap) bs := c.mapDNSResponse(tt.toMap)
tt.assertFx(t, bs) tt.assertFx(t, bs)
}) })
@@ -1376,6 +1531,7 @@ func TestHandleAddressAssignmentStoresTransitIPs(t *testing.T) {
peers: connectorPeers, peers: connectorPeers,
peerAPIURL: peersAPI.URL, peerAPIURL: peersAPI.URL,
}, },
prefs: testPrefsNotConnector,
authReconfigAsync: func() { authReconfigAsync: func() {
authReconfigAsyncCalled <- struct{}{} authReconfigAsyncCalled <- struct{}{}
}, },
@@ -1396,10 +1552,9 @@ func TestHandleAddressAssignmentStoresTransitIPs(t *testing.T) {
Domains: []string{"hoo.example.com"}, Domains: []string{"hoo.example.com"},
}, },
}, []string{}) }, []string{})
err := ext.conn25.reconfig(sn)
if err != nil { cfg := mustConfig(t, sn, testPrefsNotConnector)
t.Fatal(err) ext.conn25.reconfig(cfg)
}
type lookup struct { type lookup struct {
connKey key.NodePublic connKey key.NodePublic
@@ -1576,6 +1731,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)
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")
@@ -1634,9 +1790,8 @@ func TestClientTransitIPForMagicIP(t *testing.T) {
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := newConn25(t.Logf) c := newConn25(t.Logf)
if err := c.reconfig(sn); err != nil { c.reconfig(cfg)
t.Fatal(err)
}
if err := c.client.assignments.insert(addrs{ if err := c.client.assignments.insert(addrs{
magic: mappedMip, magic: mappedMip,
transit: mappedTip, transit: mappedTip,
@@ -1666,6 +1821,8 @@ 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)
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")
mappedTip := netip.MustParseAddr("100.64.0.41") mappedTip := netip.MustParseAddr("100.64.0.41")
@@ -1717,9 +1874,7 @@ func TestConnectorRealIPForTransitIPConnection(t *testing.T) {
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := newConn25(t.Logf) c := newConn25(t.Logf)
if err := c.reconfig(sn); err != nil { c.reconfig(cfg)
t.Fatal(err)
}
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}
@@ -1812,10 +1967,9 @@ 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)
c := newConn25(t.Logf) c := newConn25(t.Logf)
if err := c.reconfig(sn); err != nil { c.reconfig(cfg)
t.Fatal(err)
}
ext := &extension{ ext := &extension{
conn25: c, conn25: c,
} }
+1 -1
View File
@@ -864,7 +864,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
addSplitDNSRoutes(nm.DNS.Routes) addSplitDNSRoutes(nm.DNS.Routes)
// Add split DNS routes for conn25 // Add split DNS routes for conn25
conn25DNSTargets := appc.PickSplitDNSPeers(nm.HasCap, nm.SelfNode, peers) conn25DNSTargets := appc.PickSplitDNSPeers(nm.HasCap, nm.SelfNode, peers, prefs.AppConnector().Advertise)
if conn25DNSTargets != nil { if conn25DNSTargets != nil {
var m map[string][]*dnstype.Resolver var m map[string][]*dnstype.Resolver
for domain, candidateSplitDNSPeers := range conn25DNSTargets { for domain, candidateSplitDNSPeers := range conn25DNSTargets {