Skip to content

Commit e4ea2f8

Browse files
authored
Merge pull request #146 from SenseUnit/ext-resolver
External resolver integration
2 parents 913f3b4 + 674b986 commit e4ea2f8

File tree

8 files changed

+330
-3
lines changed

8 files changed

+330
-3
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ Simple, scriptable, secure forward proxy.
2525
* Optional AEAD encryption layer for cache
2626
* Per-user bandwidth limits
2727
* HTTP/2 support, both server and client, including h2c support
28-
* Optional DNS cache
28+
* Advanced DNS support
29+
* Plain DNS
30+
* DNS-over-HTTPS
31+
* DNS-over-TLS
32+
* System-provided DNS
33+
* Competitive parallel resolving using any of above
34+
* Optional DNS cache
2935
* Resilient to DPI (including active probing, see `hidden_domain` option for authentication providers)
3036
* Connecting via upstream HTTP(S)/SOCKS5 proxies (proxy chaining)
3137
* Optional parroting of TLS fingerprints of popular software such as web browsers.
@@ -493,6 +499,10 @@ Usage of /home/user/go/bin/dumbproxy:
493499
timeout for shared resolves of DNS cache (default 5s)
494500
-dns-cache-ttl duration
495501
enable DNS cache with specified fixed TTL
502+
-dns-prefer-address value
503+
address resolution preference (none/ipv4/ipv6) (default ipv4)
504+
-dns-server value
505+
nameserver specification (udp://..., tcp://..., https://..., tls://..., doh://..., dot://..., default://). Option can be used multiple times for parallel use of multiple nameservers. Empty string resets the list
496506
-hmac-genkey
497507
generate hex-encoded HMAC signing key of optimal length
498508
-hmac-sign

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/hashicorp/go-multierror v1.1.1
1212
github.com/jellydator/ttlcache/v3 v3.4.0
1313
github.com/libp2p/go-reuseport v0.4.0
14+
github.com/ncruces/go-dns v1.2.7
1415
github.com/redis/go-redis/v9 v9.13.0
1516
github.com/refraction-networking/utls v1.8.0
1617
github.com/tg123/go-htpasswd v1.2.4

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzh
4141
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
4242
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
4343
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
44+
github.com/ncruces/go-dns v1.2.7 h1:NMA7vFqXUl+nBhGFlleLyo2ni3Lqv3v+qFWZidzRemI=
45+
github.com/ncruces/go-dns v1.2.7/go.mod h1:SqmhVMBd8Wr7hsu3q6yTt6/Jno/xLMrbse/JLOMBo1Y=
4446
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
4547
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
4648
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

main.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"github.com/SenseUnit/dumbproxy/forward"
4343
"github.com/SenseUnit/dumbproxy/handler"
4444
clog "github.com/SenseUnit/dumbproxy/log"
45+
"github.com/SenseUnit/dumbproxy/resolver"
4546
"github.com/SenseUnit/dumbproxy/tlsutil"
4647
proxyproto "github.com/pires/go-proxyproto"
4748

@@ -196,6 +197,25 @@ type autocertCache struct {
196197

197198
const envCacheEncKey = "DUMBPROXY_CACHE_ENC_KEY"
198199

200+
type dnsPreferenceArg resolver.Preference
201+
202+
func (a *dnsPreferenceArg) String() string {
203+
return resolver.Preference(*a).String()
204+
}
205+
206+
func (a *dnsPreferenceArg) Set(s string) error {
207+
p, err := resolver.ParsePreference(s)
208+
if err != nil {
209+
return nil
210+
}
211+
*a = dnsPreferenceArg(p)
212+
return nil
213+
}
214+
215+
func (a *dnsPreferenceArg) Value() resolver.Preference {
216+
return resolver.Preference(*a)
217+
}
218+
199219
type bindSpec struct {
200220
af string
201221
address string
@@ -240,6 +260,8 @@ type CLIArgs struct {
240260
bwBurst int64
241261
bwBuckets uint
242262
bwSeparate bool
263+
dnsServers []string
264+
dnsPreferAddress dnsPreferenceArg
243265
dnsCacheTTL time.Duration
244266
dnsCacheNegTTL time.Duration
245267
dnsCacheTimeout time.Duration
@@ -275,6 +297,7 @@ func parse_args() CLIArgs {
275297
address: ":8080",
276298
af: "tcp",
277299
},
300+
dnsPreferAddress: dnsPreferenceArg(resolver.PreferenceIPv4),
278301
}
279302
args.autocertCacheEncKey.Set(os.Getenv(envCacheEncKey))
280303
flag.Func("bind-address", "HTTP proxy listen address. Set empty value to use systemd socket activation. (default \":8080\")", func(p string) error {
@@ -360,6 +383,15 @@ func parse_args() CLIArgs {
360383
flag.Int64Var(&args.bwBurst, "bw-limit-burst", 0, "allowed burst size for bandwidth limit, how many \"tokens\" can fit into leaky bucket")
361384
flag.UintVar(&args.bwBuckets, "bw-limit-buckets", 1024*1024, "number of buckets of bandwidth limit")
362385
flag.BoolVar(&args.bwSeparate, "bw-limit-separate", false, "separate upload and download bandwidth limits")
386+
flag.Func("dns-server", "nameserver specification (udp://..., tcp://..., https://..., tls://..., doh://..., dot://..., default://). Option can be used multiple times for parallel use of multiple nameservers. Empty string resets the list", func(p string) error {
387+
if p == "" {
388+
args.dnsServers = nil
389+
} else {
390+
args.dnsServers = append(args.dnsServers, p)
391+
}
392+
return nil
393+
})
394+
flag.Var(&args.dnsPreferAddress, "dns-prefer-address", "address resolution preference (none/ipv4/ipv6)")
363395
flag.DurationVar(&args.dnsCacheTTL, "dns-cache-ttl", 0, "enable DNS cache with specified fixed TTL")
364396
flag.DurationVar(&args.dnsCacheNegTTL, "dns-cache-neg-ttl", time.Second, "TTL for negative responses of DNS cache")
365397
flag.DurationVar(&args.dnsCacheTimeout, "dns-cache-timeout", 5*time.Second, "timeout for shared resolves of DNS cache")
@@ -510,10 +542,19 @@ func run() int {
510542

511543
dialerRoot = dialer.NewFilterDialer(filterRoot.Access, dialerRoot) // must follow after resolving in chain
512544

545+
var nameResolver dialer.Resolver = net.DefaultResolver
546+
if len(args.dnsServers) > 0 {
547+
nameResolver, err = resolver.FastFromURLs(args.dnsServers...)
548+
if err != nil {
549+
mainLogger.Critical("Failed to create name resolver: %v", err)
550+
return 3
551+
}
552+
}
553+
nameResolver = resolver.Prefer(nameResolver, args.dnsPreferAddress.Value())
513554
if args.dnsCacheTTL > 0 {
514555
cd := dialer.NewNameResolveCachingDialer(
515556
dialerRoot,
516-
net.DefaultResolver,
557+
nameResolver,
517558
args.dnsCacheTTL,
518559
args.dnsCacheNegTTL,
519560
args.dnsCacheTimeout,
@@ -522,7 +563,7 @@ func run() int {
522563
defer cd.Stop()
523564
dialerRoot = cd
524565
} else {
525-
dialerRoot = dialer.NewNameResolvingDialer(dialerRoot, net.DefaultResolver)
566+
dialerRoot = dialer.NewNameResolvingDialer(dialerRoot, nameResolver)
526567
}
527568

528569
// handler requisites

resolver/factory.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package resolver
2+
3+
import (
4+
"errors"
5+
"net"
6+
"net/url"
7+
"strings"
8+
9+
"github.com/ncruces/go-dns"
10+
)
11+
12+
func FromURL(u string) (*net.Resolver, error) {
13+
begin:
14+
parsed, err := url.Parse(u)
15+
if err != nil {
16+
return nil, err
17+
}
18+
host := parsed.Hostname()
19+
port := parsed.Port()
20+
switch scheme := strings.ToLower(parsed.Scheme); scheme {
21+
case "":
22+
switch {
23+
case strings.HasPrefix(u, "//"):
24+
u = "dns:" + u
25+
default:
26+
u = "dns://" + u
27+
}
28+
goto begin
29+
case "udp", "dns":
30+
if port == "" {
31+
port = "53"
32+
}
33+
return NewPlainResolver(net.JoinHostPort(host, port)), nil
34+
case "tcp":
35+
if port == "" {
36+
port = "53"
37+
}
38+
return NewTCPResolver(net.JoinHostPort(host, port)), nil
39+
case "http", "https", "doh":
40+
if port == "" {
41+
if scheme == "http" {
42+
port = "80"
43+
} else {
44+
port = "443"
45+
}
46+
}
47+
if scheme == "doh" {
48+
parsed.Scheme = "https"
49+
u = parsed.String()
50+
}
51+
return dns.NewDoHResolver(u, dns.DoHAddresses(net.JoinHostPort(host, port)))
52+
case "tls", "dot":
53+
if port == "" {
54+
port = "853"
55+
}
56+
hp := net.JoinHostPort(host, port)
57+
return dns.NewDoTResolver(hp, dns.DoTAddresses(hp))
58+
case "default":
59+
return net.DefaultResolver, nil
60+
default:
61+
return nil, errors.New("not implemented")
62+
}
63+
}

resolver/fast.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package resolver
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/netip"
7+
8+
"github.com/hashicorp/go-multierror"
9+
)
10+
11+
type LookupNetIPer interface {
12+
LookupNetIP(context.Context, string, string) ([]netip.Addr, error)
13+
}
14+
15+
type FastResolver struct {
16+
upstreams []LookupNetIPer
17+
}
18+
19+
func FastFromURLs(urls ...string) (LookupNetIPer, error) {
20+
resolvers := make([]LookupNetIPer, 0, len(urls))
21+
for i, u := range urls {
22+
res, err := FromURL(u)
23+
if err != nil {
24+
return nil, fmt.Errorf("unable to construct resolver #%d (%q): %w", i, u, err)
25+
}
26+
resolvers = append(resolvers, res)
27+
}
28+
if len(resolvers) == 1 {
29+
return resolvers[0], nil
30+
}
31+
return NewFastResolver(resolvers...), nil
32+
}
33+
34+
func NewFastResolver(resolvers ...LookupNetIPer) *FastResolver {
35+
return &FastResolver{
36+
upstreams: resolvers,
37+
}
38+
}
39+
40+
func (r FastResolver) LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error) {
41+
ctx, cl := context.WithCancel(ctx)
42+
defer cl()
43+
errors := make(chan error)
44+
success := make(chan []netip.Addr)
45+
for _, res := range r.upstreams {
46+
go func(res LookupNetIPer) {
47+
addrs, err := res.LookupNetIP(ctx, network, host)
48+
if err == nil {
49+
select {
50+
case success <- addrs:
51+
case <-ctx.Done():
52+
}
53+
} else {
54+
select {
55+
case errors <- err:
56+
case <-ctx.Done():
57+
}
58+
}
59+
}(res)
60+
}
61+
62+
var resErr error
63+
for _ = range r.upstreams {
64+
select {
65+
case <-ctx.Done():
66+
return nil, ctx.Err()
67+
case resAddrs := <-success:
68+
return resAddrs, nil
69+
case err := <-errors:
70+
resErr = multierror.Append(resErr, err)
71+
}
72+
}
73+
return nil, resErr
74+
}

resolver/plain.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package resolver
2+
3+
import (
4+
"context"
5+
"net"
6+
)
7+
8+
func NewPlainResolver(addr string) *net.Resolver {
9+
return &net.Resolver{
10+
PreferGo: true,
11+
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
12+
return (&net.Dialer{
13+
Resolver: &net.Resolver{},
14+
}).DialContext(ctx, network, addr)
15+
},
16+
}
17+
}
18+
19+
func NewTCPResolver(addr string) *net.Resolver {
20+
return &net.Resolver{
21+
PreferGo: true,
22+
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
23+
dnet := "tcp"
24+
switch network {
25+
case "udp4":
26+
dnet = "tcp4"
27+
case "udp6":
28+
dnet = "tcp6"
29+
}
30+
return (&net.Dialer{
31+
Resolver: &net.Resolver{},
32+
}).DialContext(ctx, dnet, addr)
33+
},
34+
}
35+
}

0 commit comments

Comments
 (0)