WIP: rebase fork onto upstream/main (v1.103.0) #15

Closed
codinget wants to merge 670 commits from webnet into save/webnet-2026-07-29
3 changed files with 143 additions and 83 deletions
Showing only changes of commit 3d5102090f - Show all commits
+31 -27
View File
@@ -547,6 +547,7 @@ type ConnectorTransitIPResponse struct {
}
const AppConnectorsExperimentalAttrName = "tailscale.com/app-connectors-experimental"
const AppConnectorsExperimentalIPPoolsAttrName = "tailscale.com/app-connectors-experimental-ippools"
// ipSets wraps all the IPSets the config needs.
type ipSets struct {
@@ -586,6 +587,14 @@ func configFromNodeView(n tailcfg.NodeView) (*config, error) {
if len(apps) == 0 {
return &config{}, nil
}
poolsSlice, err := tailcfg.UnmarshalNodeCapViewJSON[appctype.Conn25PoolsAttr](n.CapMap(), AppConnectorsExperimentalIPPoolsAttrName)
if err != nil {
return &config{}, err
}
if len(poolsSlice) != 1 {
return &config{}, errors.New("must be one conn25 pools nodeattr")
}
pools := poolsSlice[0]
selfTags := set.SetOf(n.Tags().AsSlice())
cfg := &config{
isConfigured: true,
@@ -620,34 +629,29 @@ func configFromNodeView(n tailcfg.NodeView) (*config, error) {
}
// TODO(fran) 2026-03-18 we don't yet have a proper way to communicate the
// global IP pool config. For now just take it from the first app.
if len(apps) != 0 {
app := apps[0]
v4Mipp, err := ipSetFromIPRanges(app.V4MagicIPPool)
if err != nil {
return &config{}, err
}
v4Tipp, err := ipSetFromIPRanges(app.V4TransitIPPool)
if err != nil {
return &config{}, err
}
v6Mipp, err := ipSetFromIPRanges(app.V6MagicIPPool)
if err != nil {
return &config{}, err
}
v6Tipp, err := ipSetFromIPRanges(app.V6TransitIPPool)
if err != nil {
return &config{}, err
}
ipSets := ipSets{
v4Magic: v4Mipp,
v4Transit: v4Tipp,
v6Magic: v6Mipp,
v6Transit: v6Tipp,
}
cfg.ipSets = ipSets
v4Mipp, err := ipSetFromIPRanges(pools.V4MagicIPPool)
if err != nil {
return &config{}, err
}
v4Tipp, err := ipSetFromIPRanges(pools.V4TransitIPPool)
if err != nil {
return &config{}, err
}
v6Mipp, err := ipSetFromIPRanges(pools.V6MagicIPPool)
if err != nil {
return &config{}, err
}
v6Tipp, err := ipSetFromIPRanges(pools.V6TransitIPPool)
if err != nil {
return &config{}, err
}
ipSets := ipSets{
v4Magic: v4Mipp,
v4Transit: v4Tipp,
v6Magic: v6Mipp,
v6Transit: v6Tipp,
}
cfg.ipSets = ipSets
return cfg, nil
}
+108 -55
View File
@@ -458,6 +458,9 @@ func TestReconfig(t *testing.T) {
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): []tailcfg.RawMessage{
tailcfg.RawMessage(rawCfg),
},
tailcfg.NodeCapability(AppConnectorsExperimentalIPPoolsAttrName): []tailcfg.RawMessage{
tailcfg.RawMessage("{}"),
},
}
c := newConn25(logger.Discard)
@@ -485,11 +488,26 @@ func TestReconfig(t *testing.T) {
}
}
func getRawMessages[T any](t *testing.T, attrs []T) []tailcfg.RawMessage {
t.Helper()
toRet := make([]tailcfg.RawMessage, 0, len(attrs))
for _, attr := range attrs {
bs, err := json.Marshal(attr)
if err != nil {
t.Fatalf("unexpected error in test setup: %v", err)
}
toRet = append(toRet, tailcfg.RawMessage(bs))
}
return toRet
}
func TestConfigFromNodeView(t *testing.T) {
for _, tt := range []struct {
name string
rawCfg string
cfg []appctype.Conn25Attr
rawAppCfg string
appCfg []appctype.Conn25Attr
rawPoolsCfg string
poolsCfg []appctype.Conn25PoolsAttr
tags []string
wantErr bool
wantAppsByDomain map[dnsname.FQDN][]string
@@ -497,17 +515,27 @@ func TestConfigFromNodeView(t *testing.T) {
wantSelfAppNames set.Set[string]
}{
{
name: "bad-config",
rawCfg: `bad`,
wantErr: true,
name: "bad-app-config",
rawAppCfg: `bad`,
rawPoolsCfg: "{}",
wantErr: true,
},
{
name: "bad-pool-config",
appCfg: []appctype.Conn25Attr{
{Name: "one", Domains: []string{"a.example.com"}, Connectors: []string{"tag:one"}},
},
rawPoolsCfg: "bad",
wantErr: true,
},
{
name: "simple",
cfg: []appctype.Conn25Attr{
appCfg: []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:one"},
rawPoolsCfg: "{}",
tags: []string{"tag:one"},
wantAppsByDomain: map[dnsname.FQDN][]string{
"a.example.com.": {"one"},
"b.example.com.": {"two"},
@@ -517,13 +545,14 @@ func TestConfigFromNodeView(t *testing.T) {
},
{
name: "more-complex-with-connector-self-domains",
cfg: []appctype.Conn25Attr{
appCfg: []appctype.Conn25Attr{
{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: "three", Domains: []string{"1.b.example.com", "1.c.example.com"}, Connectors: []string{}},
{Name: "four", Domains: []string{"4.b.example.com", "4.d.example.com"}, Connectors: []string{"tag:four"}},
},
tags: []string{"tag:onea", "tag:four", "tag:unrelated"},
rawPoolsCfg: "{}",
tags: []string{"tag:onea", "tag:four", "tag:unrelated"},
wantAppsByDomain: map[dnsname.FQDN][]string{
"1.a.example.com.": {"one"},
"1.b.example.com.": {"one", "three"},
@@ -538,11 +567,12 @@ func TestConfigFromNodeView(t *testing.T) {
},
{
name: "eligible-connector-no-matching-tag-no-self-domains",
cfg: []appctype.Conn25Attr{
appCfg: []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"},
rawPoolsCfg: "{}",
tags: []string{"tag:unrelated"},
wantAppsByDomain: map[dnsname.FQDN][]string{
"a.example.com.": {"one"},
"b.example.com.": {"two"},
@@ -550,11 +580,12 @@ func TestConfigFromNodeView(t *testing.T) {
wantAppsByWCDomain: map[dnsname.FQDN][]string{}},
{
name: "wildcard-collapse-and-deduplication",
cfg: []appctype.Conn25Attr{
appCfg: []appctype.Conn25Attr{
{Name: "one", Domains: []string{"*.example.com", "example.com"}, Connectors: []string{"tag:one"}},
{Name: "two", Domains: []string{"example.com", "sub.example.com"}, Connectors: []string{"tag:two"}},
},
tags: []string{"tag:one", "tag:two"},
rawPoolsCfg: "{}",
tags: []string{"tag:one", "tag:two"},
wantAppsByDomain: map[dnsname.FQDN][]string{
"example.com.": {"one", "two"},
"sub.example.com.": {"two"},
@@ -569,10 +600,11 @@ func TestConfigFromNodeView(t *testing.T) {
// domain and the app name must appear exactly once in appNamesByDomain,
// not once per case variant.
name: "case-variant-exact-domains-deduplicated-within-app",
cfg: []appctype.Conn25Attr{
appCfg: []appctype.Conn25Attr{
{Name: "one", Domains: []string{"EXAMPLE.com", "example.COM", "Example.COM"}, Connectors: []string{"tag:one"}},
},
tags: []string{"tag:one"},
rawPoolsCfg: "{}",
tags: []string{"tag:one"},
wantAppsByDomain: map[dnsname.FQDN][]string{
"example.com.": {"one"},
},
@@ -583,29 +615,39 @@ func TestConfigFromNodeView(t *testing.T) {
// Same as above but for wildcard domains: *.EXAMPLE.com and *.example.COM
// must collapse to a single entry in appNamesByWCDomain.
name: "case-variant-wildcard-domains-deduplicated-within-app",
cfg: []appctype.Conn25Attr{
appCfg: []appctype.Conn25Attr{
{Name: "one", Domains: []string{"*.EXAMPLE.com", "*.example.COM"}, Connectors: []string{"tag:one"}},
},
rawPoolsCfg: "{}",
tags: []string{"tag:one"},
wantAppsByDomain: map[dnsname.FQDN][]string{},
wantAppsByWCDomain: map[dnsname.FQDN][]string{"example.com.": {"one"}},
wantSelfAppNames: set.SetOf([]string{"one"}),
},
{
name: "too-many-pools",
appCfg: []appctype.Conn25Attr{
{Name: "one", Domains: []string{"a.example.com"}, Connectors: []string{"tag:one"}},
},
poolsCfg: []appctype.Conn25PoolsAttr{
{},
{},
},
wantErr: true,
},
} {
t.Run(tt.name, func(t *testing.T) {
cfg := []tailcfg.RawMessage{tailcfg.RawMessage(tt.rawCfg)}
if tt.cfg != nil {
cfg = []tailcfg.RawMessage{}
for _, attr := range tt.cfg {
bs, err := json.Marshal(attr)
if err != nil {
t.Fatalf("unexpected error in test setup: %v", err)
}
cfg = append(cfg, tailcfg.RawMessage(bs))
}
appCfg := []tailcfg.RawMessage{tailcfg.RawMessage(tt.rawAppCfg)}
if tt.appCfg != nil {
appCfg = getRawMessages(t, tt.appCfg)
}
poolsCfg := []tailcfg.RawMessage{tailcfg.RawMessage(tt.rawPoolsCfg)}
if tt.poolsCfg != nil {
poolsCfg = getRawMessages(t, tt.poolsCfg)
}
capMap := tailcfg.NodeCapMap{
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): cfg,
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): appCfg,
tailcfg.NodeCapability(AppConnectorsExperimentalIPPoolsAttrName): poolsCfg,
}
sn := (&tailcfg.Node{
CapMap: capMap,
@@ -639,6 +681,7 @@ func TestGetAppsForDomainName(t *testing.T) {
{Name: "four", Domains: []string{"a.sub.example.com"}, Connectors: []string{"tag:four"}},
{Name: "self-routed", Domains: []string{"*.wildcard.com", "exact-match.com"}, Connectors: []string{"tag:self-routed"}},
},
appctype.Conn25PoolsAttr{},
[]string{"tag:self-routed"},
)
@@ -732,7 +775,7 @@ func TestGetAppsForDomainName(t *testing.T) {
}
}
func makeSelfNode(t *testing.T, attrs []appctype.Conn25Attr, tags []string) tailcfg.NodeView {
func makeSelfNode(t *testing.T, attrs []appctype.Conn25Attr, pools appctype.Conn25PoolsAttr, tags []string) tailcfg.NodeView {
t.Helper()
cfg := make([]tailcfg.RawMessage, 0, len(attrs))
for i, attr := range attrs {
@@ -742,8 +785,13 @@ func makeSelfNode(t *testing.T, attrs []appctype.Conn25Attr, tags []string) tail
}
cfg = append(cfg, tailcfg.RawMessage(bs))
}
poolsBytes, err := json.Marshal(pools)
if err != nil {
t.Fatalf("unexpected error marshaling pools in test setup: %v", err)
}
capMap := tailcfg.NodeCapMap{
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): cfg,
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): cfg,
tailcfg.NodeCapability(AppConnectorsExperimentalIPPoolsAttrName): {tailcfg.RawMessage(poolsBytes)},
}
return (&tailcfg.Node{
@@ -1095,14 +1143,15 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
dnsResp = makeV6DNSResponse(t, tt.domain, tt.v6Addrs)
}
sn := makeSelfNode(t, []appctype.Conn25Attr{{
Name: "app1",
Connectors: []string{"tag:woo"},
Domains: tt.appDomains,
Name: "app1",
Connectors: []string{"tag:woo"},
Domains: tt.appDomains,
}}, appctype.Conn25PoolsAttr{
V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10"), v4RangeFrom("20", "30")},
V6MagicIPPool: []netipx.IPRange{v6RangeFrom("0", "10"), v6RangeFrom("20", "30")},
V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")},
V6TransitIPPool: []netipx.IPRange{v6RangeFrom("40", "50")},
}}, tt.selfTags)
}, tt.selfTags)
c := newConn25(logger.Discard)
cfg := mustConfig(t, sn)
@@ -1128,14 +1177,15 @@ func TestMapDNSResponseSetsExpiryBasedOnTTL(t *testing.T) {
domainName := configuredDomain + "."
dnsMessageName := dnsmessage.MustNewName(domainName)
sn := makeSelfNode(t, []appctype.Conn25Attr{{
Name: "app1",
Connectors: []string{"tag:woo"},
Domains: []string{configuredDomain},
Name: "app1",
Connectors: []string{"tag:woo"},
Domains: []string{configuredDomain},
}}, appctype.Conn25PoolsAttr{
V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10")},
V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")},
V6MagicIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("2606:4700::6812:100"), netip.MustParseAddr("2606:4700::6812:1ff"))},
V6TransitIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("2606:4700::6813:100"), netip.MustParseAddr("2606:4700::6813:1ff"))},
}}, nil)
}, nil)
c := newConn25(logger.Discard)
clock := tstest.NewClock(tstest.ClockOpts{Start: time.Now()})
@@ -1410,7 +1460,7 @@ func TestAddressAssignmentIsHandled(t *testing.T) {
Name: "app1",
Connectors: []string{"tag:woo"},
Domains: []string{"example.com"},
}}, []string{})
}}, appctype.Conn25PoolsAttr{}, []string{})
cfg := mustConfig(t, sn)
ext.conn25.reconfig(cfg)
@@ -1484,14 +1534,15 @@ func TestMapDNSResponseRewritesResponses(t *testing.T) {
domainName := configuredDomain + "."
dnsMessageName := dnsmessage.MustNewName(domainName)
sn := makeSelfNode(t, []appctype.Conn25Attr{{
Name: "app1",
Connectors: []string{"tag:connector"},
Domains: []string{configuredDomain},
Name: "app1",
Connectors: []string{"tag:connector"},
Domains: []string{configuredDomain},
}}, appctype.Conn25PoolsAttr{
V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10")},
V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")},
V6MagicIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("2606:4700::6812:100"), netip.MustParseAddr("2606:4700::6812:1ff"))},
V6TransitIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("2606:4700::6813:100"), netip.MustParseAddr("2606:4700::6813:1ff"))},
}}, []string{})
}, []string{})
cfg := mustConfig(t, sn)
@@ -2011,7 +2062,7 @@ func TestHandleAddressAssignmentStoresTransitIPs(t *testing.T) {
Connectors: []string{"tag:hoo"},
Domains: []string{"hoo.example.com"},
},
}, []string{})
}, appctype.Conn25PoolsAttr{}, []string{})
cfg := mustConfig(t, sn)
ext.conn25.reconfig(cfg)
@@ -2187,10 +2238,10 @@ func TestTransitIPConnMapping(t *testing.T) {
}
func TestClientTransitIPForMagicIP(t *testing.T) {
sn := makeSelfNode(t, []appctype.Conn25Attr{{
sn := makeSelfNode(t, []appctype.Conn25Attr{{Name: "app1"}}, appctype.Conn25PoolsAttr{
V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10")}, // 100.64.0.0 - 100.64.0.10
V6MagicIPPool: []netipx.IPRange{v6RangeFrom("0", "10")},
}}, []string{})
}, []string{})
cfg := mustConfig(t, sn)
mappedMip := netip.MustParseAddr("100.64.0.0")
@@ -2278,9 +2329,9 @@ func TestClientTransitIPForMagicIP(t *testing.T) {
}
func TestConnectorRealIPForTransitIPConnection(t *testing.T) {
sn := makeSelfNode(t, []appctype.Conn25Attr{{
sn := makeSelfNode(t, []appctype.Conn25Attr{{Name: "app1"}}, appctype.Conn25PoolsAttr{
V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")}, // 100.64.0.40 - 100.64.0.50
}}, []string{})
}, []string{})
cfg := mustConfig(t, sn)
mappedSrc := netip.MustParseAddr("100.0.0.1")
@@ -2421,12 +2472,13 @@ func TestConnectorPacketFilterAllow(t *testing.T) {
func TestGetMagicRange(t *testing.T) {
sn := makeSelfNode(t, []appctype.Conn25Attr{{
Name: "app1",
Connectors: []string{"tag:woo"},
Domains: []string{"example.com"},
Name: "app1",
Connectors: []string{"tag:woo"},
Domains: []string{"example.com"},
}}, appctype.Conn25PoolsAttr{
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"))},
}}, []string{})
}, []string{})
cfg := mustConfig(t, sn)
c := newConn25(t.Logf)
c.reconfig(cfg)
@@ -2479,14 +2531,15 @@ func TestReconfigDoesNotReissueInUseAddresses(t *testing.T) {
afterRangeV6 := mustRange("::4", "::7")
makeNodeFromMagicRange := func(v4, v6 netipx.IPRange) tailcfg.NodeView {
return makeSelfNode(t, []appctype.Conn25Attr{{
Name: appName,
Connectors: []string{"tag:woo"},
Domains: []string{"example.com"},
Name: appName,
Connectors: []string{"tag:woo"},
Domains: []string{"example.com"},
}}, appctype.Conn25PoolsAttr{
V4MagicIPPool: []netipx.IPRange{v4},
V6MagicIPPool: []netipx.IPRange{v6},
V4TransitIPPool: []netipx.IPRange{mustRange("169.254.0.0", "169.254.0.10")},
V6TransitIPPool: []netipx.IPRange{mustRange("fd7a:115c:a1e0:a99c:0200::", "fd7a:115c:a1e0:a99c:0200::10")},
}}, []string{})
}, []string{})
}
domain := must.Get(dnsname.ToFQDN("example.com."))
+4 -1
View File
@@ -104,7 +104,10 @@ type Conn25Attr struct {
// Connectors enumerates the app connectors which service these domains.
// These can either be "*" to match any advertising connector, or a
// tag of the form tag:<tag-name>.
Connectors []string `json:"connectors,omitempty"`
Connectors []string `json:"connectors,omitempty"`
}
type Conn25PoolsAttr struct {
V4MagicIPPool []netipx.IPRange `json:"v4MagicIPPool,omitempty"`
V4TransitIPPool []netipx.IPRange `json:"v4TransitIPPool,omitempty"`
V6MagicIPPool []netipx.IPRange `json:"v6MagicIPPool,omitempty"`