Implement Socks5 support (#317)

* Implement Socks5 support

* Support SOCKS5 for DoT

* Socks5 for DoH

* Return interface, not nil pointer of specific type

* Support resolving DNS server names locally instead of through the proxy

* Resolve IP4 only

* Fix go.mod after rebase

* Support LocalAddr directly in the SOCKS5 dialer

* Update docs for SOCKS5
This commit is contained in:
Frank Olbricht
2023-10-18 09:12:34 +02:00
committed by GitHub
parent 82acf91c20
commit 2e0610668b
13 changed files with 317 additions and 30 deletions

View File

@@ -20,6 +20,7 @@ Features:
- EDNS0 Client Subnet (ECS) manipulation ([RFC7871](https://tools.ietf.org/html/rfc7871))
- Support for bootstrap addresses to avoid the initial service name lookup
- Support for 0-RTT Quic queries if the upstream server supports it
- SOCKS5 proxy support
- Optional metrics export (expvar) to support monitoring and graphing
- Written in Go - Platform independent

View File

@@ -50,7 +50,13 @@ type resolver struct {
BootstrapAddr string `toml:"bootstrap-address"`
LocalAddr string `toml:"local-address"`
EDNS0UDPSize uint16 `toml:"edns0-udp-size"` // UDP resolver option
QueryTimeout int `toml:"query-timeout"` // Query timout in seconds
QueryTimeout int `toml:"query-timeout"` // Query timeout in seconds
// Proxy configuration
Socks5Address string `toml:"socks5-address"`
Socks5Username string `toml:"socks5-username"`
Socks5Password string `toml:"socks5-password"`
Socks5ResolveLocal bool `toml:"socks5-resolve-local"` // Resolve DNS server address locally (i.e. bootstrap-resolver), not on the SOCK5 proxy
}
// DoH-specific resolver options

View File

@@ -0,0 +1,13 @@
# DoH coonfiguration that connects to the upstream server via SOCKS5 proxy.
[resolvers.cloudflare-doh]
address = "https://cloudflare-dns.com/dns-query"
protocol = "doh"
socks5-address = "127.0.0.1:1080"
socks5-username = "test"
socks5-password = "test"
[listeners.local-udp]
address = "127.0.0.1:53"
protocol = "udp"
resolver = "cloudflare-doh"

View File

@@ -0,0 +1,13 @@
# DoT coonfiguration that connects to the upstream server via SOCKS5 proxy.
[resolvers.cloudflare-dot]
address = "1.1.1.1:853"
protocol = "dot"
socks5-address = "127.0.0.1:1080"
socks5-username = "test"
socks5-password = "test"
[listeners.local-udp]
address = "127.0.0.1:53"
protocol = "udp"
resolver = "cloudflare-dot"

View File

@@ -0,0 +1,13 @@
# Simple DNS queries routed through a SOCKS5 proxy.
[resolvers.cloudflare-udp]
address = "1.1.1.1:53"
protocol = "udp"
socks5-address = "127.0.0.1:1080"
socks5-username = "test"
socks5-password = "test"
[listeners.local-udp]
address = "127.0.0.1:53"
protocol = "udp"
resolver = "cloudflare-udp"

View File

@@ -42,6 +42,7 @@ func instantiateResolver(id string, r resolver, resolvers map[string]rdns.Resolv
LocalAddr: net.ParseIP(r.LocalAddr),
TLSConfig: tlsConfig,
QueryTimeout: time.Duration(r.QueryTimeout) * time.Second,
Dialer: socks5DialerFromConfig(r),
}
resolvers[id], err = rdns.NewDoTClient(id, r.Address, opt)
if err != nil {
@@ -79,6 +80,7 @@ func instantiateResolver(id string, r resolver, resolvers map[string]rdns.Resolv
Transport: r.Transport,
LocalAddr: net.ParseIP(r.LocalAddr),
QueryTimeout: time.Duration(r.QueryTimeout) * time.Second,
Dialer: socks5DialerFromConfig(r),
}
resolvers[id], err = rdns.NewDoHClient(id, r.Address, opt)
if err != nil {
@@ -91,6 +93,7 @@ func instantiateResolver(id string, r resolver, resolvers map[string]rdns.Resolv
LocalAddr: net.ParseIP(r.LocalAddr),
UDPSize: r.EDNS0UDPSize,
QueryTimeout: time.Duration(r.QueryTimeout) * time.Second,
Dialer: socks5DialerFromConfig(r),
}
resolvers[id], err = rdns.NewDNSClient(id, r.Address, r.Protocol, opt)
if err != nil {
@@ -101,3 +104,21 @@ func instantiateResolver(id string, r resolver, resolvers map[string]rdns.Resolv
}
return nil
}
// Returns a dialer if a socks5 proxy is configured, nil otherwise
func socks5DialerFromConfig(cfg resolver) rdns.Dialer {
if cfg.Socks5Address == "" {
return nil
}
r := rdns.NewSocks5Dialer(
cfg.Socks5Address,
rdns.Socks5DialerOptions{
Username: cfg.Socks5Username,
Password: cfg.Socks5Password,
TCPTimeout: 0,
UDPTimeout: 5 * time.Second,
ResolveLocal: cfg.Socks5ResolveLocal,
LocalAddr: net.ParseIP(cfg.LocalAddr),
})
return r
}

View File

@@ -3,6 +3,7 @@ package rdns
import (
"crypto/tls"
"net"
"strings"
"time"
"github.com/miekg/dns"
@@ -18,6 +19,10 @@ type DNSClient struct {
opt DNSClientOptions
}
type Dialer interface {
Dial(net string, address string) (net.Conn, error)
}
type DNSClientOptions struct {
// Local IP to use for outbound connections. If nil, a local address is chosen.
LocalAddr net.IP
@@ -27,6 +32,9 @@ type DNSClientOptions struct {
UDPSize uint16
QueryTimeout time.Duration
// Optional dialer, e.g. proxy
Dialer Dialer
}
var _ Resolver = &DNSClient{}
@@ -37,21 +45,11 @@ func NewDNSClient(id, endpoint, network string, opt DNSClientOptions) (*DNSClien
if err := validEndpoint(endpoint); err != nil {
return nil, err
}
// Use a custom dialer if a local address was provided
var dialer *net.Dialer
if opt.LocalAddr != nil {
switch network {
case "tcp":
dialer = &net.Dialer{LocalAddr: &net.TCPAddr{IP: opt.LocalAddr}, Timeout: opt.QueryTimeout}
case "udp":
dialer = &net.Dialer{LocalAddr: &net.UDPAddr{IP: opt.LocalAddr}, Timeout: opt.QueryTimeout}
}
}
client := &dns.Client{
client := GenericDNSClient{
Net: network,
Dialer: dialer,
Dialer: opt.Dialer,
TLSConfig: &tls.Config{},
UDPSize: 4096,
LocalAddr: opt.LocalAddr,
Timeout: opt.QueryTimeout,
}
return &DNSClient{
@@ -83,3 +81,93 @@ func (d *DNSClient) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
func (d *DNSClient) String() string {
return d.id
}
// GenericDNSClient is a workaround for dns.Client not supporting custom dialers
// (only *net.Dialer) which prevents the use of proxies. It implements the same
// Dial functionality, while supporting custom dialers.
type GenericDNSClient struct {
Dialer Dialer
Net string
TLSConfig *tls.Config
LocalAddr net.IP
Timeout time.Duration
}
func (d GenericDNSClient) Dial(address string) (*dns.Conn, error) {
network := d.Net
// If we want TLS on it, perform the handshake
useTLS := strings.HasPrefix(network, "tcp") && strings.HasSuffix(network, "-tls")
network = strings.TrimSuffix(network, "-tls")
dialer := d.Dialer
if dialer == nil {
// Use a custom dialer if a local address was provided
if d.LocalAddr != nil {
switch network {
case "tcp":
dialer = &net.Dialer{LocalAddr: &net.TCPAddr{IP: d.LocalAddr}, Timeout: d.Timeout}
case "udp":
dialer = &net.Dialer{LocalAddr: &net.UDPAddr{IP: d.LocalAddr}, Timeout: d.Timeout}
}
} else {
dialer = &net.Dialer{}
}
}
var (
conn = &dns.Conn{
UDPSize: 4096,
}
err error
)
// Open a raw connection
conn.Conn, err = dialer.Dial(network, address)
if err != nil {
return nil, err
}
// Trick dns.Conn.ReadMsg() into thinking this is a packet connection (udp) so it
// correctly handles any length-prefixes
if network == "udp" {
conn.Conn = packetConnWrapper{conn.Conn}
}
if useTLS {
hostname, _, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
tlsConfig := d.TLSConfig
if tlsConfig == nil {
tlsConfig = &tls.Config{}
}
// Make sure a servername is set
if tlsConfig.ServerName == "" {
c := tlsConfig.Clone()
c.ServerName = hostname
tlsConfig = c
}
conn.Conn = tls.Client(conn.Conn, tlsConfig)
}
return conn, nil
}
// packetConnWrapper is another workaround for dns.Conn which checks if the Conn
// it has implements net.PacketConn and based on that distinguishes between a UDP
// connection (don't need length prefix) and TCP (need length prefix). This doesn't
// actually implement these, but dns.Conn.ReadMsg() doesn't use them either.
type packetConnWrapper struct {
net.Conn
}
var _ net.PacketConn = packetConnWrapper{}
func (c packetConnWrapper) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
panic("not implemented")
}
func (c packetConnWrapper) WriteTo(p []byte, addr net.Addr) (n int, err error) {
panic("not implemented")
}

View File

@@ -44,6 +44,7 @@
- [DNS-over-DTLS](#DNS-over-DTLS-Resolver)
- [DNS-over-QUIC](#DNS-over-QUIC-Resolver)
- [Bootstrap Resolver](#Bootstrap-Resolver)
- [SOCKS5 Proxy Support](#SOCKS5-Proxy-Support)
## Overview
@@ -1578,3 +1579,29 @@ protocol = "dot"
```
Example config files: [bootstrap-resolver.toml](../cmd/routedns/example-config/bootstrap-resolver.toml), [use-case-6.toml](../cmd/routedns/example-config/use-case-6.toml)
### SOCKS5 Proxy Support
Several resolver types support connecting to upstream servers through a SOCKS5 proxy. This includes:
- [Plain DNS](#Plain-DNS-Resolver)
- [DNS-over-TLS](#DNS-over-TLS-Resolver)
- [DNS-over-HTTPS](#DNS-over-HTTPS-Resolver)
If SOCKS5 is available, the following options can be used to configure it:
- `socks5-address` - SOCKS5 server address, including port.
- `socks5-username` - SOCKS5 server username.
- `socks5-password` - SOCKS5 server password.
- `socks5-resolve-local` - Experimental: Resolve the upstream DNS server name locally before connecting through the proxy.
Examples:
```toml
[resolvers.cloudflare-doh]
address = "https://cloudflare-dns.com/dns-query"
protocol = "doh"
socks5-address = "1.2.3.4:1080"
socks5-username = "test"
socks5-password = "test"
```

View File

@@ -41,6 +41,9 @@ type DoHClientOptions struct {
TLSConfig *tls.Config
QueryTimeout time.Duration
// Optional dialer, e.g. proxy
Dialer Dialer
}
// DoHClient is a DNS-over-HTTP resolver with support fot HTTP/2.
@@ -235,7 +238,7 @@ func dohTcpTransport(opt DoHClientOptions) (http.RoundTripper, error) {
}
// Use a custom dialer if a bootstrap address or local address was provided
if opt.BootstrapAddr != "" || opt.LocalAddr != nil {
if opt.BootstrapAddr != "" || opt.LocalAddr != nil || opt.Dialer != nil {
d := net.Dialer{LocalAddr: &net.TCPAddr{IP: opt.LocalAddr}}
tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
if opt.BootstrapAddr != "" {
@@ -245,6 +248,9 @@ func dohTcpTransport(opt DoHClientOptions) (http.RoundTripper, error) {
}
addr = net.JoinHostPort(opt.BootstrapAddr, port)
}
if opt.Dialer != nil {
return opt.Dialer.Dial(network, addr)
}
return d.DialContext(ctx, network, addr)
}
}

View File

@@ -30,6 +30,9 @@ type DoTClientOptions struct {
TLSConfig *tls.Config
QueryTimeout time.Duration
// Optional dialer, e.g. proxy
Dialer Dialer
}
var _ Resolver = &DoTClient{}
@@ -40,15 +43,11 @@ func NewDoTClient(id, endpoint string, opt DoTClientOptions) (*DoTClient, error)
return nil, err
}
// Use a custom dialer if a local address was provided
var dialer *net.Dialer
if opt.LocalAddr != nil {
dialer = &net.Dialer{LocalAddr: &net.TCPAddr{IP: opt.LocalAddr}}
}
client := &dns.Client{
client := GenericDNSClient{
Net: "tcp-tls",
TLSConfig: opt.TLSConfig,
Dialer: dialer,
Dialer: opt.Dialer,
LocalAddr: opt.LocalAddr,
}
// If a bootstrap address was provided, we need to use the IP for the connection but the
// hostname in the TLS handshake. The DNS library doesn't support custom dialers, so

5
go.mod
View File

@@ -7,7 +7,7 @@ require (
github.com/RackSec/srslog v0.0.0-20180709174129-a4725f04ec91
github.com/heimdalr/dag v1.2.1
github.com/jtacoma/uritemplates v1.0.0
github.com/miekg/dns v1.1.50
github.com/miekg/dns v1.1.51
github.com/oschwald/maxminddb-golang v1.10.0
github.com/pion/dtls/v2 v2.2.4
github.com/pkg/errors v0.9.1
@@ -16,6 +16,7 @@ require (
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.1
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301
golang.org/x/net v0.17.0
)
@@ -31,6 +32,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/transport/v2 v2.0.0 // indirect
github.com/pion/udp v0.1.4 // indirect
@@ -38,6 +40,7 @@ require (
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.3.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/exp v0.0.0-20221227203929-1b447090c38c // indirect
golang.org/x/mod v0.10.0 // indirect

20
go.sum
View File

@@ -40,13 +40,15 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg=
github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
@@ -86,6 +88,10 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0=
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM=
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -98,30 +104,30 @@ golang.org/x/exp v0.0.0-20221227203929-1b447090c38c h1:Govq2W3bnHJimHT2ium65kXcI
golang.org/x/exp v0.0.0-20221227203929-1b447090c38c/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -133,10 +139,10 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
@@ -145,8 +151,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

91
socks5.go Normal file
View File

@@ -0,0 +1,91 @@
package rdns
import (
"context"
"net"
"sync"
"time"
"github.com/txthinking/socks5"
)
type Socks5Dialer struct {
*socks5.Client
opt Socks5DialerOptions
once sync.Once
addr string
}
type Socks5DialerOptions struct {
Username string
Password string
UDPTimeout time.Duration
TCPTimeout time.Duration
LocalAddr net.IP
// When the resolver is configured with a name, not an IP, e.g. one.one.one.one:53
// this setting will resolve that name locally rather than on the SOCKS proxy. The
// name will be resolved either on the local system, or via the bootstrap-resolver
// if one is setup.
ResolveLocal bool
}
var _ Dialer = (*Socks5Dialer)(nil)
func NewSocks5Dialer(addr string, opt Socks5DialerOptions) *Socks5Dialer {
client, _ := socks5.NewClient(
addr,
opt.Username,
opt.Password,
int(opt.TCPTimeout.Seconds()),
int(opt.UDPTimeout.Seconds()),
)
return &Socks5Dialer{Client: client, opt: opt}
}
func (d *Socks5Dialer) Dial(network string, address string) (net.Conn, error) {
d.once.Do(func() {
d.addr = address
// If the address uses a hostname and ResolveLocal is enabled, lookup
// the IP for it locally and use that when talking to the proxy going
// forward. This avoids the DNS server's address leaking out from the
// proxy.
if d.opt.ResolveLocal {
host, port, err := net.SplitHostPort(address)
if err != nil {
Log.WithError(err).Error("failed to parse socks5 address")
return
}
Log.WithField("addr", host).Debug("resolving dns server locally")
ip := net.ParseIP(host)
if ip != nil {
// Already an IP
return
}
timeout := d.opt.UDPTimeout
if timeout == 0 {
timeout = 5 * time.Second
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ips, err := net.DefaultResolver.LookupIP(ctx, "ip4", host)
if err != nil {
Log.WithError(err).Errorf("failed to lookup %q locally", host)
return
}
if len(ips) == 0 {
Log.WithError(err).Error("failed to resolve dns server locally, forwarding to socks5 proxy")
return
}
d.addr = net.JoinHostPort(ips[0].String(), port)
}
})
if d.opt.LocalAddr != nil {
return d.Client.DialWithLocalAddr(network, d.opt.LocalAddr.String(), d.addr, nil)
}
return d.Client.Dial(network, d.addr)
}