@@ -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,98 @@ 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
418440 }
419441
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 )
442+ matchDomains , upstream := chooseDNSPolicy ( a . forceUpstream , supportsSplitDNS , baseNameservers , a . conf . DNS . UpstreamDNSAddress )
443+ a . upstreamDNS = upstream
444+ a . refreshDNSConfigLocked ()
445+
446+ newOSConfig := dns. OSConfig {
447+ Nameservers : []netip. Addr { netip . MustParseAddr ( a . dnsHost )},
448+ MatchDomains : matchDomains ,
427449 }
450+ if err := a .dnsOsConfigurator .SetDNS (newOSConfig ); err != nil {
451+ a .logger .Errorf ("set dns config to os configurator: %v" , err )
452+ return
453+ }
454+ a .logger .Infof ("successfully set dns config to os (forceUpstream=%v, upstream=%s, matchDomains=%v)" ,
455+ a .forceUpstream , a .upstreamDNS , matchDomains )
456+ a .isAwlDNSSetAsSystem = true
457+ }
428458
429- err = a .dnsOsConfigurator .SetDNS (newOSConfig )
459+ // chooseDNSPolicy decides which domains the awl resolver should capture and
460+ // which upstream it forwards non-.awl queries to.
461+ //
462+ // - forceUpstream (VPN gateway client mode): capture everything
463+ // (MatchDomains=nil) and forward to the configured public upstream so DNS
464+ // goes through the tunnel — no leak.
465+ // - split-DNS supported: capture only .awl; other queries are handled by the
466+ // OS resolver directly, so the awl upstream is unused (kept as the
467+ // configured default for completeness).
468+ // - split-DNS unsupported: capture everything and forward to the system's
469+ // first base nameserver, falling back to the configured default when the OS
470+ // reports none.
471+ func chooseDNSPolicy (forceUpstream , supportsSplitDNS bool , base []netip.Addr , upstreamCfg string ) (matchDomains []dnsname.FQDN , upstream string ) {
472+ awlFQDN , err := dnsname .ToFQDN (awldns .LocalDomain )
430473 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
474+ panic (err )
475+ }
476+
477+ if forceUpstream {
478+ return nil , upstreamCfg
479+ }
480+ if supportsSplitDNS {
481+ return []dnsname.FQDN {awlFQDN }, upstreamCfg
482+ }
483+ // no split DNS: capture everything, forward to the system's base resolver
484+ if len (base ) == 0 {
485+ return nil , upstreamCfg
435486 }
487+ // TODO: use all nameservers in awldns resolver proxy
488+ return nil , net .JoinHostPort (base [0 ].String (), awldns .DefaultDNSPort )
489+ }
490+
491+ // ForceUpstreamDNS toggles full-capture mode where the awl resolver intercepts
492+ // all DNS (not just .awl) and forwards it to the configured public upstream, so
493+ // queries traverse the tunnel and do not leak. Driven by the VPN gateway client
494+ // apply/teardown. No-op (returns nil) when DNS was never set up as the system
495+ // resolver (DNS disabled, or Android where the OS DNS takeover does not apply).
496+ // Idempotent.
497+ func (a * DNSService ) ForceUpstreamDNS (enabled bool ) error {
498+ a .mu .Lock ()
499+ defer a .mu .Unlock ()
500+
501+ if a .dnsOsConfigurator == nil || ! a .isAwlDNSSetAsSystem {
502+ return nil
503+ }
504+ if a .forceUpstream == enabled {
505+ return nil
506+ }
507+ a .forceUpstream = enabled
508+ a .applyOSDNSConfigLocked ()
509+ return nil
436510}
437511
438- func (a * DNSService ) refreshDNSConfig () {
512+ func (a * DNSService ) refreshDNSConfigLocked () {
439513 if a .dnsResolver == nil {
440514 a .logger .DPanicf ("called refreshDNSConfig with nil resolver %v" , a .dnsResolver )
441515 return
@@ -446,6 +520,8 @@ func (a *DNSService) refreshDNSConfig() {
446520}
447521
448522func (a * DNSService ) Close () {
523+ a .mu .Lock ()
524+ defer a .mu .Unlock ()
449525 if a .dnsOsConfigurator != nil {
450526 err := a .dnsOsConfigurator .Close ()
451527 if err != nil {
@@ -458,13 +534,17 @@ func (a *DNSService) Close() {
458534}
459535
460536func (a * DNSService ) AwlDNSAddress () string {
537+ a .mu .Lock ()
538+ defer a .mu .Unlock ()
461539 if a .dnsResolver != nil {
462540 return a .dnsResolver .DNSAddress ()
463541 }
464542 return ""
465543}
466544
467545func (a * DNSService ) IsAwlDNSSetAsSystem () bool {
546+ a .mu .Lock ()
547+ defer a .mu .Unlock ()
468548 return a .isAwlDNSSetAsSystem
469549}
470550
0 commit comments