diff --git a/app/cmd/server.go b/app/cmd/server.go index 572d63f..910de8b 100644 --- a/app/cmd/server.go +++ b/app/cmd/server.go @@ -118,11 +118,19 @@ type serverConfigResolverTLS struct { Insecure bool `mapstructure:"insecure"` } +type serverConfigResolverHTTPS struct { + Addr string `mapstructure:"addr"` + Timeout time.Duration `mapstructure:"timeout"` + SNI string `mapstructure:"sni"` + Insecure bool `mapstructure:"insecure"` +} + type serverConfigResolver struct { - Type string `mapstructure:"type"` - TCP serverConfigResolverTCP `mapstructure:"tcp"` - UDP serverConfigResolverUDP `mapstructure:"udp"` - TLS serverConfigResolverTLS `mapstructure:"tls"` + Type string `mapstructure:"type"` + TCP serverConfigResolverTCP `mapstructure:"tcp"` + UDP serverConfigResolverUDP `mapstructure:"udp"` + TLS serverConfigResolverTLS `mapstructure:"tls"` + HTTPS serverConfigResolverHTTPS `mapstructure:"https"` } type serverConfigOutboundDirect struct { @@ -343,6 +351,11 @@ func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error { return configError{Field: "resolver.tls.addr", Err: errors.New("empty resolver address")} } ob = outbounds.NewStandardResolverTLS(c.Resolver.TLS.Addr, c.Resolver.TLS.Timeout, c.Resolver.TLS.SNI, c.Resolver.TLS.Insecure, ob) + case "https", "http": + if c.Resolver.HTTPS.Addr == "" { + return configError{Field: "resolver.https.addr", Err: errors.New("empty resolver address")} + } + ob = outbounds.NewDoHResolver(c.Resolver.HTTPS.Addr, c.Resolver.HTTPS.Timeout, c.Resolver.HTTPS.SNI, c.Resolver.HTTPS.Insecure, ob) default: return configError{Field: "resolver.type", Err: errors.New("unsupported resolver type")} } diff --git a/app/cmd/server_test.go b/app/cmd/server_test.go index 60c7b65..9402dea 100644 --- a/app/cmd/server_test.go +++ b/app/cmd/server_test.go @@ -88,6 +88,12 @@ func TestServerConfig(t *testing.T) { SNI: "server1.yolo.net", Insecure: true, }, + HTTPS: serverConfigResolverHTTPS{ + Addr: "cringe.ahh.cc", + Timeout: 5 * time.Second, + SNI: "real.stuff.net", + Insecure: true, + }, }, Outbounds: []serverConfigOutboundEntry{ { diff --git a/app/cmd/server_test.yaml b/app/cmd/server_test.yaml index 1e53060..11e3c95 100644 --- a/app/cmd/server_test.yaml +++ b/app/cmd/server_test.yaml @@ -64,6 +64,11 @@ resolver: timeout: 10s sni: server1.yolo.net insecure: true + https: + addr: cringe.ahh.cc + timeout: 5s + sni: real.stuff.net + insecure: true outbounds: - name: goodstuff diff --git a/app/go.mod b/app/go.mod index 4b3279c..5fb50a6 100644 --- a/app/go.mod +++ b/app/go.mod @@ -17,6 +17,7 @@ require ( require ( github.com/apernet/quic-go v0.37.5-0.20230809210726-5508a358d07e // indirect + github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect diff --git a/app/go.sum b/app/go.sum index 63137d6..61e319b 100644 --- a/app/go.sum +++ b/app/go.sum @@ -42,6 +42,8 @@ github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f h1:uVh0qpEslrWjg github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f/go.mod h1:xkkq9D4ygcldQQhKS/w9CadiCKwCngU7K9E3DaKahpM= github.com/apernet/quic-go v0.37.5-0.20230809210726-5508a358d07e h1:hWrd6A3QZQX2pXT1JJA2x1vgqNf5jZH8po0oa2GsbeI= github.com/apernet/quic-go v0.37.5-0.20230809210726-5508a358d07e/go.mod h1:Gqxx9qMiutRcTLNlbdPwuI9dF8+GV2GQG+5mVW0E34I= +github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0= +github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE= diff --git a/extras/go.mod b/extras/go.mod index 2da36e6..4143c22 100644 --- a/extras/go.mod +++ b/extras/go.mod @@ -4,6 +4,7 @@ go 1.20 require ( github.com/apernet/hysteria/core v0.0.0-00010101000000-000000000000 + github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 github.com/miekg/dns v1.1.55 github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.11.0 diff --git a/extras/go.sum b/extras/go.sum index bf355b5..6fba549 100644 --- a/extras/go.sum +++ b/extras/go.sum @@ -1,5 +1,7 @@ github.com/apernet/quic-go v0.37.5-0.20230809210726-5508a358d07e h1:hWrd6A3QZQX2pXT1JJA2x1vgqNf5jZH8po0oa2GsbeI= github.com/apernet/quic-go v0.37.5-0.20230809210726-5508a358d07e/go.mod h1:Gqxx9qMiutRcTLNlbdPwuI9dF8+GV2GQG+5mVW0E34I= +github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0= +github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= diff --git a/extras/outbounds/dns_https.go b/extras/outbounds/dns_https.go new file mode 100644 index 0000000..daf0dfe --- /dev/null +++ b/extras/outbounds/dns_https.go @@ -0,0 +1,84 @@ +package outbounds + +import ( + "crypto/tls" + "net" + "net/http" + "time" + + "github.com/babolivier/go-doh-client" +) + +// dohResolver is a PluggableOutbound DNS resolver that resolves hostnames +// using the user-provided DNS-over-HTTPS server. +type dohResolver struct { + Resolver *doh.Resolver + Next PluggableOutbound +} + +func NewDoHResolver(host string, timeout time.Duration, sni string, insecure bool, next PluggableOutbound) PluggableOutbound { + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.TLSClientConfig = &tls.Config{ + ServerName: sni, + InsecureSkipVerify: insecure, + } + return &dohResolver{ + Resolver: &doh.Resolver{ + Host: host, + Class: doh.IN, + HTTPClient: &http.Client{ + Transport: tr, + Timeout: timeoutOrDefault(timeout), + }, + }, + Next: next, + } +} + +func (r *dohResolver) resolve(reqAddr *AddrEx) { + if tryParseIP(reqAddr) { + // The host is already an IP address, we don't need to resolve it. + return + } + type lookupResult struct { + ip net.IP + err error + } + ch4, ch6 := make(chan lookupResult, 1), make(chan lookupResult, 1) + go func() { + recs, _, err := r.Resolver.LookupA(reqAddr.Host) + var ip net.IP + if err == nil && len(recs) > 0 { + ip = net.ParseIP(recs[0].IP4).To4() + } + ch4 <- lookupResult{ip, err} + }() + go func() { + recs, _, err := r.Resolver.LookupAAAA(reqAddr.Host) + var ip net.IP + if err == nil && len(recs) > 0 { + ip = net.ParseIP(recs[0].IP6).To16() + } + ch6 <- lookupResult{ip, err} + }() + result4, result6 := <-ch4, <-ch6 + reqAddr.ResolveInfo = &ResolveInfo{ + IPv4: result4.ip, + IPv6: result6.ip, + } + if result4.err != nil { + reqAddr.ResolveInfo.Err = result4.err + } else if result6.err != nil { + reqAddr.ResolveInfo.Err = result6.err + } +} + +func (r *dohResolver) TCP(reqAddr *AddrEx) (net.Conn, error) { + r.resolve(reqAddr) + return r.Next.TCP(reqAddr) +} + +func (r *dohResolver) UDP(reqAddr *AddrEx) (UDPConn, error) { + r.resolve(reqAddr) + return r.Next.UDP(reqAddr) +} diff --git a/extras/outbounds/dns_standard.go b/extras/outbounds/dns_standard.go index 44f0e56..a9df238 100644 --- a/extras/outbounds/dns_standard.go +++ b/extras/outbounds/dns_standard.go @@ -9,8 +9,8 @@ import ( ) const ( - standardResolverDefaultTimeout = 2 * time.Second - standardResolverRetryTimes = 2 + resolverDefaultTimeout = 2 * time.Second + standardResolverRetryTimes = 2 ) // standardResolver is a PluggableOutbound DNS resolver that resolves hostnames @@ -76,7 +76,7 @@ func addDefaultPortTLS(addr string) string { func timeoutOrDefault(timeout time.Duration) time.Duration { if timeout == 0 { - return standardResolverDefaultTimeout + return resolverDefaultTimeout } return timeout }