Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,13 @@ jobs:
./librespeed-cli --local-json config_librespeed.json --server 2 --json --share --telemetry-level disabled | python3 -m json.tool

ping 10.66.0.2 -w 20 -c 10
# TODO: remove this temporal hack for linux
ping awl-tester.awl -w 20 -c 10 || true
IPV6=$(./awl cli peers status -f p | grep awl-tester | grep -o 'fd00:[0-9a-f:]*' || true)
if [ -n "$IPV6" ]; then
echo "IPv6 detected: $IPV6. Running ping6..."
ping6 awl-tester.awl -w 20 -c 10
else
echo "awl-tester does not have IPv6 enabled yet, skipping IPv6 ping test."
fi

# ---- VPN gateway server (exit-node) mode: runtime enable/disable round-trips OS state ----
# awl runs as root here, so enabling the server installs real NAT
Expand Down Expand Up @@ -231,7 +236,13 @@ jobs:
./librespeed-cli --local-json config_librespeed.json --server 2 --json --share --telemetry-level disabled | python3 -m json.tool

ping 10.66.0.2 -c 10
ping awl-tester.awl -c 10
IPV6=$(./awl cli peers status -f p | grep awl-tester | grep -o 'fd00:[0-9a-f:]*' || true)
if [ -n "$IPV6" ]; then
echo "IPv6 detected: $IPV6. Running ping6..."
ping6 awl-tester.awl -c 10
else
echo "awl-tester does not have IPv6 enabled yet, skipping IPv6 ping test."
fi

sleep 1
sudo kill -SIGINT $awl_pid
Expand All @@ -251,7 +262,13 @@ jobs:
./librespeed-cli.exe --local-json config_librespeed.json --server 2 --json --share --telemetry-level disabled | python3 -m json.tool

ping -w 20000 -n 10 10.66.0.2
ping -w 20000 -n 10 -a awl-tester.awl
IPV6=$(./awl.exe cli peers status -f p | grep awl-tester | grep -o 'fd00:[0-9a-f:]*' || true)
if [ -n "$IPV6" ]; then
echo "IPv6 detected: $IPV6. Running ping6..."
ping -6 -w 20000 -n 10 awl-tester.awl
else
echo "awl-tester does not have IPv6 enabled yet, skipping IPv6 ping test."
fi

# ---- VPN gateway server (exit-node) mode: runtime enable/disable round-trips OS state ----
# Diagnostic first: what the runner already holds in WinNAT (a
Expand Down
1 change: 1 addition & 0 deletions api/peers.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func (h *Handler) getKnownPeers() []entity.KnownPeersResponse {
Alias: knownPeer.Alias,
Version: config.VersionFromUserAgent(h.p2p.PeerUserAgent(id)),
IpAddr: knownPeer.IPAddr,
IpAddrV6: knownPeer.IPAddrV6,
DomainName: knownPeer.DomainName,
Connected: h.p2p.IsConnected(id),
Confirmed: knownPeer.Confirmed,
Expand Down
4 changes: 4 additions & 0 deletions api/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ func (h *Handler) GetMyPeerInfo(c echo.Context) (err error) {
}(),
}

if ipV6, _ := h.conf.VPNLocalIPMaskV6(); ipV6 != nil {
peerInfo.VPN.IPv6Addr = ipV6.String()
}

return c.JSON(http.StatusOK, peerInfo)
}

Expand Down
11 changes: 8 additions & 3 deletions application.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ type NetManager interface {
EnableClientRoutes(tunIfName string) error
DisableClientRoutes() error
ClientRoutesActive() bool
EnableServerNAT(awlSubnet, tunIfName string) error
EnableServerNAT(awlSubnet, awlSubnet6, tunIfName string) error
DisableServerNAT() error
ServerNATActive() bool
}
Expand Down Expand Up @@ -147,12 +147,16 @@ func (a *Application) Init(ctx context.Context, tunDevice tun.Device) error {
a.logger.Info("VPN interface is disabled from config")
} else {
localIP, netMask := a.Conf.VPNLocalIPMask()
localIPv6, netMaskv6 := a.Conf.VPNLocalIPMaskV6()
interfaceName := a.Conf.VPNConfig.InterfaceName
a.vpnDevice, err = vpn.NewDevice(tunDevice, interfaceName, localIP, netMask)
a.vpnDevice, err = vpn.NewDevice(tunDevice, interfaceName, localIP, netMask, localIPv6, netMaskv6)
if err != nil {
return fmt.Errorf("failed to init vpn: %v", err)
}
a.logger.Infof("VPN interface created. Name: %s CIDR: %s", interfaceName, &net.IPNet{IP: localIP, Mask: netMask})
if localIPv6 != nil {
a.logger.Infof("VPN interface IPv6: %s", &net.IPNet{IP: localIPv6, Mask: netMaskv6})
}

a.Tunnel = service.NewTunnel(a.P2p, a.vpnDevice, a.Conf, a.Eventbus)
go a.vpnDevice.ReadTUNPackets(a.Tunnel.HandleReadPackets)
Expand Down Expand Up @@ -614,7 +618,8 @@ func (a *DNSService) refreshDNSConfigLocked() {
if runtime.GOOS != "android" {
dnsNamesMapping[config.AdminHttpServerDomainName] = config.AdminHttpServerIP
}
a.dnsResolver.ReceiveConfiguration(a.upstreamDNS, dnsNamesMapping)
dnsNamesMappingV6 := a.conf.DNSNamesMappingV6()
a.dnsResolver.ReceiveConfiguration(a.upstreamDNS, dnsNamesMapping, dnsNamesMappingV6)
}

func (a *DNSService) Close() {
Expand Down
35 changes: 32 additions & 3 deletions application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -845,15 +845,18 @@ func testSOCKS5Proxy(ts *TestSuite, proxyAddr string, expectSocksErr string) {
func testSOCKS5ProxyWithAuth(ts *TestSuite, proxyAddr string, auth *proxy.Auth, iterations int, expectSocksErr string) {
// setup mock server
expectedBody := strings.Repeat("test text", 10_000)
addr := pickFreeAddr(ts.t)
l, err := net.Listen("tcp", "127.0.0.1:0")
ts.NoError(err)
addr := l.Addr().String()

mux := http.NewServeMux()
mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprint(w, expectedBody)
})
//nolint
httpServer := &http.Server{Addr: addr, Handler: mux}
httpServer := &http.Server{Handler: mux}
go func() {
_ = httpServer.ListenAndServe()
_ = httpServer.Serve(l)
}()
defer func() {
httpServer.Shutdown(context.Background())
Expand Down Expand Up @@ -937,6 +940,32 @@ func TestTunnelPackets(t *testing.T) {
received2 := peer2.tun.InboundCount()
ts.EqualValues(packetsCount, received1)
ts.EqualValues(packetsCount, received2)

// --- IPv6 Routing Test ---
peer2ConfigInPeer1, _ := peer1.app.Conf.GetPeer(peer2.PeerID())
peer1ConfigInPeer2, _ := peer2.app.Conf.GetPeer(peer1.PeerID())

peer1IPv6Str := peer1ConfigInPeer2.IPAddrV6
peer2IPv6Str := peer2ConfigInPeer1.IPAddrV6

ts.t.Logf("DEBUG: peer1 IPNetV6: %v", peer1.app.Conf.VPNConfig.IPNetV6)
ts.t.Logf("DEBUG: peer1IPv6 calculated: %s, peer2IPv6 calculated: %s", peer1IPv6Str, peer2IPv6Str)

peer1.tun.ClearInboundCount()
peer2.tun.ClearInboundCount()

// Send IPv6 packets from peer1 to peer2
const ipv6PacketsCount = 10
ipv6Packet := testPacketWithSrcDestV6(packetSize, peer1IPv6Str, peer2IPv6Str)

for i := 0; i < ipv6PacketsCount; i++ {
peer1.tun.Outbound <- [][]byte{ipv6Packet}
time.Sleep(10 * time.Millisecond)
}

time.Sleep(1 * time.Second)
receivedIPv6 := peer2.tun.InboundCount()
ts.EqualValues(ipv6PacketsCount, receivedIPv6, "peer2 should receive exactly %d IPv6 packets", ipv6PacketsCount)
}

func BenchmarkTunnelPackets(b *testing.B) {
Expand Down
120 changes: 105 additions & 15 deletions awldns/awldns.go
Comment thread
NNdroid marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package awldns

import (
"net"
"strconv"
"strings"
"sync/atomic"
"time"
Expand All @@ -17,6 +18,7 @@ const (
defaultTTL = 60 * time.Second
defaultTTLSeconds = uint32(defaultTTL / time.Second)
ptrV4Suffix = ".in-addr.arpa."
ptrV6Suffix = ".ip6.arpa."
)

const (
Expand Down Expand Up @@ -44,9 +46,10 @@ type Resolver struct {
}

type config struct {
upstreamDNS string
directMapping map[string]string
reverseMapping map[string]string
upstreamDNS string
directMapping map[string]string
directMappingV6 map[string]string
reverseMapping map[string]string
}

// NewResolver creates a resolver that binds its own UDP and TCP sockets on
Expand Down Expand Up @@ -91,7 +94,8 @@ func newResolver(dnsAddress string) *Resolver {

mux := dns.NewServeMux()
mux.HandleFunc(LocalDomain, r.dnsLocalDomainHandler)
mux.HandleFunc(strings.TrimPrefix(ptrV4Suffix, "."), r.ptrv4Handler)
mux.HandleFunc(strings.TrimPrefix(ptrV4Suffix, "."), r.ptrHandler)
mux.HandleFunc(strings.TrimPrefix(ptrV6Suffix, "."), r.ptrHandler)
mux.HandleFunc(".", r.dnsProxyHandler)

r.udpServer = &dns.Server{
Expand Down Expand Up @@ -138,9 +142,11 @@ func serveDNSServer(srv *dns.Server) error {
return srv.ListenAndServe()
}

func (r *Resolver) ReceiveConfiguration(upstreamDNS string, namesMapping map[string]string) {
reverseMapping := make(map[string]string, len(namesMapping))
func (r *Resolver) ReceiveConfiguration(upstreamDNS string, namesMapping map[string]string, namesMappingV6 map[string]string) {
reverseMapping := make(map[string]string, len(namesMapping)+len(namesMappingV6))
directMapping := make(map[string]string, len(namesMapping))
directMappingV6 := make(map[string]string, len(namesMappingV6))

for key, ip := range namesMapping {
canonicalName := dns.CanonicalName(key + "." + LocalDomain)
directMapping[canonicalName] = ip
Expand All @@ -154,10 +160,22 @@ func (r *Resolver) ReceiveConfiguration(upstreamDNS string, namesMapping map[str
}
}

for key, ip := range namesMappingV6 {
canonicalName := dns.CanonicalName(key + "." + LocalDomain)
directMappingV6[canonicalName] = ip
existedName, exists := reverseMapping[ip]
if !exists {
reverseMapping[ip] = canonicalName
} else if exists && len(canonicalName) < len(existedName) {
reverseMapping[ip] = canonicalName
}
}

cfg := config{
upstreamDNS: upstreamDNS,
directMapping: directMapping,
reverseMapping: reverseMapping,
upstreamDNS: upstreamDNS,
directMapping: directMapping,
directMappingV6: directMappingV6,
reverseMapping: reverseMapping,
}
r.cfg.Store(&cfg)
}
Expand Down Expand Up @@ -201,10 +219,14 @@ func (r *Resolver) dnsLocalDomainHandler(resp dns.ResponseWriter, req *dns.Msg)
qtype := question.Qtype
hostnameLower := strings.ToLower(hostname)
mappedIP, found := cfg.directMapping[hostnameLower]
mappedIPv6, foundV6 := cfg.directMappingV6[hostnameLower]

switch qtype {
case dns.TypeA, dns.TypeANY:
case dns.TypeA:
if !found {
if foundV6 {
continue // domain exists but no A record, return NOERROR with 0 answers (NODATA)
}
m.SetRcode(req, dns.RcodeNameError)
continue
}
Expand All @@ -221,11 +243,55 @@ func (r *Resolver) dnsLocalDomainHandler(resp dns.ResponseWriter, req *dns.Msg)
})
}
case dns.TypeAAAA:
if !found {
if !foundV6 {
if found {
continue // domain exists but no AAAA record, return NOERROR with 0 answers (NODATA)
}
m.SetRcode(req, dns.RcodeNameError)
continue
}
if ip := net.ParseIP(mappedIPv6).To16(); ip != nil {
m.Answer = append(m.Answer, &dns.AAAA{
Hdr: dns.RR_Header{
Name: hostname,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
Ttl: defaultTTLSeconds,
},
AAAA: ip,
})
}
case dns.TypeANY:
if !found && !foundV6 {
m.SetRcode(req, dns.RcodeNameError)
continue
}
// TODO: support IPv6 addresses in cfg.directMapping.
if found {
if ip := net.ParseIP(mappedIP).To4(); ip != nil {
m.Answer = append(m.Answer, &dns.A{
Hdr: dns.RR_Header{
Name: hostname,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: defaultTTLSeconds,
},
A: ip,
})
}
}
if foundV6 {
if ip := net.ParseIP(mappedIPv6).To16(); ip != nil {
m.Answer = append(m.Answer, &dns.AAAA{
Hdr: dns.RR_Header{
Name: hostname,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
Ttl: defaultTTLSeconds,
},
AAAA: ip,
})
}
}
}
}

Expand All @@ -234,7 +300,7 @@ func (r *Resolver) dnsLocalDomainHandler(resp dns.ResponseWriter, req *dns.Msg)
_ = resp.WriteMsg(m)
}

func (r *Resolver) ptrv4Handler(resp dns.ResponseWriter, req *dns.Msg) {
func (r *Resolver) ptrHandler(resp dns.ResponseWriter, req *dns.Msg) {
metrics.DNSQueriesTotal.WithLabelValues("awl_ptr").Inc()
start := time.Now()
defer func() {
Expand All @@ -249,7 +315,13 @@ func (r *Resolver) ptrv4Handler(resp dns.ResponseWriter, req *dns.Msg) {
name := req.Question[0].Name
cfg := r.loadConfig()

ip := ptrV4NameToIP(name)
var ip net.IP
if strings.HasSuffix(strings.ToLower(name), ptrV6Suffix) {
ip = ptrV6NameToIP(name)
} else {
ip = ptrV4NameToIP(name)
}

if ip == nil {
r.dnsProxyHandler(resp, req)
return
Expand Down Expand Up @@ -350,11 +422,29 @@ func IsValidDomainName(domain string) bool {
}

func ptrV4NameToIP(name string) net.IP {
s := strings.TrimSuffix(name, ptrV4Suffix)
s := strings.TrimSuffix(strings.ToLower(name), ptrV4Suffix)
revIp := net.ParseIP(s)
revIp = revIp.To4()
if revIp == nil {
return nil
}
return net.IP{revIp[3], revIp[2], revIp[1], revIp[0]}
}

func ptrV6NameToIP(name string) net.IP {
s := strings.TrimSuffix(strings.ToLower(name), ptrV6Suffix)
parts := strings.Split(s, ".")
if len(parts) != 32 {
return nil
}
ip := make(net.IP, 16)
for i := 0; i < 16; i++ {
high, err1 := strconv.ParseUint(parts[31-(i*2)], 16, 8)
low, err2 := strconv.ParseUint(parts[31-(i*2)-1], 16, 8)
if err1 != nil || err2 != nil {
return nil
}
ip[i] = byte((high << 4) | low)
}
return ip
}
Loading