mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-05 04:06:35 +02:00
wip: register resolver scheme for apps
This commit is contained in:
parent
a3744bf1cf
commit
934907eba9
@ -5,11 +5,13 @@ package appc
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"tailscale.com/ipn/ipnext"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/appctype"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
@ -52,7 +54,12 @@ func PickConnector(nb ipnext.NodeBackend, app appctype.Conn25Attr) []tailcfg.Nod
|
||||
return matches
|
||||
}
|
||||
|
||||
func AppNameByDomain(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.NodeView) map[string]string {
|
||||
// DNSAddrScheme is the custom URI scheme used for conn25-managed split DNS
|
||||
// entries to determine the destination at query time rather than configuration
|
||||
// time.
|
||||
const DNSAddrScheme = "tailscale-app"
|
||||
|
||||
func AppDNSRoutes(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.NodeView) map[string][]*dnstype.Resolver {
|
||||
if !hasCap(AppConnectorsExperimentalAttrName) {
|
||||
return nil
|
||||
}
|
||||
@ -68,5 +75,13 @@ func AppNameByDomain(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.No
|
||||
appNamesByDomain[domain] = app.Name
|
||||
}
|
||||
}
|
||||
return appNamesByDomain
|
||||
|
||||
// TODO: filter out apps that have no peers? There is not enough information
|
||||
// available here.
|
||||
|
||||
m := make(map[string][]*dnstype.Resolver, len(appNamesByDomain))
|
||||
for domain, appName := range appNamesByDomain {
|
||||
m[domain] = []*dnstype.Resolver{{Addr: fmt.Sprintf("%s:%s", DNSAddrScheme, appName)}}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@ import (
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/ipn/ipnext"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tstun"
|
||||
@ -138,6 +137,7 @@ func (e *extension) installHooks(dph *datapathHandler) error {
|
||||
if !ok {
|
||||
return errors.New("could not access system dns manager")
|
||||
}
|
||||
|
||||
tun, ok := e.backend.Sys().Tun.GetOK()
|
||||
if !ok {
|
||||
return errors.New("could not access system tun")
|
||||
@ -152,8 +152,13 @@ func (e *extension) installHooks(dph *datapathHandler) error {
|
||||
return e.conn25.mapDNSResponse(bs)
|
||||
})
|
||||
|
||||
if !resolver.FranNewDynamicResolverThing.IsSet() {
|
||||
resolver.FranNewDynamicResolverThing.Set(func(appName string) (string, error) {
|
||||
if resolver := dnsManager.Resolver(); resolver != nil {
|
||||
if err := resolver.RegisterCustomScheme(appc.DNSAddrScheme, func(addr string) (newAddr string, err error) {
|
||||
scheme, appName, ok := strings.Cut(addr, ":")
|
||||
if !ok || scheme != appc.DNSAddrScheme {
|
||||
return "", fmt.Errorf("unexpected conn25 scheme %q", scheme)
|
||||
}
|
||||
|
||||
if !e.conn25.isConfigured() {
|
||||
return "", errors.New("conn25 not configured")
|
||||
}
|
||||
@ -167,7 +172,9 @@ func (e *extension) installHooks(dph *datapathHandler) error {
|
||||
return "", errors.New("no peer found for app")
|
||||
}
|
||||
return urlBase + "/dns-query", nil
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("could not register DNS resolver scheme: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Intercept packets from the tun device and from WireGuard
|
||||
|
||||
@ -431,7 +431,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{
|
||||
dnsname.FQDN("example.com."): {
|
||||
{Addr: "http://100.102.0.1:1234/dns-query"},
|
||||
{Addr: "tailscale-app:app1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -862,15 +862,10 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
|
||||
// Add split DNS routes, with no regard to exit node configuration.
|
||||
addSplitDNSRoutes(nm.DNS.Routes)
|
||||
|
||||
// Add split DNS routes for conn25
|
||||
appMap := appc.AppNameByDomain(nm.HasCap, nm.SelfNode)
|
||||
if appMap != nil {
|
||||
var m map[string][]*dnstype.Resolver
|
||||
for domain, appName := range appMap {
|
||||
mak.Set(&m, domain, []*dnstype.Resolver{{Addr: appName, IsFranNewDynamicResolverThing: true}})
|
||||
}
|
||||
if m != nil {
|
||||
addSplitDNSRoutes(m)
|
||||
if buildfeatures.HasConn25 {
|
||||
// Add split DNS routes for conn25
|
||||
if appRoutes := appc.AppDNSRoutes(nm.HasCap, nm.SelfNode); appRoutes != nil {
|
||||
addSplitDNSRoutes(appRoutes)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@ -41,15 +42,14 @@ import (
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/cloudenv"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/race"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// TODO comment, name
|
||||
var FranNewDynamicResolverThing feature.Hook[func(string) (string, error)]
|
||||
|
||||
// headerBytes is the number of bytes in a DNS message header.
|
||||
const headerBytes = 12
|
||||
|
||||
@ -326,6 +326,18 @@ type forwarder struct {
|
||||
// /etc/resolv.conf is missing/corrupt, and the peerapi ExitDNS stub
|
||||
// resolver lookup.
|
||||
cloudHostFallback []resolverAndDelay
|
||||
// schemes are the collection of registered URI scheme names that
|
||||
// dynamically decide which resolver to use at the time of each query. The
|
||||
// key is the scheme (the portion before the first `:`) and the value is a
|
||||
// handler that determines where the current query should be sent.
|
||||
// Use schemeCacheLocked() to get the current contents that can continue to
|
||||
// be accessed once mu is released. This allows the (much more common)
|
||||
// resolver code path to avoid repeated locking and unlocking.
|
||||
// When modified, call invalidateSchemeCacheLocked() before unlocking mu.
|
||||
schemes map[string]CustomSchemeHandler
|
||||
// schemeCache is an immutable copy of schemes. Do not read directly,
|
||||
// use schemeCacheLocked() which will regenerate its contents as needed.
|
||||
schemeCache views.Map[string, CustomSchemeHandler]
|
||||
|
||||
// acceptDNS tracks the CorpDNS pref (--accept-dns)
|
||||
// This lets us skip health warnings if the forwarder receives inbound
|
||||
@ -1004,23 +1016,35 @@ func (f *forwarder) resolvers(domain dnsname.FQDN) []resolverAndDelay {
|
||||
f.mu.Lock()
|
||||
routes := f.routes
|
||||
cloudHostFallback := f.cloudHostFallback
|
||||
schemes := f.schemeCacheLocked()
|
||||
f.mu.Unlock()
|
||||
for _, route := range routes {
|
||||
if route.Suffix == "." || route.Suffix.Contains(domain) {
|
||||
triedToResolveResolver := false
|
||||
resolvers := []resolverAndDelay{}
|
||||
resolvers := make([]resolverAndDelay, 0, len(route.Resolvers))
|
||||
for _, r := range route.Resolvers {
|
||||
if r.name.IsFranNewDynamicResolverThing {
|
||||
triedToResolveResolver = true
|
||||
fx := FranNewDynamicResolverThing.Get()
|
||||
if fx != nil {
|
||||
url, err := fx(r.name.Addr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
r.name.Addr = url
|
||||
}
|
||||
scheme, _, ok := strings.Cut(r.name.Addr, ":")
|
||||
if !ok {
|
||||
resolvers = append(resolvers, r)
|
||||
continue
|
||||
}
|
||||
h := schemes.Get(scheme)
|
||||
if h == nil {
|
||||
resolvers = append(resolvers, r)
|
||||
continue
|
||||
}
|
||||
|
||||
triedToResolveResolver = true
|
||||
newAddr, err := h(r.name.Addr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
r.name = &dnstype.Resolver{
|
||||
Addr: newAddr,
|
||||
UseWithExitNode: r.name.UseWithExitNode,
|
||||
}
|
||||
|
||||
resolvers = append(resolvers, r)
|
||||
}
|
||||
// if there turned out to actually be no resolvers for this route Suffix then
|
||||
@ -1047,6 +1071,52 @@ func (f *forwarder) GetUpstreamResolvers(name dnsname.FQDN) []*dnstype.Resolver
|
||||
return upstreamResolvers
|
||||
}
|
||||
|
||||
// RegisterCustomScheme adds a [CustomSchemaHandler] that is called to provide
|
||||
// an updated address when a [dnstype.Resolver.Addr] uses that scheme.
|
||||
func (f *forwarder) RegisterCustomScheme(scheme string, h CustomSchemeHandler) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if _, ok := f.schemes[scheme]; ok {
|
||||
return fmt.Errorf("scheme %q already registered", scheme)
|
||||
}
|
||||
f.invalidateSchemeCacheLocked()
|
||||
mak.Set(&f.schemes, scheme, h)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnregisterCustomScheme removes a scheme previously registered via
|
||||
// RegisterCustomScheme.
|
||||
func (f *forwarder) UnregisterCustomScheme(scheme string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if _, ok := f.schemes[scheme]; !ok {
|
||||
return fmt.Errorf("scheme %q not registered", scheme)
|
||||
}
|
||||
f.invalidateSchemeCacheLocked()
|
||||
delete(f.schemes, scheme)
|
||||
return nil
|
||||
}
|
||||
|
||||
// invalidateSchemeCacheLocked clears f.schemeCache so that it will be rebuilt
|
||||
// on the next call to f.schemeCacheLocked().
|
||||
func (f *forwarder) invalidateSchemeCacheLocked() {
|
||||
f.schemeCache = views.Map[string, CustomSchemeHandler]{}
|
||||
}
|
||||
|
||||
// schemeCacheLocked returns an immutable copy of f.schemes that can be used
|
||||
// after mu is unlocked.
|
||||
func (f *forwarder) schemeCacheLocked() views.Map[string, CustomSchemeHandler] {
|
||||
if !f.schemeCache.IsNil() {
|
||||
return f.schemeCache
|
||||
}
|
||||
if f.schemes == nil {
|
||||
return f.schemeCache // returns a nil view
|
||||
}
|
||||
// Regenerate the cache
|
||||
f.schemeCache = views.MapOf(maps.Clone(f.schemes))
|
||||
return f.schemeCache
|
||||
}
|
||||
|
||||
// forwardQuery is information and state about a forwarded DNS query that's
|
||||
// being sent to 1 or more upstreams.
|
||||
//
|
||||
|
||||
@ -293,6 +293,24 @@ func (r *Resolver) SetConfig(cfg Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CustomSchemeHandler takes a URI (retrieved from [dnstype.Resolver.Addr]) and
|
||||
// returns an updated URI to use for the current query. The result is only valid
|
||||
// for right now and may change over time.
|
||||
type CustomSchemeHandler func(addr string) (newAddr string, err error)
|
||||
|
||||
// RegisterCustomScheme adds a [CustomSchemaHandler] that is called to provide
|
||||
// an updated address to the forwarder when a [dnstype.Resolver.Addr] uses that
|
||||
// scheme.
|
||||
func (r *Resolver) RegisterCustomScheme(scheme string, h CustomSchemeHandler) error {
|
||||
return r.forwarder.RegisterCustomScheme(scheme, h)
|
||||
}
|
||||
|
||||
// UnregisterCustomScheme unregisters a scheme previously registered with
|
||||
// RegisterCustomScheme.
|
||||
func (r *Resolver) UnregisterCustomScheme(scheme string) error {
|
||||
return r.forwarder.UnregisterCustomScheme(scheme)
|
||||
}
|
||||
|
||||
// Close shuts down the resolver and ensures poll goroutines have exited.
|
||||
// The Resolver cannot be used again after Close is called.
|
||||
func (r *Resolver) Close() {
|
||||
|
||||
@ -41,8 +41,6 @@ type Resolver struct {
|
||||
// there are situations where it is preferable to still use a Split DNS server and/or
|
||||
// global DNS server instead of the exit node.
|
||||
UseWithExitNode bool `json:",omitempty"`
|
||||
|
||||
IsFranNewDynamicResolverThing bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// IPPort returns r.Addr as an IP address and port if either
|
||||
|
||||
@ -23,10 +23,9 @@ func (src *Resolver) Clone() *Resolver {
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _ResolverCloneNeedsRegeneration = Resolver(struct {
|
||||
Addr string
|
||||
BootstrapResolution []netip.Addr
|
||||
UseWithExitNode bool
|
||||
IsFranNewDynamicResolverThing bool
|
||||
Addr string
|
||||
BootstrapResolution []netip.Addr
|
||||
UseWithExitNode bool
|
||||
}{})
|
||||
|
||||
// Clone duplicates src into dst and reports whether it succeeded.
|
||||
|
||||
@ -113,14 +113,12 @@ func (v ResolverView) BootstrapResolution() views.Slice[netip.Addr] {
|
||||
// exit node is in use. Normally, DNS resolution is delegated to the exit node but
|
||||
// there are situations where it is preferable to still use a Split DNS server and/or
|
||||
// global DNS server instead of the exit node.
|
||||
func (v ResolverView) UseWithExitNode() bool { return v.ж.UseWithExitNode }
|
||||
func (v ResolverView) IsFranNewDynamicResolverThing() bool { return v.ж.IsFranNewDynamicResolverThing }
|
||||
func (v ResolverView) Equal(v2 ResolverView) bool { return v.ж.Equal(v2.ж) }
|
||||
func (v ResolverView) UseWithExitNode() bool { return v.ж.UseWithExitNode }
|
||||
func (v ResolverView) Equal(v2 ResolverView) bool { return v.ж.Equal(v2.ж) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _ResolverViewNeedsRegeneration = Resolver(struct {
|
||||
Addr string
|
||||
BootstrapResolution []netip.Addr
|
||||
UseWithExitNode bool
|
||||
IsFranNewDynamicResolverThing bool
|
||||
Addr string
|
||||
BootstrapResolution []netip.Addr
|
||||
UseWithExitNode bool
|
||||
}{})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user