Skip to content

Commit efa542a

Browse files
committed
dnsx/alg: consistent hash domain -> alg-ip
Some apps cache dns answers beyond the stipulated TTLs (ex: GMaps, Instagram) end up connecting to alg-ips whose alg-ip -> real-ip mapping might have been lost to a tunnel reset (as mappings are ephemeral). This then results in those apps errorenously reporting a loss of connectivity. By using a hash to map domain names to alg ips, mappings can be "ressurrected" across tunnel resets, collisions notwithstanding.
1 parent 954bbf5 commit efa542a

File tree

3 files changed

+87
-5
lines changed

3 files changed

+87
-5
lines changed

intra/dnscrypt/proxy.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -509,10 +509,10 @@ func NewProxy() *Proxy {
509509
return &Proxy{
510510
routes: nil,
511511
registeredServers: make(map[string]RegisteredServer),
512-
certRefreshDelay: time.Duration(240) * time.Minute,
513-
certRefreshDelayAfterFailure: time.Duration(10 * time.Second),
512+
certRefreshDelay: 240 * time.Minute,
513+
certRefreshDelayAfterFailure: 10 * time.Second,
514514
certIgnoreTimestamp: false,
515-
timeout: time.Duration(20000) * time.Millisecond,
515+
timeout: 20000 * time.Millisecond,
516516
serversInfo: NewServersInfo(),
517517
liveServers: nil,
518518
lastStatus: dnsx.Start,

intra/dnsx/alg.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package dnsx
99
import (
1010
"encoding/binary"
1111
"errors"
12+
"hash/fnv"
1213
"net/netip"
1314
"strconv"
1415
"strings"
@@ -29,7 +30,7 @@ const (
2930
key4 = ":a"
3031
key6 = ":aaaa"
3132
NoTransport = "NoTransport"
32-
maxiter = 1000 // max number alg/nat evict iterations
33+
maxiter = 100 // max number alg/nat evict iterations
3334
)
3435

3536
var (
@@ -105,6 +106,7 @@ type dnsgateway struct {
105106
rdns RdnsResolver // local and remote rdns blocks
106107
octets []uint8 // ip4 octets, 100.x.y.z
107108
hexes []uint16 // ip6 hex, 64:ff9b:1:da19:0100.x.y.z
109+
chash bool // use consistent hashing to generae alg ips
108110
}
109111

110112
// NewDNSGateway returns a DNS ALG, ready for use.
@@ -120,6 +122,7 @@ func NewDNSGateway(inner Transport, outer RdnsResolver) (t *dnsgateway) {
120122
rdns: outer,
121123
octets: rfc6598,
122124
hexes: rfc8215a,
125+
chash: true,
123126
}
124127
// initial transport must be set before starting the gateway
125128
t.withTransport(inner)
@@ -493,6 +496,7 @@ func (t *dnsgateway) registerMultiLocked(q string, am *ansMulti) bool {
493496
return true
494497
}
495498

499+
// register mapping from qname -> algip+realip (alg) and algip -> qname+realip (nat)
496500
func (t *dnsgateway) registerNatLocked(q string, idx int, x *ans) bool {
497501
ip := x.algip
498502
var k string
@@ -508,6 +512,7 @@ func (t *dnsgateway) registerNatLocked(q string, idx int, x *ans) bool {
508512
return true
509513
}
510514

515+
// register mapping from realip -> algip+qname (px)
511516
func (t *dnsgateway) registerPxLocked(q string, idx int, x *ans) bool {
512517
ip := x.realip[idx]
513518
t.px[*ip] = x
@@ -528,6 +533,20 @@ func (t *dnsgateway) take4Locked(q string, idx int) (*netip.Addr, bool) {
528533
}
529534
}
530535

536+
if t.chash {
537+
for i := 0; i < maxiter; i++ {
538+
genip := gen4Locked(k, i)
539+
if !genip.IsGlobalUnicast() {
540+
continue
541+
}
542+
if _, taken := t.nat[genip]; !taken {
543+
return &genip, genip.IsValid()
544+
}
545+
}
546+
log.W("alg: gen: no more IP4s (%v)", q)
547+
return nil, false
548+
}
549+
531550
gen := true
532551
// 100.x.y.z: 4m+ ip4s
533552
if z := t.octets[3]; z < 254 {
@@ -566,6 +585,20 @@ func (t *dnsgateway) take4Locked(q string, idx int) (*netip.Addr, bool) {
566585
return nil, false
567586
}
568587

588+
func gen4Locked(k string, hop int) netip.Addr {
589+
s := strconv.Itoa(hop) + k
590+
v18 := hash18(s)
591+
// 100.64.y.z/14 2m+ ip4s
592+
b4 := [4]byte{
593+
rfc6598[0], // 100
594+
rfc6598[1] | uint8(v18>>16)<<6, // 64 | msb 2 bits
595+
uint8((v18 >> 8) & 0xff), // extract next 8 bits
596+
uint8(v18 & 0xff), // extract last 8 bits
597+
}
598+
599+
return netip.AddrFrom4(b4).Unmap()
600+
}
601+
569602
func (t *dnsgateway) take6Locked(q string, idx int) (*netip.Addr, bool) {
570603
k := q + key6 + strconv.Itoa(idx)
571604
if ans, ok := t.alg[k]; ok {
@@ -580,6 +613,17 @@ func (t *dnsgateway) take6Locked(q string, idx int) (*netip.Addr, bool) {
580613
}
581614
}
582615

616+
if t.chash {
617+
for i := 0; i < maxiter; i++ {
618+
genip := gen6Locked(k, i)
619+
if _, taken := t.nat[genip]; !taken {
620+
return &genip, genip.IsValid()
621+
}
622+
}
623+
log.W("alg: gen: no more IP6s (%v)", q)
624+
return nil, false
625+
}
626+
583627
gen := true
584628
// 64:ff9b:1:da19:0100.x.y.z: 281 trillion ip6s
585629
if z := t.hexes[7]; z < 65534 {
@@ -610,6 +654,28 @@ func (t *dnsgateway) take6Locked(q string, idx int) (*netip.Addr, bool) {
610654
return nil, false
611655
}
612656

657+
func gen6Locked(k string, hop int) netip.Addr {
658+
s := strconv.Itoa(hop) + k
659+
v48 := hash48(s)
660+
// 64:ff9b:1:da19:0100.x.y.z: 281 trillion ip6s
661+
a16 := [8]uint16{
662+
rfc8215a[0], // 64
663+
rfc8215a[1], // ff9b
664+
rfc8215a[2], // 1
665+
rfc8215a[3], // da19
666+
rfc8215a[4], // 0100
667+
uint16((v48 >> 32) & 0xffff), // extract the top 16 bits
668+
uint16((v48 >> 16) & 0xffff), // extract the mid 16 bits
669+
uint16(v48 & 0xffff), // extract the last 16 bits
670+
}
671+
b16 := [16]byte{}
672+
for i, hx := range a16 {
673+
i = i * 2
674+
binary.BigEndian.PutUint16(b16[i:i+2], hx)
675+
}
676+
return netip.AddrFrom16(b16)
677+
}
678+
613679
// Implements Gateway
614680
func (t *dnsgateway) withTransport(inner Transport) bool {
615681
if inner == nil {
@@ -748,3 +814,19 @@ func (t *dnsgateway) rdnsbl(algip *netip.Addr) (bcsv string) {
748814
}
749815
return
750816
}
817+
818+
// xor fold fnv to 18 bits: www.isthe.com/chongo/tech/comp/fnv
819+
func hash18(s string) uint32 {
820+
h := fnv.New64a()
821+
h.Write([]byte(s))
822+
v64 := h.Sum64()
823+
return (uint32(v64>>18) ^ uint32(v64)) & 0x3FFFF // 18 bits
824+
}
825+
826+
// xor fold fnv to 48 bits: www.isthe.com/chongo/tech/comp/fnv
827+
func hash48(s string) uint64 {
828+
h := fnv.New64a()
829+
h.Write([]byte(s))
830+
v64 := h.Sum64()
831+
return (uint64(v64>>48) ^ uint64(v64)) & 0xFFFFFFFFFFFF // 48 bits
832+
}

intra/ipn/socks5.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func NewSocks5Proxy(id string, ctl protect.Controller, po *settings.ProxyOptions
3434
tx.Dial = protect.MakeNsXDial(ctl)
3535

3636
// x.net.proxy doesn't yet support udp
37-
// https://github.com/golang/net/blob/62affa334/internal/socks/socks.go#L233
37+
// github.com/golang/net/blob/62affa334/internal/socks/socks.go#L233
3838
// if po.Auth.User and po.Auth.Password are empty strings, the upstream
3939
// socks5 server may throw err when dialing with golang/net/x/proxy;
4040
// although, txthinking/socks5 deals gracefully with empty auth strings

0 commit comments

Comments
 (0)