diff --git a/README.md b/README.md index 6f0e1a1..4e20ee2 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ eu3.sec-tunnel.com,77.111.244.22,443 | api-password | String | SurfEasy API password (default "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II") | | api-user-agent | String | user agent reported to SurfEasy API (default "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0") | | bind-address | String | proxy listen address (default "127.0.0.1:18080") | -| bootstrap-dns | String | Comma-separated list of DNS/DoH/DoT/DoQ resolvers for initial discovery of SurfEasy API address. See https://github.com/ameshkov/dnslookup/ for upstream DNS URL format. Examples: `https://1.1.1.1/dns-query`, `quic://dns.adguard.com` (default `https://1.1.1.3/dns-query,https://8.8.8.8/dns-query,https://dns.google/dns-query,https://security.cloudflare-dns.com/dns-query,https://fidelity.vm-0.com/q,https://wikimedia-dns.org/dns-query,https://dns.adguard-dns.com/dns-query,https://dns.quad9.net/dns-query,https://doh.cleanbrowsing.org/doh/adult-filter/`) | +| bootstrap-dns | String | Comma-separated list of DNS/DoH/DoT resolvers for initial discovery of SurfEasy API address. Supported schemes are: `dns://`, `https://`, `tls://`, `tcp://`. Examples: `https://1.1.1.1/dns-query`, `tls://9.9.9.9:853` (default `https://1.1.1.3/dns-query,https://8.8.8.8/dns-query,https://dns.google/dns-query,https://security.cloudflare-dns.com/dns-query,https://fidelity.vm-0.com/q,https://wikimedia-dns.org/dns-query,https://dns.adguard-dns.com/dns-query,https://dns.quad9.net/dns-query,https://doh.cleanbrowsing.org/doh/adult-filter/`) | | cafile | String | use custom CA certificate bundle file | | certchain-workaround | Boolean | add bundled cross-signed intermediate cert to certchain to make it check out on old systems (default true) | | country | String | desired proxy location (default "EU") | diff --git a/dialer/resolver.go b/dialer/resolver.go index 84e2c0a..0ba2ae2 100644 --- a/dialer/resolver.go +++ b/dialer/resolver.go @@ -3,55 +3,10 @@ package dialer import ( "context" "fmt" - "io" "net" "net/netip" - "time" - - "github.com/AdguardTeam/dnsproxy/upstream" - "github.com/hashicorp/go-multierror" ) -type Resolver struct { - resolvers upstream.ParallelResolver - timeout time.Duration -} - -func NewResolver(addresses []string, timeout time.Duration) (*Resolver, error) { - resolvers := make([]upstream.Resolver, 0, len(addresses)) - opts := &upstream.Options{ - Timeout: timeout, - } - for _, addr := range addresses { - u, err := upstream.AddressToUpstream(addr, opts) - if err != nil { - return nil, fmt.Errorf("unable to construct upstream resolver from string %q: %w", - addr, err) - } - resolvers = append(resolvers, &upstream.UpstreamResolver{Upstream: u}) - } - return &Resolver{ - resolvers: resolvers, - timeout: timeout, - }, nil -} - -func (r *Resolver) LookupNetIP(ctx context.Context, network string, host string) (addrs []netip.Addr, err error) { - return r.resolvers.LookupNetIP(ctx, network, host) -} - -func (r *Resolver) Close() error { - var res error - for _, resolver := range r.resolvers { - if closer, ok := resolver.(io.Closer); ok { - if err := closer.Close(); err != nil { - res = multierror.Append(res, err) - } - } - } - return res -} - type LookupNetIPer interface { LookupNetIP(context.Context, string, string) ([]netip.Addr, error) } diff --git a/go.mod b/go.mod index 3e68ec8..6dad3e6 100644 --- a/go.mod +++ b/go.mod @@ -5,31 +5,11 @@ go 1.24.1 toolchain go1.24.2 require ( - github.com/AdguardTeam/dnsproxy v0.75.2 github.com/Snawoot/go-http-digest-auth-client v1.1.3 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/hashicorp/go-multierror v1.1.1 + github.com/ncruces/go-dns v1.2.7 golang.org/x/net v0.39.0 ) -require ( - github.com/AdguardTeam/golibs v0.32.7 // indirect - github.com/ameshkov/dnscrypt/v2 v2.4.0 // indirect - github.com/ameshkov/dnsstamps v1.0.3 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/miekg/dns v1.1.65 // indirect - github.com/onsi/ginkgo/v2 v2.23.4 // indirect - github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.50.1 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect - go.uber.org/mock v0.5.1 // indirect - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect - golang.org/x/tools v0.32.0 // indirect -) +require github.com/hashicorp/errwrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 6b72ed5..bb97e94 100644 --- a/go.sum +++ b/go.sum @@ -1,69 +1,13 @@ -github.com/AdguardTeam/dnsproxy v0.75.2 h1:bciOkzQh/GG8vcZGdFn6+rS3pu+2Npt9tbA4bNA/rsc= -github.com/AdguardTeam/dnsproxy v0.75.2/go.mod h1:U/ouLftmXMIrkTAf8JepqbPuoQzsbXJo0Vxxn+LAdgA= -github.com/AdguardTeam/golibs v0.32.7 h1:3dmGlAVgmvquCCwHsvEl58KKcRAK3z1UnjMnwSIeDH4= -github.com/AdguardTeam/golibs v0.32.7/go.mod h1:bE8KV1zqTzgZjmjFyBJ9f9O5DEKO717r7e57j1HclJA= github.com/Snawoot/go-http-digest-auth-client v1.1.3 h1:Xd/SNBuIUJqotzmxRpbXovBJxmlVZOT19IZZdMdrJ0Q= github.com/Snawoot/go-http-digest-auth-client v1.1.3/go.mod h1:WiwNiPXTRGyjTGpBtSQJlM2wDPRRPpFGhMkMWpV4uqg= -github.com/ameshkov/dnscrypt/v2 v2.4.0 h1:if6ZG2cuQmcP2TwSY+D0+8+xbPfoatufGlOQTMNkI9o= -github.com/ameshkov/dnscrypt/v2 v2.4.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI= -github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= -github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc= -github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= -github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q= -github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= -go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +github.com/ncruces/go-dns v1.2.7 h1:NMA7vFqXUl+nBhGFlleLyo2ni3Lqv3v+qFWZidzRemI= +github.com/ncruces/go-dns v1.2.7/go.mod h1:SqmhVMBd8Wr7hsu3q6yTt6/Jno/xLMrbse/JLOMBo1Y= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handler/socks.go b/handler/socks.go index e19754d..a37c427 100644 --- a/handler/socks.go +++ b/handler/socks.go @@ -1,9 +1,10 @@ package handler import ( + "log" + "github.com/Snawoot/opera-proxy/dialer" "github.com/armon/go-socks5" - "log" ) func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger) (*socks5.Server, error) { diff --git a/main.go b/main.go index ce67b40..8524719 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ import ( "github.com/Snawoot/opera-proxy/dialer" "github.com/Snawoot/opera-proxy/handler" clog "github.com/Snawoot/opera-proxy/log" + "github.com/Snawoot/opera-proxy/resolver" se "github.com/Snawoot/opera-proxy/seclient" ) @@ -142,9 +143,9 @@ func parse_args() *CLIArgs { flag.StringVar(&args.apiPassword, "api-password", "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II", "SurfEasy API password") flag.StringVar(&args.apiAddress, "api-address", "", fmt.Sprintf("override IP address of %s", API_DOMAIN)) flag.Var(args.bootstrapDNS, "bootstrap-dns", - "comma-separated list of DNS/DoH/DoT/DoQ resolvers for initial discovery of SurfEasy API address. "+ - "See https://github.com/ameshkov/dnslookup/ for upstream DNS URL format. "+ - "Examples: https://1.1.1.1/dns-query,quic://dns.adguard.com") + "comma-separated list of DNS/DoH/DoT resolvers for initial discovery of SurfEasy API address. "+ + "Supported schemes are: dns://, https://, tls://. "+ + "Examples: https://1.1.1.1/dns-query,tls://9.9.9.9:853") flag.DurationVar(&args.refresh, "refresh", 4*time.Hour, "login refresh interval") flag.DurationVar(&args.refreshRetry, "refresh-retry", 5*time.Second, "login refresh retry interval") flag.IntVar(&args.initRetries, "init-retries", 0, "number of attempts for initialization steps, zero for unlimited retry") @@ -220,12 +221,11 @@ func run() int { mainLogger.Info("Using fixed API host IP address = %s", args.apiAddress) seclientDialer = dialer.NewFixedDialer(args.apiAddress, d) } else if len(args.bootstrapDNS.values) > 0 { - resolver, err := dialer.NewResolver(args.bootstrapDNS.values, args.timeout) + resolver, err := resolver.FastFromURLs(args.bootstrapDNS.values...) if err != nil { mainLogger.Critical("Unable to instantiate DNS resolver: %v", err) return 4 } - defer resolver.Close() seclientDialer = dialer.NewResolvingDialer(resolver, d) } diff --git a/resolver/fabric.go b/resolver/fabric.go new file mode 100644 index 0000000..52c5cd2 --- /dev/null +++ b/resolver/fabric.go @@ -0,0 +1,44 @@ +package resolver + +import ( + "errors" + "net" + "net/url" + "strings" + + "github.com/ncruces/go-dns" +) + +func FromURL(u string) (*net.Resolver, error) { + parsed, err := url.Parse(u) + if err != nil { + return nil, err + } + switch strings.ToLower(parsed.Scheme) { + case "", "dns": + host := parsed.Hostname() + port := parsed.Port() + if port == "" { + port = "53" + } + return NewPlainResolver(net.JoinHostPort(host, port)), nil + case "tcp": + host := parsed.Hostname() + port := parsed.Port() + if port == "" { + port = "53" + } + return NewTCPResolver(net.JoinHostPort(host, port)), nil + case "http", "https": + return dns.NewDoHResolver(u) + case "tls": + host := parsed.Hostname() + port := parsed.Port() + if port == "" { + port = "853" + } + return dns.NewDoTResolver(net.JoinHostPort(host, port)) + default: + return nil, errors.New("not implemented") + } +} diff --git a/resolver/fast.go b/resolver/fast.go new file mode 100644 index 0000000..3bef339 --- /dev/null +++ b/resolver/fast.go @@ -0,0 +1,72 @@ +package resolver + +import ( + "context" + "fmt" + "net/netip" + + "github.com/hashicorp/go-multierror" +) + +type LookupNetIPer interface { + LookupNetIP(context.Context, string, string) ([]netip.Addr, error) +} + +type FastResolver struct { + upstreams []LookupNetIPer +} + +type lookupReply struct { + addrs []netip.Addr + err error +} + +func FastFromURLs(urls ...string) (*FastResolver, error) { + resolvers := make([]LookupNetIPer, 0, len(urls)) + for i, u := range urls { + res, err := FromURL(u) + if err != nil { + return nil, fmt.Errorf("unable to construct resolver #%d (%q): %w", i, u, err) + } + resolvers = append(resolvers, res) + } + return NewFastResolver(resolvers...), nil +} + +func NewFastResolver(resolvers ...LookupNetIPer) *FastResolver { + return &FastResolver{ + upstreams: resolvers, + } +} + +func (r FastResolver) LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error) { + ctx, cl := context.WithCancel(ctx) + drain := make(chan lookupReply, len(r.upstreams)) + for _, res := range r.upstreams { + go func(res LookupNetIPer) { + addrs, err := res.LookupNetIP(ctx, network, host) + drain <- lookupReply{addrs, err} + }(res) + } + + i := 0 + var resAddrs []netip.Addr + var resErr error + for ; i < len(r.upstreams); i++ { + pair := <-drain + if pair.err != nil { + resErr = multierror.Append(resErr, pair.err) + } else { + cl() + resAddrs = pair.addrs + resErr = nil + break + } + } + go func() { + for i = i + 1; i < len(r.upstreams); i++ { + <-drain + } + }() + return resAddrs, resErr +} diff --git a/resolver/plain.go b/resolver/plain.go new file mode 100644 index 0000000..99726d5 --- /dev/null +++ b/resolver/plain.go @@ -0,0 +1,35 @@ +package resolver + +import ( + "context" + "net" +) + +func NewPlainResolver(addr string) *net.Resolver { + return &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, _ string) (net.Conn, error) { + return (&net.Dialer{ + Resolver: &net.Resolver{}, + }).DialContext(ctx, network, addr) + }, + } +} + +func NewTCPResolver(addr string) *net.Resolver { + return &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, _ string) (net.Conn, error) { + dnet := "tcp" + switch (network) { + case "udp4": + dnet = "tcp4" + case "udp6": + dnet = "tcp6" + } + return (&net.Dialer{ + Resolver: &net.Resolver{}, + }).DialContext(ctx, dnet, addr) + }, + } +}