diff --git a/ipn/ipnlocal/dnsconfig_test.go b/ipn/ipnlocal/dnsconfig_test.go index 5b0a7e387..96395fc09 100644 --- a/ipn/ipnlocal/dnsconfig_test.go +++ b/ipn/ipnlocal/dnsconfig_test.go @@ -305,6 +305,22 @@ func TestDNSConfigForNetmap(t *testing.T) { Routes: map[dnsname.FQDN][]*dnstype.Resolver{}, }, }, + { + name: "on_demand_domains", + nm: &netmap.NetworkMap{ + DNS: tailcfg.DNSConfig{ + OnDemandDomains: []string{"ts.net"}, + }, + }, + prefs: &ipn.Prefs{ + CorpDNS: true, + }, + want: &dns.Config{ + Hosts: map[dnsname.FQDN][]netip.Addr{}, + Routes: map[dnsname.FQDN][]*dnstype.Resolver{}, + OnDemandDomains: []string{"ts.net"}, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 4bbd97876..2f8385680 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -2965,8 +2965,9 @@ func shouldUseOneCGNATRoute(nm *netmap.NetworkMap, logf logger.Logf, versionOS s // a runtime.GOOS. func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.Logf, versionOS string) *dns.Config { dcfg := &dns.Config{ - Routes: map[dnsname.FQDN][]*dnstype.Resolver{}, - Hosts: map[dnsname.FQDN][]netip.Addr{}, + Routes: map[dnsname.FQDN][]*dnstype.Resolver{}, + Hosts: map[dnsname.FQDN][]netip.Addr{}, + OnDemandDomains: append([]string(nil), nm.DNS.OnDemandDomains...), } // selfV6Only is whether we only have IPv6 addresses ourselves. diff --git a/net/dns/config.go b/net/dns/config.go index 9c55f6d73..f70b137d0 100644 --- a/net/dns/config.go +++ b/net/dns/config.go @@ -44,6 +44,14 @@ type Config struct { // OnlyIPv6, if true, uses the IPv6 service IP (for MagicDNS) // instead of the IPv4 version (100.100.100.100). OnlyIPv6 bool + // OnDemandDomains are the set of domain names for which the OS + // should enable the tailscale client, if it is not already running. + // + // This is plumbed through to the onDemand rules of + // NETunnelProviderManager on macOS/iOS. + // + // The typical OnDemandDomains is ["ts.net"]. + OnDemandDomains []string `json:",omitempty"` } func (c *Config) serviceIP() netip.Addr { diff --git a/net/dns/osconfig.go b/net/dns/osconfig.go index b12e6418b..aef357eb9 100644 --- a/net/dns/osconfig.go +++ b/net/dns/osconfig.go @@ -63,10 +63,18 @@ type OSConfig struct { // from the OS, which will only work with OSConfigurators that // report SupportsSplitDNS()=true. MatchDomains []dnsname.FQDN + // OnDemandDomains are the set of domain names for which the OS + // should enable the tailscale client, if it is not already running. + // + // This is plumbed through to the onDemand rules of + // NETunnelProviderManager on macOS/iOS. + // + // The typical OnDemandDomains is ["ts.net"]. + OnDemandDomains []string } func (o OSConfig) IsZero() bool { - return len(o.Nameservers) == 0 && len(o.SearchDomains) == 0 && len(o.MatchDomains) == 0 + return len(o.Nameservers) == 0 && len(o.SearchDomains) == 0 && len(o.MatchDomains) == 0 && len(o.OnDemandDomains) == 0 } func (a OSConfig) Equal(b OSConfig) bool { @@ -95,6 +103,11 @@ func (a OSConfig) Equal(b OSConfig) bool { return false } } + for i := range a.OnDemandDomains { + if a.OnDemandDomains[i] != b.OnDemandDomains[i] { + return false + } + } return true } @@ -126,6 +139,13 @@ func (a OSConfig) Format(f fmt.State, verb rune) { } fmt.Fprintf(w, "%+v", domain) } + w.WriteString(`] OnDemandDomains:[`) + for i, domain := range a.OnDemandDomains { + if i != 0 { + w.WriteString(" ") + } + fmt.Fprintf(w, "%+v", domain) + } w.WriteString(`] Hosts:[`) for i, host := range a.Hosts { if i != 0 { diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 9b8905647..45e72c1cb 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -1190,6 +1190,15 @@ type DNSConfig struct { // // Matches are case insensitive. ExitNodeFilteredSet []string + + // OnDemandDomains are the set of domain names for which the OS + // should enable the tailscale client, if it is not already running. + // + // This is plumbed through to the onDemand rules of + // NETunnelProviderManager on macOS/iOS. + // + // The typical OnDemandDomains is ["ts.net"]. + OnDemandDomains []string `json:",omitempty"` } // DNSRecord is an extra DNS record to add to MagicDNS. diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index 689b57771..dada5348d 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -234,6 +234,7 @@ func (src *DNSConfig) Clone() *DNSConfig { dst.CertDomains = append(src.CertDomains[:0:0], src.CertDomains...) dst.ExtraRecords = append(src.ExtraRecords[:0:0], src.ExtraRecords...) dst.ExitNodeFilteredSet = append(src.ExitNodeFilteredSet[:0:0], src.ExitNodeFilteredSet...) + dst.OnDemandDomains = append(src.OnDemandDomains[:0:0], src.OnDemandDomains...) return dst } @@ -248,6 +249,7 @@ func (src *DNSConfig) Clone() *DNSConfig { CertDomains []string ExtraRecords []DNSRecord ExitNodeFilteredSet []string + OnDemandDomains []string }{}) // Clone makes a deep copy of RegisterResponse. diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index 463cc3620..50f38d64d 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -533,6 +533,9 @@ func (v DNSConfigView) ExtraRecords() views.Slice[DNSRecord] { return views.Slic func (v DNSConfigView) ExitNodeFilteredSet() views.Slice[string] { return views.SliceOf(v.ж.ExitNodeFilteredSet) } +func (v DNSConfigView) OnDemandDomains() views.Slice[string] { + return views.SliceOf(v.ж.OnDemandDomains) +} // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _DNSConfigViewNeedsRegeneration = DNSConfig(struct { @@ -545,6 +548,7 @@ func (v DNSConfigView) ExitNodeFilteredSet() views.Slice[string] { CertDomains []string ExtraRecords []DNSRecord ExitNodeFilteredSet []string + OnDemandDomains []string }{}) // View returns a readonly view of RegisterResponse.