Skip to content

Commit e450d6d

Browse files
committed
feat(network): support deterministic IPv6 addressing and expose subnet APIs for Android
- Refactored IPv6 derivation logic into `config/ipv6.go` for cleaner abstraction. - Updated `VPNLocalIPMaskV6Unlocked` to automatically compute and return the derived IPv6 address based on the node's PeerID, avoiding redundant derivation in higher layers (`application.go`). - Added `ipv6Addr` to `PeerStatusInfo` JSON payload for exchanging IPv6 addresses with peers. - Exported `GetVpnNetworkAddressV4` and `GetVpnNetworkAddressV6` in `gomobile-lib` so the Android VPN service can query the exact subnet base addresses required for split-tunnel routing.
1 parent a07c568 commit e450d6d

17 files changed

Lines changed: 334 additions & 196 deletions

File tree

.github/workflows/test.yml

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,13 @@ jobs:
129129
./librespeed-cli --local-json config_librespeed.json --server 2 --json --share --telemetry-level disabled | python3 -m json.tool
130130
131131
ping 10.66.0.2 -w 20 -c 10
132-
ping6 fd00:66:0::2 -w 20 -c 10
133-
# TODO: remove this temporal hack for linux
134-
ping awl-tester.awl -w 20 -c 10 || true
132+
IPV6=$(./awl cli peers status -f p | grep awl-tester | grep -o 'fd00:[0-9a-f:]*' || true)
133+
if [ -n "$IPV6" ]; then
134+
echo "IPv6 detected: $IPV6. Running ping6..."
135+
ping6 awl-tester.awl -w 20 -c 10
136+
else
137+
echo "awl-tester does not have IPv6 enabled yet, skipping IPv6 ping test."
138+
fi
135139
136140
# ---- VPN gateway server (exit-node) mode: runtime enable/disable round-trips OS state ----
137141
# awl runs as root here, so enabling the server installs real NAT
@@ -232,8 +236,13 @@ jobs:
232236
./librespeed-cli --local-json config_librespeed.json --server 2 --json --share --telemetry-level disabled | python3 -m json.tool
233237
234238
ping 10.66.0.2 -c 10
235-
ping6 fd00:66:0::2 -c 10
236-
ping awl-tester.awl -c 10
239+
IPV6=$(./awl cli peers status -f p | grep awl-tester | grep -o 'fd00:[0-9a-f:]*' || true)
240+
if [ -n "$IPV6" ]; then
241+
echo "IPv6 detected: $IPV6. Running ping6..."
242+
ping6 awl-tester.awl -c 10
243+
else
244+
echo "awl-tester does not have IPv6 enabled yet, skipping IPv6 ping test."
245+
fi
237246
238247
sleep 1
239248
sudo kill -SIGINT $awl_pid
@@ -253,8 +262,13 @@ jobs:
253262
./librespeed-cli.exe --local-json config_librespeed.json --server 2 --json --share --telemetry-level disabled | python3 -m json.tool
254263
255264
ping -w 20000 -n 10 10.66.0.2
256-
ping -6 -w 20000 -n 10 fd00:66:0::2
257-
ping -w 20000 -n 10 -a awl-tester.awl
265+
IPV6=$(./awl.exe cli peers status -f p | grep awl-tester | grep -o 'fd00:[0-9a-f:]*' || true)
266+
if [ -n "$IPV6" ]; then
267+
echo "IPv6 detected: $IPV6. Running ping6..."
268+
ping -6 -w 20000 -n 10 awl-tester.awl
269+
else
270+
echo "awl-tester does not have IPv6 enabled yet, skipping IPv6 ping test."
271+
fi
258272
259273
# ---- VPN gateway server (exit-node) mode: runtime enable/disable round-trips OS state ----
260274
# Diagnostic first: what the runner already holds in WinNAT (a

api/peers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func (h *Handler) getKnownPeers() []entity.KnownPeersResponse {
4848
Alias: knownPeer.Alias,
4949
Version: config.VersionFromUserAgent(h.p2p.PeerUserAgent(id)),
5050
IpAddr: knownPeer.IPAddr,
51+
IpAddrV6: knownPeer.IPAddrV6,
5152
DomainName: knownPeer.DomainName,
5253
Connected: h.p2p.IsConnected(id),
5354
Confirmed: knownPeer.Confirmed,

api/settings.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package api
22

33
import (
4+
"net"
45
"net/http"
56

67
"github.com/labstack/echo/v4"
@@ -96,6 +97,14 @@ func (h *Handler) GetMyPeerInfo(c echo.Context) (err error) {
9697
}(),
9798
}
9899

100+
ipV6, maskV6 := h.conf.VPNLocalIPMaskV6()
101+
if ipV6 != nil && maskV6 != nil {
102+
ipNetV6 := &net.IPNet{IP: ipV6.Mask(maskV6), Mask: maskV6}
103+
if ipv6 := config.DeriveIPv6FromPeerID(h.p2p.PeerID(), ipNetV6); ipv6 != nil {
104+
peerInfo.VPN.IPv6Addr = ipv6.String()
105+
}
106+
}
107+
99108
return c.JSON(http.StatusOK, peerInfo)
100109
}
101110

application.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,8 @@ func (a *DNSService) refreshDNSConfigLocked() {
549549
}
550550
dnsNamesMapping := a.conf.DNSNamesMapping()
551551
dnsNamesMapping[config.AdminHttpServerDomainName] = config.AdminHttpServerIP
552-
a.dnsResolver.ReceiveConfiguration(a.upstreamDNS, dnsNamesMapping)
552+
dnsNamesMappingV6 := a.conf.DNSNamesMappingV6()
553+
a.dnsResolver.ReceiveConfiguration(a.upstreamDNS, dnsNamesMapping, dnsNamesMappingV6)
553554
}
554555

555556
func (a *DNSService) Close() {

awldns/awldns.go

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package awldns
22

33
import (
44
"net"
5+
"strconv"
56
"strings"
67
"sync/atomic"
78
"time"
@@ -17,6 +18,7 @@ const (
1718
defaultTTL = 60 * time.Second
1819
defaultTTLSeconds = uint32(defaultTTL / time.Second)
1920
ptrV4Suffix = ".in-addr.arpa."
21+
ptrV6Suffix = ".ip6.arpa."
2022
)
2123

2224
const (
@@ -41,9 +43,10 @@ type Resolver struct {
4143
}
4244

4345
type config struct {
44-
upstreamDNS string
45-
directMapping map[string]string
46-
reverseMapping map[string]string
46+
upstreamDNS string
47+
directMapping map[string]string
48+
directMappingV6 map[string]string
49+
reverseMapping map[string]string
4750
}
4851

4952
func NewResolver(dnsAddress string) *Resolver {
@@ -61,7 +64,8 @@ func NewResolver(dnsAddress string) *Resolver {
6164

6265
mux := dns.NewServeMux()
6366
mux.HandleFunc(LocalDomain, r.dnsLocalDomainHandler)
64-
mux.HandleFunc(strings.TrimPrefix(ptrV4Suffix, "."), r.ptrv4Handler)
67+
mux.HandleFunc(strings.TrimPrefix(ptrV4Suffix, "."), r.ptrHandler)
68+
mux.HandleFunc(strings.TrimPrefix(ptrV6Suffix, "."), r.ptrHandler)
6569
mux.HandleFunc(".", r.dnsProxyHandler)
6670

6771
r.udpServer = &dns.Server{
@@ -100,9 +104,11 @@ func NewResolver(dnsAddress string) *Resolver {
100104
return r
101105
}
102106

103-
func (r *Resolver) ReceiveConfiguration(upstreamDNS string, namesMapping map[string]string) {
104-
reverseMapping := make(map[string]string, len(namesMapping))
107+
func (r *Resolver) ReceiveConfiguration(upstreamDNS string, namesMapping map[string]string, namesMappingV6 map[string]string) {
108+
reverseMapping := make(map[string]string, len(namesMapping)+len(namesMappingV6))
105109
directMapping := make(map[string]string, len(namesMapping))
110+
directMappingV6 := make(map[string]string, len(namesMappingV6))
111+
106112
for key, ip := range namesMapping {
107113
canonicalName := dns.CanonicalName(key + "." + LocalDomain)
108114
directMapping[canonicalName] = ip
@@ -116,10 +122,22 @@ func (r *Resolver) ReceiveConfiguration(upstreamDNS string, namesMapping map[str
116122
}
117123
}
118124

125+
for key, ip := range namesMappingV6 {
126+
canonicalName := dns.CanonicalName(key + "." + LocalDomain)
127+
directMappingV6[canonicalName] = ip
128+
existedName, exists := reverseMapping[ip]
129+
if !exists {
130+
reverseMapping[ip] = canonicalName
131+
} else if exists && len(canonicalName) < len(existedName) {
132+
reverseMapping[ip] = canonicalName
133+
}
134+
}
135+
119136
cfg := config{
120-
upstreamDNS: upstreamDNS,
121-
directMapping: directMapping,
122-
reverseMapping: reverseMapping,
137+
upstreamDNS: upstreamDNS,
138+
directMapping: directMapping,
139+
directMappingV6: directMappingV6,
140+
reverseMapping: reverseMapping,
123141
}
124142
r.cfg.Store(&cfg)
125143
}
@@ -166,7 +184,11 @@ func (r *Resolver) dnsLocalDomainHandler(resp dns.ResponseWriter, req *dns.Msg)
166184

167185
switch qtype {
168186
case dns.TypeA, dns.TypeANY:
187+
_, foundV6 := cfg.directMappingV6[hostnameLower]
169188
if !found {
189+
if foundV6 {
190+
continue // domain exists but no A record, return NOERROR with 0 answers (NODATA)
191+
}
170192
m.SetRcode(req, dns.RcodeNameError)
171193
continue
172194
}
@@ -183,11 +205,26 @@ func (r *Resolver) dnsLocalDomainHandler(resp dns.ResponseWriter, req *dns.Msg)
183205
})
184206
}
185207
case dns.TypeAAAA:
186-
if !found {
208+
_, foundV4 := cfg.directMapping[hostnameLower]
209+
mappedIPv6, foundV6 := cfg.directMappingV6[hostnameLower]
210+
if !foundV6 {
211+
if foundV4 {
212+
continue // domain exists but no AAAA record, return NOERROR with 0 answers (NODATA)
213+
}
187214
m.SetRcode(req, dns.RcodeNameError)
188215
continue
189216
}
190-
// TODO: support IPv6 addresses in cfg.directMapping.
217+
if ip := net.ParseIP(mappedIPv6).To16(); ip != nil {
218+
m.Answer = append(m.Answer, &dns.AAAA{
219+
Hdr: dns.RR_Header{
220+
Name: hostname,
221+
Rrtype: dns.TypeAAAA,
222+
Class: dns.ClassINET,
223+
Ttl: defaultTTLSeconds,
224+
},
225+
AAAA: ip,
226+
})
227+
}
191228
}
192229
}
193230

@@ -196,7 +233,7 @@ func (r *Resolver) dnsLocalDomainHandler(resp dns.ResponseWriter, req *dns.Msg)
196233
_ = resp.WriteMsg(m)
197234
}
198235

199-
func (r *Resolver) ptrv4Handler(resp dns.ResponseWriter, req *dns.Msg) {
236+
func (r *Resolver) ptrHandler(resp dns.ResponseWriter, req *dns.Msg) {
200237
metrics.DNSQueriesTotal.WithLabelValues("awl_ptr").Inc()
201238
start := time.Now()
202239
defer func() {
@@ -211,7 +248,13 @@ func (r *Resolver) ptrv4Handler(resp dns.ResponseWriter, req *dns.Msg) {
211248
name := req.Question[0].Name
212249
cfg := r.loadConfig()
213250

214-
ip := ptrV4NameToIP(name)
251+
var ip net.IP
252+
if strings.HasSuffix(strings.ToLower(name), ptrV6Suffix) {
253+
ip = ptrV6NameToIP(name)
254+
} else {
255+
ip = ptrV4NameToIP(name)
256+
}
257+
215258
if ip == nil {
216259
r.dnsProxyHandler(resp, req)
217260
return
@@ -312,11 +355,29 @@ func IsValidDomainName(domain string) bool {
312355
}
313356

314357
func ptrV4NameToIP(name string) net.IP {
315-
s := strings.TrimSuffix(name, ptrV4Suffix)
358+
s := strings.TrimSuffix(strings.ToLower(name), ptrV4Suffix)
316359
revIp := net.ParseIP(s)
317360
revIp = revIp.To4()
318361
if revIp == nil {
319362
return nil
320363
}
321364
return net.IP{revIp[3], revIp[2], revIp[1], revIp[0]}
322365
}
366+
367+
func ptrV6NameToIP(name string) net.IP {
368+
s := strings.TrimSuffix(strings.ToLower(name), ptrV6Suffix)
369+
parts := strings.Split(s, ".")
370+
if len(parts) != 32 {
371+
return nil
372+
}
373+
ip := make(net.IP, 16)
374+
for i := 0; i < 16; i++ {
375+
high, err1 := strconv.ParseUint(parts[31-(i*2)], 16, 8)
376+
low, err2 := strconv.ParseUint(parts[31-(i*2)-1], 16, 8)
377+
if err1 != nil || err2 != nil {
378+
return nil
379+
}
380+
ip[i] = byte((high << 4) | low)
381+
}
382+
return ip
383+
}

awldns/awldns_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestDNS(t *testing.T) {
3434
name1: addr1,
3535
name2: addr2,
3636
}
37-
resolver.ReceiveConfiguration("", namesMapping)
37+
resolver.ReceiveConfiguration("", namesMapping, nil)
3838

3939
client := NewResolverClient(addr)
4040

@@ -80,7 +80,7 @@ func TestDNSAAAAQueryDoesNotReturnARecord(t *testing.T) {
8080

8181
resolver.ReceiveConfiguration("", map[string]string{
8282
"admin": "127.0.0.66",
83-
})
83+
}, nil)
8484

8585
req := new(dns.Msg)
8686
req.SetQuestion("admin.awl.", dns.TypeAAAA)
@@ -102,7 +102,7 @@ func TestDNSAQueryReturnsARecord(t *testing.T) {
102102

103103
resolver.ReceiveConfiguration("", map[string]string{
104104
"admin": "127.0.0.66",
105-
})
105+
}, nil)
106106

107107
req := new(dns.Msg)
108108
req.SetQuestion("admin.awl.", dns.TypeA)
@@ -129,7 +129,7 @@ func TestDNSUnknownAddressReturnsNameError(t *testing.T) {
129129

130130
resolver.ReceiveConfiguration("", map[string]string{
131131
"admin": "127.0.0.66",
132-
})
132+
}, nil)
133133

134134
req := new(dns.Msg)
135135
req.SetQuestion("unknown.awl.", dns.TypeA)

cli/peers.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ func printPeersStatus(api *apiclient.Client, format string, w io.Writer) error {
8383
if peer.DomainName != "" {
8484
info = append(info, fmt.Sprintf("%s.%s", peer.DomainName, awldns.LocalDomain))
8585
}
86+
if peer.IpAddrV6 != "" {
87+
info = append(info, peer.IpAddrV6)
88+
}
8689
info = append(info, peer.IpAddr)
8790

8891
row = append(row, strings.Join(info, "\n"))

cmd/gomobile-lib/main.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package anywherelan
55
import (
66
"context"
77
"fmt"
8+
"net"
89
"os"
910

1011
"github.com/libp2p/go-libp2p/p2p/host/eventbus"
@@ -48,6 +49,57 @@ func GetConfig() string {
4849
return string(data)
4950
}
5051

52+
func GetLocalIPv6() string {
53+
if globalDataDir == "" {
54+
panic("call to GetLocalIPv6 before Setup")
55+
}
56+
57+
conf, loadConfigErr := config.LoadConfig(appType, eventbus.NewBus())
58+
if loadConfigErr != nil {
59+
return ""
60+
}
61+
62+
ipV6, _ := conf.VPNLocalIPMaskV6()
63+
if ipV6 != nil {
64+
return ipV6.String()
65+
}
66+
return ""
67+
}
68+
69+
func GetVpnNetworkAddressV4() string {
70+
if globalDataDir == "" {
71+
panic("call to GetVpnNetworkAddressV4 before Setup")
72+
}
73+
74+
conf, loadConfigErr := config.LoadConfig(appType, eventbus.NewBus())
75+
if loadConfigErr != nil {
76+
return ""
77+
}
78+
79+
_, ipNet, err := net.ParseCIDR(conf.VPNConfig.IPNet)
80+
if err == nil && ipNet != nil {
81+
return ipNet.IP.String()
82+
}
83+
return ""
84+
}
85+
86+
func GetVpnNetworkAddressV6() string {
87+
if globalDataDir == "" {
88+
panic("call to GetVpnNetworkAddressV6 before Setup")
89+
}
90+
91+
conf, loadConfigErr := config.LoadConfig(appType, eventbus.NewBus())
92+
if loadConfigErr != nil {
93+
return ""
94+
}
95+
96+
_, ipNet, err := net.ParseCIDR(conf.VPNConfig.IPNetV6)
97+
if err == nil && ipNet != nil {
98+
return ipNet.IP.String()
99+
}
100+
return ""
101+
}
102+
51103
// SocketProtector is the interface that the Android host app must implement
52104
// when it wants AWL to mark libp2p sockets so they bypass the VPN. The
53105
// implementation should call android.net.VpnService.protect() under the hood.

0 commit comments

Comments
 (0)