feature/conn25: stop adding multiple entries for same domain+dst

We should only add one entry to our magic ips for each domain+dst and
look up any existing entry instead of always creating a new one.

Fixes tailscale/corp#34252
Signed-off-by: Fran Bull <fran@tailscale.com>
This commit is contained in:
Fran Bull
2026-02-20 08:00:17 -08:00
committed by franbull
parent 2d21dd46cd
commit 120f27f383
2 changed files with 139 additions and 69 deletions
+60 -24
View File
@@ -12,7 +12,6 @@ import (
"errors" "errors"
"net/http" "net/http"
"net/netip" "net/netip"
"strings"
"sync" "sync"
"go4.org/netipx" "go4.org/netipx"
@@ -310,8 +309,8 @@ const AppConnectorsExperimentalAttrName = "tailscale.com/app-connectors-experime
type config struct { type config struct {
isConfigured bool isConfigured bool
apps []appctype.Conn25Attr apps []appctype.Conn25Attr
appsByDomain map[string][]string appsByDomain map[dnsname.FQDN][]string
selfRoutedDomains set.Set[string] selfRoutedDomains set.Set[dnsname.FQDN]
} }
func configFromNodeView(n tailcfg.NodeView) (config, error) { func configFromNodeView(n tailcfg.NodeView) (config, error) {
@@ -326,8 +325,8 @@ func configFromNodeView(n tailcfg.NodeView) (config, error) {
cfg := config{ cfg := config{
isConfigured: true, isConfigured: true,
apps: apps, apps: apps,
appsByDomain: map[string][]string{}, appsByDomain: map[dnsname.FQDN][]string{},
selfRoutedDomains: set.Set[string]{}, selfRoutedDomains: set.Set[dnsname.FQDN]{},
} }
for _, app := range apps { for _, app := range apps {
selfMatchesTags := false selfMatchesTags := false
@@ -342,10 +341,9 @@ func configFromNodeView(n tailcfg.NodeView) (config, error) {
if err != nil { if err != nil {
return config{}, err return config{}, err
} }
key := fqdn.WithTrailingDot() mak.Set(&cfg.appsByDomain, fqdn, append(cfg.appsByDomain[fqdn], app.Name))
mak.Set(&cfg.appsByDomain, key, append(cfg.appsByDomain[key], app.Name))
if selfMatchesTags { if selfMatchesTags {
cfg.selfRoutedDomains.Add(key) cfg.selfRoutedDomains.Add(fqdn)
} }
} }
} }
@@ -362,9 +360,8 @@ type client struct {
mu sync.Mutex // protects the fields below mu sync.Mutex // protects the fields below
magicIPPool *ippool magicIPPool *ippool
transitIPPool *ippool transitIPPool *ippool
// map of magic IP -> (transit IP, app) assignments addrAssignments
magicIPs map[netip.Addr]appAddr config config
config config
} }
func (c *client) isConfigured() bool { func (c *client) isConfigured() bool {
@@ -407,13 +404,7 @@ func (c *client) reconfig(newCfg config) error {
return nil return nil
} }
func (c *client) setMagicIP(magicAddr, transitAddr netip.Addr, app string) { func (c *client) isConnectorDomain(domain dnsname.FQDN) bool {
c.mu.Lock()
defer c.mu.Unlock()
mak.Set(&c.magicIPs, magicAddr, appAddr{addr: transitAddr, app: app})
}
func (c *client) isConnectorDomain(domain string) bool {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
appNames, ok := c.config.appsByDomain[domain] appNames, ok := c.config.appsByDomain[domain]
@@ -424,9 +415,12 @@ func (c *client) isConnectorDomain(domain string) 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 string, dst netip.Addr) (addrs, error) { func (c *client) reserveAddresses(domain dnsname.FQDN, dst netip.Addr) (addrs, error) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if existing, ok := c.assignments.lookupByDomainDst(domain, dst); ok {
return existing, nil
}
appNames, _ := c.config.appsByDomain[domain] appNames, _ := c.config.appsByDomain[domain]
// only reserve for first app // only reserve for first app
app := appNames[0] app := appNames[0]
@@ -438,17 +432,20 @@ func (c *client) reserveAddresses(domain string, dst netip.Addr) (addrs, error)
if err != nil { if err != nil {
return addrs{}, err return addrs{}, err
} }
addrs := addrs{ as := addrs{
dst: dst, dst: dst,
magic: mip, magic: mip,
transit: tip, transit: tip,
app: app, app: app,
domain: domain,
} }
return addrs, nil if err := c.assignments.insert(as); err != nil {
return addrs{}, err
}
return as, nil
} }
func (c *client) enqueueAddressAssignment(addrs addrs) { func (c *client) enqueueAddressAssignment(addrs addrs) {
c.setMagicIP(addrs.magic, addrs.transit, addrs.app)
// TODO(fran) 2026-02-03 asynchronously send peerapi req to connector to // TODO(fran) 2026-02-03 asynchronously send peerapi req to connector to
// allocate these addresses for us. // allocate these addresses for us.
} }
@@ -483,8 +480,12 @@ func (c *client) mapDNSResponse(buf []byte) []byte {
switch h.Type { switch h.Type {
case dnsmessage.TypeA: case dnsmessage.TypeA:
domain := strings.ToLower(h.Name.String()) domain, err := dnsname.ToFQDN(h.Name.String())
if len(domain) == 0 || !c.isConnectorDomain(domain) { if err != nil {
c.logf("bad dnsname: %v", err)
return buf
}
if !c.isConnectorDomain(domain) {
if err := p.SkipAnswer(); err != nil { if err := p.SkipAnswer(); err != nil {
c.logf("error parsing dns response: %v", err) c.logf("error parsing dns response: %v", err)
return buf return buf
@@ -540,9 +541,44 @@ type addrs struct {
dst netip.Addr dst netip.Addr
magic netip.Addr magic netip.Addr
transit netip.Addr transit netip.Addr
domain dnsname.FQDN
app string app string
} }
func (c addrs) isValid() bool { func (c addrs) isValid() bool {
return c.dst.IsValid() return c.dst.IsValid()
} }
// domainDst is a key for looking up an existing address assignment by the
// DNS response domain and destination IP pair.
type domainDst struct {
domain dnsname.FQDN
dst netip.Addr
}
// addrAssignments is the collection of addrs assigned by this client
// supporting lookup by magicip or domain+dst
type addrAssignments struct {
byMagicIP map[netip.Addr]addrs
byDomainDst map[domainDst]addrs
}
func (a *addrAssignments) insert(as addrs) error {
// we likely will want to allow overwriting in the future when we
// have address expiry, but for now this should not happen
if _, ok := a.byMagicIP[as.magic]; ok {
return errors.New("byMagicIP key exists")
}
ddst := domainDst{domain: as.domain, dst: as.dst}
if _, ok := a.byDomainDst[ddst]; ok {
return errors.New("byDomainDst key exists")
}
mak.Set(&a.byMagicIP, as.magic, as)
mak.Set(&a.byDomainDst, ddst, as)
return nil
}
func (a *addrAssignments) lookupByDomainDst(domain dnsname.FQDN, dst netip.Addr) (addrs, bool) {
v, ok := a.byDomainDst[domainDst{domain: domain, dst: dst}]
return v, ok
}
+79 -45
View File
@@ -16,6 +16,8 @@ import (
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/appctype" "tailscale.com/types/appctype"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/dnsname"
"tailscale.com/util/must"
"tailscale.com/util/set" "tailscale.com/util/set"
) )
@@ -206,34 +208,16 @@ func TestTransitIPTargetUnknownTIP(t *testing.T) {
} }
} }
func TestSetMagicIP(t *testing.T) {
c := newConn25(logger.Discard)
mip := netip.MustParseAddr("0.0.0.1")
tip := netip.MustParseAddr("0.0.0.2")
app := "a"
c.client.setMagicIP(mip, tip, app)
val, ok := c.client.magicIPs[mip]
if !ok {
t.Fatal("expected there to be a value stored for the magic IP")
}
if val.addr != tip {
t.Fatalf("want %v, got %v", tip, val.addr)
}
if val.app != app {
t.Fatalf("want %s, got %s", app, val.app)
}
}
func TestReserveIPs(t *testing.T) { func TestReserveIPs(t *testing.T) {
c := newConn25(logger.Discard) c := newConn25(logger.Discard)
c.client.magicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24")) c.client.magicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24"))
c.client.transitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24")) c.client.transitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24"))
mbd := map[string][]string{} mbd := map[dnsname.FQDN][]string{}
mbd["example.com."] = []string{"a"} mbd["example.com."] = []string{"a"}
c.client.config.appsByDomain = mbd c.client.config.appsByDomain = mbd
dst := netip.MustParseAddr("0.0.0.1") dst := netip.MustParseAddr("0.0.0.1")
con, err := c.client.reserveAddresses("example.com.", dst) addrs, err := c.client.reserveAddresses("example.com.", dst)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -242,18 +226,22 @@ func TestReserveIPs(t *testing.T) {
wantMagic := netip.MustParseAddr("100.64.0.0") // first from magic pool wantMagic := netip.MustParseAddr("100.64.0.0") // first from magic pool
wantTransit := netip.MustParseAddr("169.254.0.0") // first from transit pool wantTransit := netip.MustParseAddr("169.254.0.0") // first from transit pool
wantApp := "a" // the app name related to example.com. wantApp := "a" // the app name related to example.com.
wantDomain := must.Get(dnsname.ToFQDN("example.com."))
if wantDst != con.dst { if wantDst != addrs.dst {
t.Errorf("want %v, got %v", wantDst, con.dst) t.Errorf("want %v, got %v", wantDst, addrs.dst)
} }
if wantMagic != con.magic { if wantMagic != addrs.magic {
t.Errorf("want %v, got %v", wantMagic, con.magic) t.Errorf("want %v, got %v", wantMagic, addrs.magic)
} }
if wantTransit != con.transit { if wantTransit != addrs.transit {
t.Errorf("want %v, got %v", wantTransit, con.transit) t.Errorf("want %v, got %v", wantTransit, addrs.transit)
} }
if wantApp != con.app { if wantApp != addrs.app {
t.Errorf("want %s, got %s", wantApp, con.app) t.Errorf("want %s, got %s", wantApp, addrs.app)
}
if wantDomain != addrs.domain {
t.Errorf("want %s, got %s", wantDomain, addrs.domain)
} }
} }
@@ -287,8 +275,8 @@ func TestConfigReconfig(t *testing.T) {
cfg []appctype.Conn25Attr cfg []appctype.Conn25Attr
tags []string tags []string
wantErr bool wantErr bool
wantAppsByDomain map[string][]string wantAppsByDomain map[dnsname.FQDN][]string
wantSelfRoutedDomains set.Set[string] wantSelfRoutedDomains set.Set[dnsname.FQDN]
}{ }{
{ {
name: "bad-config", name: "bad-config",
@@ -302,11 +290,11 @@ func TestConfigReconfig(t *testing.T) {
{Name: "two", Domains: []string{"b.example.com"}, Connectors: []string{"tag:two"}}, {Name: "two", Domains: []string{"b.example.com"}, Connectors: []string{"tag:two"}},
}, },
tags: []string{"tag:one"}, tags: []string{"tag:one"},
wantAppsByDomain: map[string][]string{ wantAppsByDomain: map[dnsname.FQDN][]string{
"a.example.com.": {"one"}, "a.example.com.": {"one"},
"b.example.com.": {"two"}, "b.example.com.": {"two"},
}, },
wantSelfRoutedDomains: set.SetOf([]string{"a.example.com."}), wantSelfRoutedDomains: set.SetOf([]dnsname.FQDN{"a.example.com."}),
}, },
{ {
name: "more-complex", name: "more-complex",
@@ -317,7 +305,7 @@ func TestConfigReconfig(t *testing.T) {
{Name: "four", Domains: []string{"4.b.example.com", "4.d.example.com"}, Connectors: []string{"tag:four"}}, {Name: "four", Domains: []string{"4.b.example.com", "4.d.example.com"}, Connectors: []string{"tag:four"}},
}, },
tags: []string{"tag:onea", "tag:four", "tag:unrelated"}, tags: []string{"tag:onea", "tag:four", "tag:unrelated"},
wantAppsByDomain: map[string][]string{ wantAppsByDomain: map[dnsname.FQDN][]string{
"1.a.example.com.": {"one"}, "1.a.example.com.": {"one"},
"1.b.example.com.": {"one", "three"}, "1.b.example.com.": {"one", "three"},
"1.c.example.com.": {"three"}, "1.c.example.com.": {"three"},
@@ -326,7 +314,7 @@ 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([]string{"1.a.example.com.", "1.b.example.com.", "4.b.example.com.", "4.d.example.com."}), wantSelfRoutedDomains: set.SetOf([]dnsname.FQDN{"1.a.example.com.", "1.b.example.com.", "4.b.example.com.", "4.d.example.com."}),
}, },
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -431,18 +419,24 @@ func TestMapDNSResponse(t *testing.T) {
} }
for _, tt := range []struct { for _, tt := range []struct {
name string name string
domain string domain string
addrs []dnsmessage.AResource addrs []dnsmessage.AResource
wantMagicIPs map[netip.Addr]appAddr wantByMagicIP map[netip.Addr]addrs
}{ }{
{ {
name: "one-ip-matches", name: "one-ip-matches",
domain: "example.com.", domain: "example.com.",
addrs: []dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}}, addrs: []dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
// these are 'expected' because they are the beginning of the provided pools // these are 'expected' because they are the beginning of the provided pools
wantMagicIPs: map[netip.Addr]appAddr{ wantByMagicIP: map[netip.Addr]addrs{
netip.MustParseAddr("100.64.0.0"): {app: "app1", addr: netip.MustParseAddr("100.64.0.40")}, 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",
},
}, },
}, },
{ {
@@ -452,9 +446,21 @@ func TestMapDNSResponse(t *testing.T) {
{A: [4]byte{1, 0, 0, 0}}, {A: [4]byte{1, 0, 0, 0}},
{A: [4]byte{2, 0, 0, 0}}, {A: [4]byte{2, 0, 0, 0}},
}, },
wantMagicIPs: map[netip.Addr]appAddr{ wantByMagicIP: map[netip.Addr]addrs{
netip.MustParseAddr("100.64.0.0"): {app: "app1", addr: netip.MustParseAddr("100.64.0.40")}, netip.MustParseAddr("100.64.0.0"): {
netip.MustParseAddr("100.64.0.1"): {app: "app1", addr: netip.MustParseAddr("100.64.0.41")}, 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",
},
netip.MustParseAddr("100.64.0.1"): {
domain: "example.com.",
dst: netip.MustParseAddr("2.0.0.0"),
magic: netip.MustParseAddr("100.64.0.1"),
transit: netip.MustParseAddr("100.64.0.41"),
app: "app1",
},
}, },
}, },
{ {
@@ -482,9 +488,37 @@ func TestMapDNSResponse(t *testing.T) {
if !reflect.DeepEqual(dnsResp, bs) { if !reflect.DeepEqual(dnsResp, bs) {
t.Fatal("shouldn't be changing the bytes (yet)") t.Fatal("shouldn't be changing the bytes (yet)")
} }
if diff := cmp.Diff(tt.wantMagicIPs, c.client.magicIPs, cmpopts.EquateComparable(appAddr{}, netip.Addr{})); diff != "" { if diff := cmp.Diff(tt.wantByMagicIP, c.client.assignments.byMagicIP, cmpopts.EquateComparable(addrs{}, netip.Addr{})); diff != "" {
t.Errorf("magicIPs diff (-want, +got):\n%s", diff) t.Errorf("byMagicIP diff (-want, +got):\n%s", diff)
} }
}) })
} }
} }
func TestReserveAddressesDeduplicated(t *testing.T) {
c := newConn25(logger.Discard)
c.client.magicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24"))
c.client.transitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24"))
c.client.config.appsByDomain = map[dnsname.FQDN][]string{"example.com.": {"a"}}
dst := netip.MustParseAddr("0.0.0.1")
first, err := c.client.reserveAddresses("example.com.", dst)
if err != nil {
t.Fatal(err)
}
second, err := c.client.reserveAddresses("example.com.", dst)
if err != nil {
t.Fatal(err)
}
if first != second {
t.Errorf("expected same addrs on repeated call, got first=%v second=%v", first, second)
}
if got := len(c.client.assignments.byMagicIP); got != 1 {
t.Errorf("want 1 entry in byMagicIP, got %d", got)
}
if got := len(c.client.assignments.byDomainDst); got != 1 {
t.Errorf("want 1 entry in byDomainDst, got %d", got)
}
}