diff --git a/appc/embedded.go b/appc/embedded.go index 167377431..84192e2c1 100644 --- a/appc/embedded.go +++ b/appc/embedded.go @@ -11,8 +11,10 @@ import ( "strings" "sync" + xmaps "golang.org/x/exp/maps" "golang.org/x/net/dns/dnsmessage" "tailscale.com/types/logger" + "tailscale.com/types/views" ) /* @@ -70,6 +72,15 @@ func (e *EmbeddedAppConnector) UpdateDomains(domains []string) { d = strings.ToLower(d) e.domains[d] = old[d] } + e.logf("handling domains: %v", xmaps.Keys(e.domains)) +} + +// Domains returns the currently configured domain list. +func (e *EmbeddedAppConnector) Domains() views.Slice[string] { + e.mu.Lock() + defer e.mu.Unlock() + + return views.SliceOf(xmaps.Keys(e.domains)) } // ObserveDNSResponse is a callback invoked by the DNS resolver when a DNS diff --git a/appc/embedded_test.go b/appc/embedded_test.go index fbf29f998..7b1687ea6 100644 --- a/appc/embedded_test.go +++ b/appc/embedded_test.go @@ -16,7 +16,7 @@ import ( func TestUpdateDomains(t *testing.T) { a := NewEmbeddedAppConnector(t.Logf, nil) a.UpdateDomains([]string{"example.com"}) - if got, want := xmaps.Keys(a.domains), []string{"example.com"}; !slices.Equal(got, want) { + if got, want := a.Domains().AsSlice(), []string{"example.com"}; !slices.Equal(got, want) { t.Errorf("got %v; want %v", got, want) } diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 92c59b1ff..4c945a67d 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -321,7 +321,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/tstime/mono from tailscale.com/net/tstun+ tailscale.com/tstime/rate from tailscale.com/wgengine/filter+ tailscale.com/tsweb/varz from tailscale.com/cmd/tailscaled - tailscale.com/types/appctype from tailscale.com/appc + tailscale.com/types/appctype from tailscale.com/appc+ tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+ tailscale.com/types/empty from tailscale.com/ipn+ tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 0d46a19c0..17f23e0f1 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -67,6 +67,7 @@ import ( "tailscale.com/tka" "tailscale.com/tsd" "tailscale.com/tstime" + "tailscale.com/types/appctype" "tailscale.com/types/dnstype" "tailscale.com/types/empty" "tailscale.com/types/key" @@ -3233,6 +3234,49 @@ func (b *LocalBackend) blockEngineUpdates(block bool) { b.mu.Unlock() } +// reconfigAppConnectorLocked updates the app connector state based on the +// current network map and preferences. +// b.mu must be held. +func (b *LocalBackend) reconfigAppConnectorLocked(nm *netmap.NetworkMap, prefs ipn.PrefsView) { + const appConnectorCapName = "tailscale.com/app-connectors" + + if !prefs.AppConnector().Advertise { + b.appConnector = nil + return + } + + if b.appConnector == nil { + b.appConnector = appc.NewEmbeddedAppConnector(b.logf, b) + } + if nm == nil { + return + } + + // TODO(raggi): rework the view infrastructure so the large deep clone is no + // longer required + sn := nm.SelfNode.AsStruct() + attrs, err := tailcfg.UnmarshalNodeCapJSON[appctype.AppConnectorAttr](sn.CapMap, appConnectorCapName) + if err != nil { + b.logf("[unexpected] error parsing app connector mapcap: %v", err) + return + } + + var domains []string + for _, attr := range attrs { + // Geometric cost, assumes that the number of advertised tags is small + if !nm.SelfNode.Tags().ContainsFunc(func(tag string) bool { + return slices.Contains(attr.Connectors, tag) + }) { + continue + } + + domains = append(domains, attr.Domains...) + } + slices.Sort(domains) + slices.Compact(domains) + b.appConnector.UpdateDomains(domains) +} + // authReconfig pushes a new configuration into wgengine, if engine // updates are not currently blocked, based on the cached netmap and // user prefs. @@ -3246,9 +3290,7 @@ func (b *LocalBackend) authReconfig() { dohURL, dohURLOK := exitNodeCanProxyDNS(nm, b.peers, prefs.ExitNodeID()) dcfg := dnsConfigForNetmap(nm, b.peers, prefs, b.logf, version.OS()) // If the current node is an app connector, ensure the app connector machine is started - if prefs.AppConnector().Advertise && b.appConnector == nil { - b.appConnector = appc.NewEmbeddedAppConnector(b.logf, b) - } + b.reconfigAppConnectorLocked(nm, prefs) b.mu.Unlock() if blocked { diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 79f58c2c4..1ac51ce58 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -1187,6 +1187,49 @@ func TestObserveDNSResponse(t *testing.T) { } } +func TestReconfigureAppConnector(t *testing.T) { + b := newTestBackend(t) + b.reconfigAppConnectorLocked(b.netMap, b.pm.prefs) + if b.appConnector != nil { + t.Fatal("unexpected app connector") + } + + b.EditPrefs(&ipn.MaskedPrefs{ + Prefs: ipn.Prefs{ + AppConnector: ipn.AppConnectorPrefs{ + Advertise: true, + }, + }, + AppConnectorSet: true, + }) + b.reconfigAppConnectorLocked(b.netMap, b.pm.prefs) + if b.appConnector == nil { + t.Fatal("expected app connector") + } + + appCfg := `{ + "name": "example", + "domains": ["example.com"], + "connectors": ["tag:example"] + }` + + b.netMap.SelfNode = (&tailcfg.Node{ + Name: "example.ts.net", + Tags: []string{"tag:example"}, + CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ + "tailscale.com/app-connectors": {tailcfg.RawMessage(appCfg)}, + }), + }).View() + + b.reconfigAppConnectorLocked(b.netMap, b.pm.prefs) + + want := []string{"example.com"} + if !slices.Equal(b.appConnector.Domains().AsSlice(), want) { + t.Fatalf("got domains %v, want %v", b.appConnector.Domains(), want) + } + +} + func resolversEqual(t *testing.T, a, b []*dnstype.Resolver) bool { if a == nil && b == nil { return true