diff --git a/README.md b/README.md index dfc245c..76ca3ab 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/routedns/config.go b/cmd/routedns/config.go index 16abc16..1726caa 100644 --- a/cmd/routedns/config.go +++ b/cmd/routedns/config.go @@ -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 diff --git a/cmd/routedns/example-config/socks5-doh.toml b/cmd/routedns/example-config/socks5-doh.toml new file mode 100644 index 0000000..0b8aa92 --- /dev/null +++ b/cmd/routedns/example-config/socks5-doh.toml @@ -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" diff --git a/cmd/routedns/example-config/socks5-dot.toml b/cmd/routedns/example-config/socks5-dot.toml new file mode 100644 index 0000000..17e75a4 --- /dev/null +++ b/cmd/routedns/example-config/socks5-dot.toml @@ -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" diff --git a/cmd/routedns/example-config/socks5-udp.toml b/cmd/routedns/example-config/socks5-udp.toml new file mode 100644 index 0000000..5fb5c30 --- /dev/null +++ b/cmd/routedns/example-config/socks5-udp.toml @@ -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" diff --git a/cmd/routedns/resolver.go b/cmd/routedns/resolver.go index 6b6fa4c..b758896 100644 --- a/cmd/routedns/resolver.go +++ b/cmd/routedns/resolver.go @@ -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 +} diff --git a/dnsclient.go b/dnsclient.go index a19fa1a..54afc25 100644 --- a/dnsclient.go +++ b/dnsclient.go @@ -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") +} diff --git a/doc/configuration.md b/doc/configuration.md index 3aafe29..8eed4cb 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -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" +``` diff --git a/dohclient.go b/dohclient.go index 6c72986..091e16f 100644 --- a/dohclient.go +++ b/dohclient.go @@ -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) } } diff --git a/dotclient.go b/dotclient.go index 22df881..02f5acc 100644 --- a/dotclient.go +++ b/dotclient.go @@ -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 diff --git a/go.mod b/go.mod index 3f8a130..6e7f29a 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index dda6425..0d3a3a2 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/socks5.go b/socks5.go new file mode 100644 index 0000000..1cb3b72 --- /dev/null +++ b/socks5.go @@ -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) +}