@@ -10,6 +10,7 @@ import (
1010 "os"
1111 "runtime"
1212 "strings"
13+ "sync"
1314 "time"
1415
1516 "github.com/anywherelan/ts-dns/control/controlknobs"
@@ -161,7 +162,7 @@ func (a *Application) Init(ctx context.Context, tunDevice tun.Device) error {
161162 }, a .Eventbus , new (awlevent.KnownPeerChanged ))
162163 }
163164
164- a .VPNGateway = service .NewVPNGateway (a .Conf , a .Tunnel , a .vpnDevice , a .P2p , a .SockMarker , a .DisableGatewayOSSetup )
165+ a .VPNGateway = service .NewVPNGateway (a .Conf , a .Tunnel , a .vpnDevice , a .P2p , a .SockMarker , a .Dns , a . DisableGatewayOSSetup )
165166
166167 handler := api .NewHandler (a .Conf , a .P2p , a .AuthStatus , a .Tunnel , a .SOCKS5 , a .LogBuffer , a .Dns , a .VPNGateway )
167168 a .Api = handler
@@ -353,33 +354,60 @@ type DNSService struct {
353354 ctx context.Context
354355 logger * log.ZapEventLogger
355356
357+ mu sync.Mutex
358+ dnsHost string
359+ dnsFQDN dnsname.FQDN
356360 dnsOsConfigurator dns.OSConfigurator
357361 dnsResolver * awldns.Resolver
358362 upstreamDNS string
359363 isAwlDNSSetAsSystem bool
364+ // forceUpstream forces the awl resolver to capture all queries
365+ // (MatchDomains=nil) and forward them to the configured public upstream so
366+ // DNS traverses the tunnel instead of leaking to the system resolver. Set
367+ // in VPN gateway client mode.
368+ forceUpstream bool
360369}
361370
362371func NewDNSService (conf * config.Config , eventbus awlevent.Bus , ctx context.Context , logger * log.ZapEventLogger ) * DNSService {
363372 return & DNSService {conf : conf , eventbus : eventbus , ctx : ctx , logger : logger }
364373}
365374
366375func (a * DNSService ) initDNS (interfaceName string ) {
367- var err error
376+ a .mu .Lock ()
377+ defer a .mu .Unlock ()
378+
368379 dnsAddr := a .conf .DNS .ListenAddress
369380 dnsHost , _ , err := net .SplitHostPort (dnsAddr )
370381 if err != nil {
371382 a .logger .Errorf ("invalid dns listen address %s: %v" , dnsAddr , err )
372383 return
373384 }
385+ a .dnsHost = dnsHost
374386
387+ fqdn , err := dnsname .ToFQDN (awldns .LocalDomain )
388+ if err != nil {
389+ panic (err )
390+ }
391+ a .dnsFQDN = fqdn
392+
393+ // TODO(android awldns): on Android this NewResolver cannot bind :53 (needs
394+ // root) and dnsOsConfigurator.SetDNS below fails (no writable resolv.conf),
395+ // so awldns is effectively inert there and .awl names do not resolve. The
396+ // Android host instead points VpnService at DNS.UpstreamDNSAddress directly
397+ // (see awl-flutter MainActivity.establishTun), which prevents leaks but
398+ // gives no .awl resolution. A full fix would intercept :53 to a magic awl
399+ // IP inside the tunnel read-path (userspace netstack),
400+ // rather than binding an OS socket.
375401 a .dnsResolver = awldns .NewResolver (dnsAddr )
376- a .upstreamDNS = awldns .DefaultUpstreamDNSAddress
377- a .refreshDNSConfig ()
402+ a .upstreamDNS = a .conf .DNS .UpstreamDNSAddress
403+ a .forceUpstream = a .conf .VPNGateway .ClientEnabled
404+ a .refreshDNSConfigLocked ()
378405
379406 awlevent .WrapSubscriptionToCallback (a .ctx , func (_ interface {}) {
380- a .refreshDNSConfig ()
407+ a .mu .Lock ()
408+ defer a .mu .Unlock ()
409+ a .refreshDNSConfigLocked ()
381410 }, a .eventbus , new (awlevent.KnownPeerChanged ))
382- defer a .refreshDNSConfig ()
383411
384412 tsLogger := log .Logger ("ts/dnsconf" )
385413 a .dnsOsConfigurator , err = dns .NewOSConfigurator (func (format string , args ... interface {}) {
@@ -390,52 +418,110 @@ func (a *DNSService) initDNS(interfaceName string) {
390418 return
391419 }
392420
393- fqdn , err := dnsname .ToFQDN (awldns .LocalDomain )
394- if err != nil {
395- panic (err )
396- }
397- newOSConfig := dns.OSConfig {
398- Nameservers : []netip.Addr {netip .MustParseAddr (dnsHost )},
399- MatchDomains : []dnsname.FQDN {fqdn },
400- }
421+ a .applyOSDNSConfigLocked ()
422+ }
423+
424+ // applyOSDNSConfigLocked (re)computes the OS DNS takeover config from the
425+ // current state (split-DNS support, base config, forceUpstream) and pushes it
426+ // to the OS, then refreshes the awl resolver. Caller must hold a.mu and have a
427+ // non-nil dnsOsConfigurator. Safe to call repeatedly.
428+ func (a * DNSService ) applyOSDNSConfigLocked () {
429+ supportsSplitDNS := a .dnsOsConfigurator .SupportsSplitDNS ()
401430
402- if ! a . dnsOsConfigurator . SupportsSplitDNS () {
403- newOSConfig . MatchDomains = nil
431+ var baseNameservers []netip. Addr
432+ if ! supportsSplitDNS {
404433 baseOSConfig , err := a .dnsOsConfigurator .GetBaseConfig ()
405434 if err != nil {
406435 a .logger .Errorf ("get base config from os configurator, abort setting os dns: %v" , err )
407436 return
408437 }
409-
410438 a .logger .Infof ("os does not support split dns. base config: %v" , baseOSConfig )
411- if len (baseOSConfig .Nameservers ) == 0 {
412- a .logger .Errorf ("got zero nameservers from os configurator, use %s as default" , awldns .DefaultUpstreamDNSAddress )
413- a .upstreamDNS = awldns .DefaultUpstreamDNSAddress
414- } else {
415- // TODO: use all nameservers in awldns resolver proxy
416- a .upstreamDNS = net .JoinHostPort (baseOSConfig .Nameservers [0 ].String (), awldns .DefaultDNSPort )
417- }
439+ baseNameservers = baseOSConfig .Nameservers
440+ }
441+
442+ matchDomains , upstream := chooseDNSPolicy (a .forceUpstream , supportsSplitDNS , baseNameservers , a .conf .DNS .UpstreamDNSAddress )
443+ a .upstreamDNS = upstream
444+ a .refreshDNSConfigLocked ()
445+
446+ // TODO: consider setting SearchDomains = ["awl."] so peers resolve by bare
447+ // short name (e.g. "mypeer" -> "mypeer.awl") instead of requiring the full
448+ // .awl suffix. SearchDomains expands single-label queries into FQDNs and is
449+ // additive to the OS's existing search list (distinct from MatchDomains,
450+ // which only routes which zones reach the awl resolver). Would likely want a
451+ // config toggle to gate it.
452+ //
453+ // TODO: consider pushing admin.awl into Hosts (a static FQDN->IP map applied
454+ // to /etc/hosts) instead of (or in addition to) injecting
455+ // AdminHttpServerDomainName into the resolver name mapping in
456+ // refreshDNSConfigLocked — that would make the admin UI name resolvable even
457+ // when the awl :53 resolver itself is not reachable.
458+ newOSConfig := dns.OSConfig {
459+ Nameservers : []netip.Addr {netip .MustParseAddr (a .dnsHost )},
460+ MatchDomains : matchDomains ,
418461 }
419-
420- // In VPN gateway client mode, force upstream DNS to a public resolver to
421- // prevent DNS leaks. All traffic (including DNS) routes through the TUN
422- // and the gateway peer. This must come after the split-DNS logic so it
423- // takes final precedence.
424- if a .conf .VPNGateway .ClientEnabled {
425- a .upstreamDNS = awldns .DefaultUpstreamDNSAddress
426- a .logger .Infof ("VPN gateway client mode: using public upstream DNS %s to prevent DNS leaks" , a .upstreamDNS )
462+ if err := a .dnsOsConfigurator .SetDNS (newOSConfig ); err != nil {
463+ a .logger .Errorf ("set dns config to os configurator: %v" , err )
464+ return
427465 }
466+ a .logger .Infof ("successfully set dns config to os (forceUpstream=%v, upstream=%s, matchDomains=%v)" ,
467+ a .forceUpstream , a .upstreamDNS , matchDomains )
468+ a .isAwlDNSSetAsSystem = true
469+ }
428470
429- err = a .dnsOsConfigurator .SetDNS (newOSConfig )
471+ // chooseDNSPolicy decides which domains the awl resolver should capture and
472+ // which upstream it forwards non-.awl queries to.
473+ //
474+ // - forceUpstream (VPN gateway client mode): capture everything
475+ // (MatchDomains=nil) and forward to the configured public upstream so DNS
476+ // goes through the tunnel — no leak.
477+ // - split-DNS supported: capture only .awl; other queries are handled by the
478+ // OS resolver directly, so the awl upstream is unused (kept as the
479+ // configured default for completeness).
480+ // - split-DNS unsupported: capture everything and forward to the system's
481+ // first base nameserver, falling back to the configured default when the OS
482+ // reports none.
483+ func chooseDNSPolicy (forceUpstream , supportsSplitDNS bool , base []netip.Addr , upstreamCfg string ) (matchDomains []dnsname.FQDN , upstream string ) {
484+ awlFQDN , err := dnsname .ToFQDN (awldns .LocalDomain )
430485 if err != nil {
431- a .logger .Errorf ("set dns config to os configurator: %v" , err )
432- } else {
433- a .logger .Info ("successfully set dns config to os" )
434- a .isAwlDNSSetAsSystem = true
486+ panic (err )
487+ }
488+
489+ if forceUpstream {
490+ return nil , upstreamCfg
435491 }
492+ if supportsSplitDNS {
493+ return []dnsname.FQDN {awlFQDN }, upstreamCfg
494+ }
495+ // no split DNS: capture everything, forward to the system's base resolver
496+ if len (base ) == 0 {
497+ return nil , upstreamCfg
498+ }
499+ // TODO: use all nameservers in awldns resolver proxy
500+ return nil , net .JoinHostPort (base [0 ].String (), awldns .DefaultDNSPort )
501+ }
502+
503+ // ForceUpstreamDNS toggles full-capture mode where the awl resolver intercepts
504+ // all DNS (not just .awl) and forwards it to the configured public upstream, so
505+ // queries traverse the tunnel and do not leak. Driven by the VPN gateway client
506+ // apply/teardown. No-op (returns nil) when DNS was never set up as the system
507+ // resolver (DNS disabled, or Android where the OS DNS takeover does not apply).
508+ // Idempotent.
509+ func (a * DNSService ) ForceUpstreamDNS (enabled bool ) error {
510+ a .mu .Lock ()
511+ defer a .mu .Unlock ()
512+
513+ if a .dnsOsConfigurator == nil || ! a .isAwlDNSSetAsSystem {
514+ return nil
515+ }
516+ if a .forceUpstream == enabled {
517+ return nil
518+ }
519+ a .forceUpstream = enabled
520+ a .applyOSDNSConfigLocked ()
521+ return nil
436522}
437523
438- func (a * DNSService ) refreshDNSConfig () {
524+ func (a * DNSService ) refreshDNSConfigLocked () {
439525 if a .dnsResolver == nil {
440526 a .logger .DPanicf ("called refreshDNSConfig with nil resolver %v" , a .dnsResolver )
441527 return
@@ -446,6 +532,8 @@ func (a *DNSService) refreshDNSConfig() {
446532}
447533
448534func (a * DNSService ) Close () {
535+ a .mu .Lock ()
536+ defer a .mu .Unlock ()
449537 if a .dnsOsConfigurator != nil {
450538 err := a .dnsOsConfigurator .Close ()
451539 if err != nil {
@@ -458,13 +546,17 @@ func (a *DNSService) Close() {
458546}
459547
460548func (a * DNSService ) AwlDNSAddress () string {
549+ a .mu .Lock ()
550+ defer a .mu .Unlock ()
461551 if a .dnsResolver != nil {
462552 return a .dnsResolver .DNSAddress ()
463553 }
464554 return ""
465555}
466556
467557func (a * DNSService ) IsAwlDNSSetAsSystem () bool {
558+ a .mu .Lock ()
559+ defer a .mu .Unlock ()
468560 return a .isAwlDNSSetAsSystem
469561}
470562
0 commit comments