Skip to content

Commit 1c2355c

Browse files
committed
Implement remote DNS
This commit implements remote DNS. It introduces two new dependencies: ttlcache and dns. Remote DNS intercepts UDP DNS queries for A records on port 53. It replies with an unused IP address from an address pool, 198.18.0.0/15 by default. When obtaining a new address from the pool, tun2socks needs to memorize which name the address belongs to, so that when a client connects to the address, it can instruct the proxy to connect to the FQDN. To implement this IP to name mapping, ttlcache is used. To prevent using multiple addresses for the same name, ttlcache is also used to implement a name to IP mapping. If an IP address is already cached for a name, that address is returned instread. When building a connection, the connection metadata is inspected and if the destination address is associated with a DNS name, the proxy is instructed to use this name instead of the IP address.
1 parent 63f71e0 commit 1c2355c

File tree

14 files changed

+354
-21
lines changed

14 files changed

+354
-21
lines changed

component/remotedns/handle.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package remotedns
2+
3+
import (
4+
"net"
5+
6+
"github.com/miekg/dns"
7+
"gvisor.dev/gvisor/pkg/tcpip"
8+
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
9+
"gvisor.dev/gvisor/pkg/tcpip/stack"
10+
"gvisor.dev/gvisor/pkg/waiter"
11+
12+
"github.com/xjasonlyu/tun2socks/v2/log"
13+
M "github.com/xjasonlyu/tun2socks/v2/metadata"
14+
)
15+
16+
func RewriteMetadata(metadata *M.Metadata) bool {
17+
if !IsEnabled() {
18+
return false
19+
}
20+
dstName, found := getCachedName(metadata.DstIP)
21+
if !found {
22+
return false
23+
}
24+
metadata.DstIP = nil
25+
metadata.DstName = dstName
26+
return true
27+
}
28+
29+
func HandleDNSQuery(s *stack.Stack, id stack.TransportEndpointID, ptr *stack.PacketBuffer) bool {
30+
if !IsEnabled() {
31+
return false
32+
}
33+
34+
msg := dns.Msg{}
35+
err := msg.Unpack(ptr.Data().AsRange().ToSlice())
36+
37+
// Ignore UDP packets that are not IP queries to a recursive resolver
38+
if id.LocalPort != 53 || err != nil || len(msg.Question) != 1 || msg.Question[0].Qtype != dns.TypeA &&
39+
msg.Question[0].Qtype != dns.TypeAAAA || msg.Question[0].Qclass != dns.ClassINET || !msg.RecursionDesired ||
40+
msg.Response {
41+
return false
42+
}
43+
44+
qname := msg.Question[0].Name
45+
qtype := msg.Question[0].Qtype
46+
47+
log.Debugf("[DNS] query %s %s", dns.TypeToString[qtype], qname)
48+
49+
var ip net.IP
50+
if qtype == dns.TypeA {
51+
rr := dns.A{}
52+
ip = findOrInsertNameAndReturnIP(4, qname)
53+
if ip == nil {
54+
log.Warnf("[DNS] IP space exhausted")
55+
return true
56+
}
57+
rr.A = ip
58+
rr.Hdr.Name = qname
59+
rr.Hdr.Ttl = dnsTTL
60+
rr.Hdr.Class = dns.ClassINET
61+
rr.Hdr.Rrtype = qtype
62+
msg.Answer = append(msg.Answer, &rr)
63+
}
64+
65+
msg.Response = true
66+
msg.RecursionDesired = false
67+
msg.RecursionAvailable = true
68+
69+
var wq waiter.Queue
70+
71+
ep, err2 := s.NewEndpoint(ptr.TransportProtocolNumber, ptr.NetworkProtocolNumber, &wq)
72+
if err2 != nil {
73+
return true
74+
}
75+
defer ep.Close()
76+
77+
ep.Bind(tcpip.FullAddress{NIC: ptr.NICID, Addr: id.LocalAddress, Port: id.LocalPort})
78+
conn := gonet.NewUDPConn(&wq, ep)
79+
defer conn.Close()
80+
packed, err := msg.Pack()
81+
if err != nil {
82+
return true
83+
}
84+
_, _ = conn.WriteTo(packed, &net.UDPAddr{IP: id.RemoteAddress.AsSlice(), Port: int(id.RemotePort)})
85+
return true
86+
}

component/remotedns/iputil.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package remotedns
2+
3+
import "net"
4+
5+
func copyIP(ip net.IP) net.IP {
6+
dup := make(net.IP, len(ip))
7+
copy(dup, ip)
8+
return dup
9+
}
10+
11+
func incrementIP(ip net.IP) net.IP {
12+
result := copyIP(ip)
13+
for i := len(result) - 1; i >= 0; i-- {
14+
result[i]++
15+
if result[i] != 0 {
16+
break
17+
}
18+
}
19+
return result
20+
}
21+
22+
func getBroadcastAddress(ipnet *net.IPNet) net.IP {
23+
result := copyIP(ipnet.IP)
24+
for i := 0; i < len(ipnet.IP); i++ {
25+
result[i] |= ^ipnet.Mask[i]
26+
}
27+
return result
28+
}
29+
30+
func getNetworkAddress(ipnet *net.IPNet) net.IP {
31+
result := copyIP(ipnet.IP)
32+
for i := 0; i < len(ipnet.IP); i++ {
33+
result[i] &= ipnet.Mask[i]
34+
}
35+
return result
36+
}

component/remotedns/pool.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package remotedns
2+
3+
import (
4+
"net"
5+
"sync"
6+
"time"
7+
8+
"github.com/jellydator/ttlcache/v3"
9+
)
10+
11+
var (
12+
ipToName = ttlcache.New[string, string]()
13+
nameToIP = ttlcache.New[string, net.IP]()
14+
mutex = sync.Mutex{}
15+
16+
ip4NextAddress net.IP
17+
ip4BroadcastAddress net.IP
18+
)
19+
20+
func findOrInsertNameAndReturnIP(ipVersion int, name string) net.IP {
21+
if ipVersion != 4 {
22+
panic("Method not implemented for IPv6")
23+
}
24+
mutex.Lock()
25+
defer mutex.Unlock()
26+
var result net.IP = nil
27+
var ipnet *net.IPNet
28+
var nextAddress *net.IP
29+
var broadcastAddress net.IP
30+
if ipVersion == 4 {
31+
ipnet = ip4net
32+
nextAddress = &ip4NextAddress
33+
broadcastAddress = ip4BroadcastAddress
34+
}
35+
36+
entry := nameToIP.Get(name)
37+
if entry != nil {
38+
ip := entry.Value()
39+
ipToName.Touch(ip.String())
40+
return ip
41+
}
42+
43+
// Beginning from the pointer to the next most likely free IP, loop through the IP address space
44+
// until either a free IP is found or the space is exhausted
45+
passedBroadcastAddress := false
46+
for result == nil {
47+
if nextAddress.Equal(broadcastAddress) {
48+
*nextAddress = getNetworkAddress(ipnet)
49+
*nextAddress = incrementIP(ipnet.IP)
50+
51+
// We have seen the broadcast address twice during looping
52+
// This means that our IP address space is exhausted
53+
if passedBroadcastAddress {
54+
return nil
55+
}
56+
passedBroadcastAddress = true
57+
}
58+
59+
// Do not touch entries that exist in the cache already.
60+
hasKey := ipToName.Has((*nextAddress).String())
61+
if !hasKey {
62+
_ = ipToName.Set((*nextAddress).String(), name, time.Duration(dnsTTL)*time.Second+cacheGraceTime)
63+
_ = nameToIP.Set(name, *nextAddress, time.Duration(dnsTTL)*time.Second+cacheGraceTime)
64+
result = *nextAddress
65+
}
66+
67+
*nextAddress = incrementIP(*nextAddress)
68+
}
69+
70+
return result
71+
}
72+
73+
func getCachedName(address net.IP) (string, bool) {
74+
mutex.Lock()
75+
defer mutex.Unlock()
76+
entry := ipToName.Get(address.String())
77+
if entry == nil {
78+
return "", false
79+
}
80+
nameToIP.Touch(entry.Value())
81+
return entry.Value(), true
82+
}

component/remotedns/settings.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package remotedns
2+
3+
import (
4+
"errors"
5+
"net"
6+
"time"
7+
)
8+
9+
// Timeouts are somewhat arbitrary. For example, netcat will resolve the DNS
10+
// names upon startup and then stick to the resolved IP address. A timeout of 1
11+
// second may therefore be too low in cases where the first UDP packet is not
12+
// sent immediately.
13+
// cacheGraceTime defines how long an entry should still be retained in the cache
14+
// after being resolved by DNS.
15+
const (
16+
cacheGraceTime = 30 * time.Second
17+
)
18+
19+
var (
20+
enabled = false
21+
dnsTTL uint32 = 0
22+
ip4net *net.IPNet
23+
)
24+
25+
func IsEnabled() bool {
26+
return enabled
27+
}
28+
29+
func SetDNSTTL(timeout time.Duration) {
30+
dnsTTL = uint32(timeout.Seconds())
31+
}
32+
33+
func SetNetwork(ipnet *net.IPNet) error {
34+
leadingOnes, _ := ipnet.Mask.Size()
35+
if len(ipnet.IP) == 4 {
36+
if leadingOnes > 30 {
37+
return errors.New("IPv4 remote DNS subnet too small")
38+
}
39+
ip4net = ipnet
40+
} else {
41+
return errors.New("unsupported protocol")
42+
}
43+
return nil
44+
}
45+
46+
func Enable() {
47+
ip4NextAddress = incrementIP(getNetworkAddress(ip4net))
48+
ip4BroadcastAddress = getBroadcastAddress(ip4net)
49+
enabled = true
50+
}

core/udp.go

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,38 @@ import (
77
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
88
"gvisor.dev/gvisor/pkg/waiter"
99

10+
"github.com/xjasonlyu/tun2socks/v2/component/remotedns"
1011
"github.com/xjasonlyu/tun2socks/v2/core/adapter"
1112
"github.com/xjasonlyu/tun2socks/v2/core/option"
1213
)
1314

1415
func withUDPHandler(handle func(adapter.UDPConn)) option.Option {
1516
return func(s *stack.Stack) error {
16-
udpForwarder := udp.NewForwarder(s, func(r *udp.ForwarderRequest) {
17-
var (
18-
wq waiter.Queue
19-
id = r.ID()
20-
)
21-
ep, err := r.CreateEndpoint(&wq)
22-
if err != nil {
23-
glog.Debugf("forward udp request: %s:%d->%s:%d: %s",
24-
id.RemoteAddress, id.RemotePort, id.LocalAddress, id.LocalPort, err)
25-
return
17+
s.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, ptr *stack.PacketBuffer) bool {
18+
if remotedns.HandleDNSQuery(s, id, ptr) {
19+
return true
2620
}
2721

28-
conn := &udpConn{
29-
UDPConn: gonet.NewUDPConn(&wq, ep),
30-
id: id,
31-
}
32-
handle(conn)
22+
udpForwarder := udp.NewForwarder(s, func(r *udp.ForwarderRequest) {
23+
var (
24+
wq waiter.Queue
25+
id = r.ID()
26+
)
27+
ep, err := r.CreateEndpoint(&wq)
28+
if err != nil {
29+
glog.Debugf("forward udp request %s:%d->%s:%d: %s",
30+
id.RemoteAddress, id.RemotePort, id.LocalAddress, id.LocalPort, err)
31+
return
32+
}
33+
34+
conn := &udpConn{
35+
UDPConn: gonet.NewUDPConn(&wq, ep),
36+
id: id,
37+
}
38+
handle(conn)
39+
})
40+
return udpForwarder.HandlePacket(id, ptr)
3341
})
34-
s.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)
3542
return nil
3643
}
3744
}

engine/engine.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import (
1212
"gvisor.dev/gvisor/pkg/tcpip"
1313
"gvisor.dev/gvisor/pkg/tcpip/stack"
1414

15+
"github.com/xjasonlyu/tun2socks/v2/component/remotedns"
1516
"github.com/xjasonlyu/tun2socks/v2/core"
1617
"github.com/xjasonlyu/tun2socks/v2/core/device"
1718
"github.com/xjasonlyu/tun2socks/v2/core/option"
1819
"github.com/xjasonlyu/tun2socks/v2/dialer"
1920
"github.com/xjasonlyu/tun2socks/v2/engine/mirror"
2021
"github.com/xjasonlyu/tun2socks/v2/log"
2122
"github.com/xjasonlyu/tun2socks/v2/proxy"
23+
"github.com/xjasonlyu/tun2socks/v2/proxy/proto"
2224
"github.com/xjasonlyu/tun2socks/v2/restapi"
2325
"github.com/xjasonlyu/tun2socks/v2/tunnel"
2426
)
@@ -164,6 +166,32 @@ func restAPI(k *Key) error {
164166
return nil
165167
}
166168

169+
func remoteDNS(k *Key, proxy proxy.Proxy) (err error) {
170+
if !k.RemoteDNS {
171+
return
172+
}
173+
if proxy.Proto() != proto.Socks5 && proxy.Proto() != proto.HTTP && proxy.Proto() != proto.Shadowsocks &&
174+
proxy.Proto() != proto.Socks4 {
175+
return errors.New("remote DNS not supported with this proxy protocol")
176+
}
177+
178+
_, ipnet, err := net.ParseCIDR(k.RemoteDNSNetIPv4)
179+
if err != nil {
180+
return err
181+
}
182+
183+
err = remotedns.SetNetwork(ipnet)
184+
if err != nil {
185+
return err
186+
}
187+
188+
remotedns.SetDNSTTL(k.RemoteDNSTTL)
189+
190+
remotedns.Enable()
191+
log.Infof("[DNS] Remote DNS enabled")
192+
return
193+
}
194+
167195
func netstack(k *Key) (err error) {
168196
if k.Proxy == "" {
169197
return errors.New("empty proxy")
@@ -238,5 +266,11 @@ func netstack(k *Key) (err error) {
238266
_defaultDevice.Type(), _defaultDevice.Name(),
239267
_defaultProxy.Proto(), _defaultProxy.Addr(),
240268
)
269+
270+
err = remoteDNS(k, _defaultProxy)
271+
if err != nil {
272+
return err
273+
}
274+
241275
return nil
242276
}

engine/key.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,7 @@ type Key struct {
1717
TUNPreUp string `yaml:"tun-pre-up"`
1818
TUNPostUp string `yaml:"tun-post-up"`
1919
UDPTimeout time.Duration `yaml:"udp-timeout"`
20+
RemoteDNS bool `yaml:"remote-dns"`
21+
RemoteDNSNetIPv4 string `yaml:"remote-dns-net-ipv4"`
22+
RemoteDNSTTL time.Duration `yaml:"remote-dns-timeout"`
2023
}

0 commit comments

Comments
 (0)