Skip to content

Commit 9289efa

Browse files
committed
Merge remote-tracking branch 'anywherelan/awl/master'
2 parents e11a35f + b918a6f commit 9289efa

26 files changed

Lines changed: 1526 additions & 123 deletions

README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
- [Terminal-based client](#terminal-based-client)
3333
- [Common examples](#common-examples)
3434
- [Upgrading](#upgrading)
35+
- [Platform notes & known limitations](#platform-notes--known-limitations)
3536
- [Contributing](#contributing)
3637
- [License](#license)
3738

@@ -196,8 +197,6 @@ Open the web UI at http://admin.awl (or the Android app). On the Status / Overvi
196197

197198
To try the public tester: enter `12D3KooWJMUjt9b5T1umzgzjLv5yG2ViuuF4qjmN65tsRXZGS1p8` as peer id, name it `awl-tester`, save. After a few seconds it will appear in your peer list. Open http://awl-tester.awl/ — you should see a network speed-test page.
198199

199-
> `.awl` DNS is not yet available on Android ([#17](https://github.com/anywherelan/awl/issues/17)); on Android you access peers by IP.
200-
201200
When someone invites you, a notification will appear; accept or block in the admin UI.
202201

203202
### Server
@@ -496,6 +495,24 @@ systemctl restart awl
496495

497496
As an alternative on desktop or server: download the new build from the [releases page](https://github.com/anywherelan/awl/releases) and replace the files manually.
498497

498+
## Platform notes & known limitations
499+
500+
### `.awl` name resolution
501+
502+
`.awl` names resolve on every platform, but the mechanism differs:
503+
504+
- **Desktop (Linux / Windows / macOS):** awl runs a local resolver and registers it with the OS. Where the OS supports split-DNS only the `.awl` zone is captured; the rest of your DNS is left untouched, so LAN names keep working.
505+
- **Android:** the app resolves `.awl` inside the tunnel — `.awl` is answered locally, everything else is forwarded to the configured upstream resolver (`1.1.1.1` by default, `dns.upstreamDNSAddress` in the config). This has a few consequences:
506+
- **Private DNS in strict mode** (a hostname set under Android's *Private DNS* setting) bypasses the VPN's DNS entirely, so `.awl` names won't resolve. The default *Automatic* mode works fine.
507+
- While DNS is enabled, all queries go to the configured upstream instead of your network's own resolver, so LAN-only names handed out by your router (e.g. `printer.lan`) won't resolve.
508+
- `admin.awl` is not reachable on Android — use the app's own UI instead.
509+
- `dns.disableDNS: true` in the config turns awl's DNS handling off entirely: `.awl` stops resolving and queries go straight to the system resolver again.
510+
511+
### Other limitations
512+
513+
- **IPv6 inside the tunnel is not supported** — IPv6 packets are dropped, only IPv4 is carried.
514+
- **Serving as a VPN gateway / exit node** is not available on every platform — see the [support table](#vpn-gateway-full-tunnel-exit-node).
515+
499516
# Contributing
500517

501518
Contributions to this repository are very welcome.

application.go

Lines changed: 112 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package awl
33
import (
44
"context"
55
"embed"
6+
"errors"
67
"fmt"
78
"io/fs"
89
"net"
@@ -32,6 +33,7 @@ import (
3233

3334
"github.com/anywherelan/awl/api"
3435
"github.com/anywherelan/awl/awldns"
36+
"github.com/anywherelan/awl/awldns/dnsbridge"
3537
"github.com/anywherelan/awl/awlevent"
3638
"github.com/anywherelan/awl/config"
3739
"github.com/anywherelan/awl/metrics"
@@ -198,12 +200,7 @@ func (a *Application) Init(ctx context.Context, tunDevice tun.Device) error {
198200
go a.SOCKS5.ServeConns(a.ctx)
199201

200202
if !a.Conf.DNS.DisableDNS && !a.Conf.VPNConfig.DisableVPNInterface {
201-
interfaceName, err := a.vpnDevice.InterfaceName()
202-
if err != nil {
203-
a.logger.Errorf("failed to get TUN interface name: %v", err)
204-
} else {
205-
a.Dns.initDNS(interfaceName)
206-
}
203+
a.setupDNS()
207204
}
208205

209206
// Metrics
@@ -222,6 +219,23 @@ func (a *Application) Init(ctx context.Context, tunDevice tun.Device) error {
222219
return nil
223220
}
224221

222+
// setupDNS picks the platform DNS path. On Android there are no OS sockets
223+
// and no OS DNS configurator: DNS packets are intercepted from the TUN read
224+
// path into a netstack bridge instead.
225+
func (a *Application) setupDNS() {
226+
if runtime.GOOS == "android" {
227+
a.Dns.initDNSAndroid(a.vpnDevice, a.Tunnel)
228+
return
229+
}
230+
231+
interfaceName, err := a.vpnDevice.InterfaceName()
232+
if err != nil {
233+
a.logger.Errorf("failed to get TUN interface name: %v", err)
234+
return
235+
}
236+
a.Dns.initDNS(interfaceName)
237+
}
238+
225239
func (a *Application) SetupLoggerAndConfig(appType config.AppType) *log.ZapEventLogger {
226240
a.Eventbus = eventbus.NewBus()
227241
// Config
@@ -267,8 +281,16 @@ func (a *Application) SetupLoggerAndConfig(appType config.AppType) *log.ZapEvent
267281
a.logger = log.Logger("awl")
268282
a.Conf = conf
269283

270-
if loadConfigErr != nil {
271-
a.logger.Warnf("failed to read config file, creating new one: %v", loadConfigErr)
284+
if errors.Is(loadConfigErr, fs.ErrNotExist) {
285+
// First run: there is simply no config yet.
286+
a.logger.Infof("no config file found, creating new one")
287+
} else if loadConfigErr != nil {
288+
// The file is there but unusable. We are about to run with a new
289+
// identity and no known peers, and the first save will overwrite the
290+
// old file, so this is data loss and must not read as a routine
291+
// warning. LoadConfig has already copied a corrupted config aside.
292+
a.logger.Errorf("failed to read existing config file, starting with a new one "+
293+
"(previous identity and known peers will not be used): %v", loadConfigErr)
272294
}
273295
a.logger.Infof("Anywherelan %s (%s %s-%s)", config.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
274296
a.logger.Infof("Initializing app in %s directory", conf.DataDir())
@@ -320,7 +342,7 @@ func (a *Application) Close() {
320342
a.logger.Errorf("closing vpn: %v", err)
321343
}
322344
}
323-
a.Conf.Save()
345+
a.Conf.Close()
324346
}
325347

326348
func (a *Application) makeP2pHostConfig() p2p.HostConfig {
@@ -377,15 +399,17 @@ type DNSService struct {
377399

378400
mu sync.Mutex
379401
dnsHost string
380-
dnsFQDN dnsname.FQDN
381402
dnsOsConfigurator dns.OSConfigurator
382403
dnsResolver *awldns.Resolver
404+
dnsBridge *dnsbridge.Bridge
383405
upstreamDNS string
384406
isAwlDNSSetAsSystem bool
385407
// forceUpstream forces the awl resolver to capture all queries
386408
// (MatchDomains=nil) and forward them to the configured public upstream so
387409
// DNS traverses the tunnel instead of leaking to the system resolver. Set
388-
// in VPN gateway client mode.
410+
// in VPN gateway client mode. Desktop only: the Android netstack bridge has
411+
// no split-DNS choice to make (it already captures all device DNS), so it
412+
// leaves this at zero and ForceUpstreamDNS is a no-op there.
389413
forceUpstream bool
390414
}
391415

@@ -405,20 +429,6 @@ func (a *DNSService) initDNS(interfaceName string) {
405429
}
406430
a.dnsHost = dnsHost
407431

408-
fqdn, err := dnsname.ToFQDN(awldns.LocalDomain)
409-
if err != nil {
410-
panic(err)
411-
}
412-
a.dnsFQDN = fqdn
413-
414-
// TODO(android awldns): on Android this NewResolver cannot bind :53 (needs
415-
// root) and dnsOsConfigurator.SetDNS below fails (no writable resolv.conf),
416-
// so awldns is effectively inert there and .awl names do not resolve. The
417-
// Android host instead points VpnService at DNS.UpstreamDNSAddress directly
418-
// (see awl-flutter MainActivity.establishTun), which prevents leaks but
419-
// gives no .awl resolution. A full fix would intercept :53 to a magic awl
420-
// IP inside the tunnel read-path (userspace netstack),
421-
// rather than binding an OS socket.
422432
a.dnsResolver = awldns.NewResolver(dnsAddr)
423433
a.upstreamDNS = a.conf.DNS.UpstreamDNSAddress
424434
a.forceUpstream = a.conf.VPNGateway.ClientEnabled
@@ -442,6 +452,60 @@ func (a *DNSService) initDNS(interfaceName string) {
442452
a.applyOSDNSConfigLocked()
443453
}
444454

455+
// initDNSAndroid sets up the Android DNS path: no OS sockets and no OS DNS
456+
// configurator. A netstack bridge (awldns/dnsbridge) owns the in-subnet DNS
457+
// IP, the Tunnel feeds it packets intercepted from the TUN read path, and the
458+
// resolver serves on the bridge's listeners. The Android host passes the same
459+
// IP to VpnService.Builder.addDnsServer, so all device DNS arrives there. On
460+
// any failure DNS stays off with a log; VPN keeps working.
461+
func (a *DNSService) initDNSAndroid(vpnDevice *vpn.Device, tunnel *service.Tunnel) {
462+
a.mu.Lock()
463+
defer a.mu.Unlock()
464+
465+
dnsIP := a.conf.NetstackDNSIP()
466+
if dnsIP == nil {
467+
a.logger.Errorf("no free IP for the DNS server in VPN subnet %s, DNS is disabled", a.conf.VPNConfig.IPNet)
468+
return
469+
}
470+
dnsAddr, ok := netip.AddrFromSlice(dnsIP.To4())
471+
if !ok {
472+
a.logger.Errorf("invalid DNS server IP %v, DNS is disabled", dnsIP)
473+
return
474+
}
475+
476+
bridge, err := dnsbridge.New(dnsAddr, vpn.InterfaceMTU, vpnDevice.WriteRawPacket)
477+
if err != nil {
478+
a.logger.Errorf("create DNS netstack bridge, DNS is disabled: %v", err)
479+
return
480+
}
481+
a.dnsBridge = bridge
482+
a.dnsResolver = awldns.NewResolverFromListeners(bridge.UDPConn(), bridge.TCPListener(),
483+
net.JoinHostPort(dnsIP.String(), awldns.DefaultDNSPort))
484+
485+
// TODO(android awldns): use the system DNS servers reported by the Android
486+
// layer (ConnectivityManager network callback) as upstream, so LAN names
487+
// keep resolving; for now non-.awl queries always go to the configured
488+
// public upstream. In VPN gateway client mode the resolver's upstream
489+
// socket is deliberately unprotected: its traffic loops back into the TUN
490+
// and leaves through the gateway peer — no DNS leak.
491+
a.upstreamDNS = a.conf.DNS.UpstreamDNSAddress
492+
a.refreshDNSConfigLocked()
493+
494+
awlevent.WrapSubscriptionToCallback(a.ctx, func(_ interface{}) {
495+
a.mu.Lock()
496+
defer a.mu.Unlock()
497+
a.refreshDNSConfigLocked()
498+
}, a.eventbus, new(awlevent.KnownPeerChanged))
499+
500+
tunnel.SetDNSHandler(dnsIP, bridge)
501+
// The interceptor now handles all device DNS — the host sets the same IP
502+
// via addDnsServer — which is exactly what this flag means to the UI.
503+
// (dnsIP is config.NetstackDNSIP, reserved from peers since setDefaults.)
504+
a.isAwlDNSSetAsSystem = true
505+
506+
a.logger.Infof("DNS interceptor is set up on %s (upstream %s)", dnsAddr, a.upstreamDNS)
507+
}
508+
445509
// applyOSDNSConfigLocked (re)computes the OS DNS takeover config from the
446510
// current state (split-DNS support, base config, forceUpstream) and pushes it
447511
// to the OS, then refreshes the awl resolver. Caller must hold a.mu and have a
@@ -548,7 +612,12 @@ func (a *DNSService) refreshDNSConfigLocked() {
548612
return
549613
}
550614
dnsNamesMapping := a.conf.DNSNamesMapping()
551-
dnsNamesMapping[config.AdminHttpServerDomainName] = config.AdminHttpServerIP
615+
// TODO(android awldns): make admin.awl work on Android. The admin server is
616+
// not reachable on AdminHttpServerIP there (the API listens elsewhere and
617+
// port 80 cannot be bound), so don't advertise a dead name.
618+
if runtime.GOOS != "android" {
619+
dnsNamesMapping[config.AdminHttpServerDomainName] = config.AdminHttpServerIP
620+
}
552621
dnsNamesMappingV6 := a.conf.DNSNamesMappingV6()
553622
a.dnsResolver.ReceiveConfiguration(a.upstreamDNS, dnsNamesMapping, dnsNamesMappingV6)
554623
}
@@ -565,8 +634,14 @@ func (a *DNSService) Close() {
565634
if a.dnsResolver != nil {
566635
a.dnsResolver.Close()
567636
}
637+
if a.dnsBridge != nil {
638+
a.dnsBridge.Close()
639+
}
568640
}
569641

642+
// AwlDNSAddress returns the resolver address (ip:port) to display in the
643+
// status API/UI/CLI, empty until both resolver servers are up. Not the
644+
// host-wiring IP — for that see NetstackDNSServerIP.
570645
func (a *DNSService) AwlDNSAddress() string {
571646
a.mu.Lock()
572647
defer a.mu.Unlock()
@@ -582,6 +657,17 @@ func (a *DNSService) IsAwlDNSSetAsSystem() bool {
582657
return a.isAwlDNSSetAsSystem
583658
}
584659

660+
// NetstackDNSServerIP returns the in-tunnel IP owned by the running DNS
661+
// interceptor (Android), nil when the interceptor is not set up.
662+
func (a *DNSService) NetstackDNSServerIP() net.IP {
663+
a.mu.Lock()
664+
defer a.mu.Unlock()
665+
if a.dnsBridge == nil {
666+
return nil
667+
}
668+
return a.dnsBridge.DNSIP().AsSlice()
669+
}
670+
585671
// configMetricsAdapter implements metrics.ConfigMetrics by combining Config and AuthStatus.
586672
type configMetricsAdapter struct {
587673
conf *config.Config

application_gateway_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,3 +1145,32 @@ func TestGatewayUnknownPeerIDAtStartupFailsBoot(t *testing.T) {
11451145
ts.Equal(unknownPeerID, tp.app.Conf.VPNGateway.GatewayPeerID,
11461146
"unknown peer at startup must NOT auto-wipe GatewayPeerID")
11471147
}
1148+
1149+
// TestGatewayDNSInterception verifies the DNS interceptor filter runs before
1150+
// the gateway-client branch in HandleReadPackets: a packet to the in-tunnel
1151+
// DNS IP is diverted to the handler instead of being dropped as in-subnet
1152+
// traffic (or forwarded to the exit node).
1153+
func TestGatewayDNSInterception(t *testing.T) {
1154+
skipIfVPNGatewayUnsupported(t)
1155+
ts := NewTestSuite(t)
1156+
1157+
client, _, _ := setupGatewayPeers(ts)
1158+
1159+
dnsIP := client.app.Conf.NetstackDNSIP()
1160+
ts.NotNil(dnsIP)
1161+
1162+
intercepted := make(chan []byte, 16)
1163+
client.app.Tunnel.SetDNSHandler(dnsIP, dnsHandlerFunc(func(packet []byte) {
1164+
intercepted <- append([]byte{}, packet...)
1165+
}))
1166+
1167+
dnsPacket := testPacketWithDest(0, dnsIP.String())
1168+
client.tun.Outbound <- [][]byte{dnsPacket}
1169+
1170+
select {
1171+
case got := <-intercepted:
1172+
ts.Equal(dnsPacket, got)
1173+
case <-time.After(5 * time.Second):
1174+
ts.FailNow("dns packet was not intercepted in gateway client mode")
1175+
}
1176+
}

0 commit comments

Comments
 (0)