Skip to content

Commit

Permalink
DNS DoH: Add h2c Remote mode (with TLS serverNameToVerify)
Browse files Browse the repository at this point in the history
  • Loading branch information
RPRX authored Jan 25, 2025
1 parent a0822cb commit 2522cfd
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 56 deletions.
8 changes: 5 additions & 3 deletions app/dns/nameserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dis
switch {
case strings.EqualFold(u.String(), "localhost"):
return NewLocalNameServer(queryStrategy), nil
case strings.EqualFold(u.Scheme, "https"): // DOH Remote mode
return NewDoHNameServer(u, dispatcher, queryStrategy)
case strings.EqualFold(u.Scheme, "https+local"): // DOH Local mode
case strings.EqualFold(u.Scheme, "https"): // DNS-over-HTTPS Remote mode
return NewDoHNameServer(u, dispatcher, queryStrategy, false)
case strings.EqualFold(u.Scheme, "h2c"): // DNS-over-HTTPS h2c Remote mode
return NewDoHNameServer(u, dispatcher, queryStrategy, true)
case strings.EqualFold(u.Scheme, "https+local"): // DNS-over-HTTPS Local mode
return NewDoHLocalNameServer(u, queryStrategy), nil
case strings.EqualFold(u.Scheme, "quic+local"): // DNS-over-QUIC Local mode
return NewQUICNameServer(u, queryStrategy)
Expand Down
86 changes: 49 additions & 37 deletions app/dns/nameserver_doh.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dns
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
Expand All @@ -23,6 +24,7 @@ import (
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport/internet"
"golang.org/x/net/dns/dnsmessage"
"golang.org/x/net/http2"
)

// DoHNameServer implemented DNS over HTTPS (RFC8484) Wire Format,
Expand All @@ -41,49 +43,59 @@ type DoHNameServer struct {
}

// NewDoHNameServer creates DOH server object for remote resolving.
func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, queryStrategy QueryStrategy) (*DoHNameServer, error) {
errors.LogInfo(context.Background(), "DNS: created Remote DOH client for ", url.String())
func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, queryStrategy QueryStrategy, h2c bool) (*DoHNameServer, error) {
url.Scheme = "https"
errors.LogInfo(context.Background(), "DNS: created Remote DNS-over-HTTPS client for ", url.String(), ", with h2c ", h2c)
s := baseDOHNameServer(url, "DOH", queryStrategy)

s.dispatcher = dispatcher
tr := &http.Transport{
MaxIdleConns: 30,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 30 * time.Second,
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dest, err := net.ParseDestination(network + ":" + addr)
if err != nil {
return nil, err
}
link, err := s.dispatcher.Dispatch(toDnsContext(ctx, s.dohURL), dest)
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
dest, err := net.ParseDestination(network + ":" + addr)
if err != nil {
return nil, err
}
link, err := s.dispatcher.Dispatch(toDnsContext(ctx, s.dohURL), dest)
select {
case <-ctx.Done():
return nil, ctx.Err()
default:

}
if err != nil {
return nil, err
}
}
if err != nil {
return nil, err
}

cc := common.ChainedClosable{}
if cw, ok := link.Writer.(common.Closable); ok {
cc = append(cc, cw)
}
if cr, ok := link.Reader.(common.Closable); ok {
cc = append(cc, cr)
}
return cnc.NewConnection(
cnc.ConnectionInputMulti(link.Writer),
cnc.ConnectionOutputMulti(link.Reader),
cnc.ConnectionOnClose(cc),
), nil
},
cc := common.ChainedClosable{}
if cw, ok := link.Writer.(common.Closable); ok {
cc = append(cc, cw)
}
if cr, ok := link.Reader.(common.Closable); ok {
cc = append(cc, cr)
}
return cnc.NewConnection(
cnc.ConnectionInputMulti(link.Writer),
cnc.ConnectionOutputMulti(link.Reader),
cnc.ConnectionOnClose(cc),
), nil
}

s.httpClient = &http.Client{
Timeout: time.Second * 180,
Transport: tr,
Timeout: time.Second * 180,
Transport: &http.Transport{
MaxIdleConns: 30,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 30 * time.Second,
ForceAttemptHTTP2: true,
DialContext: dialContext,
},
}
if h2c {
s.httpClient.Transport = &http2.Transport{
IdleConnTimeout: 90 * time.Second,
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
return dialContext(ctx, network, addr)
},
}
}

return s, nil
Expand Down Expand Up @@ -118,7 +130,7 @@ func NewDoHLocalNameServer(url *url.URL, queryStrategy QueryStrategy) *DoHNameSe
Timeout: time.Second * 180,
Transport: tr,
}
errors.LogInfo(context.Background(), "DNS: created Local DOH client for ", url.String())
errors.LogInfo(context.Background(), "DNS: created Local DNS-over-HTTPS client for ", url.String())
return s
}

Expand Down
5 changes: 5 additions & 0 deletions infra/conf/transport_internet.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ type TLSConfig struct {
PinnedPeerCertificatePublicKeySha256 *[]string `json:"pinnedPeerCertificatePublicKeySha256"`
CurvePreferences *StringList `json:"curvePreferences"`
MasterKeyLog string `json:"masterKeyLog"`
ServerNameToVerify string `json:"serverNameToVerify"`
}

// Build implements Buildable.
Expand Down Expand Up @@ -468,6 +469,10 @@ func (c *TLSConfig) Build() (proto.Message, error) {
}

config.MasterKeyLog = c.MasterKeyLog
config.ServerNameToVerify = c.ServerNameToVerify
if config.ServerNameToVerify != "" && config.Fingerprint == "unsafe" {
return nil, errors.New(`serverNameToVerify only works with uTLS for now`)
}

return config, nil
}
Expand Down
12 changes: 12 additions & 0 deletions transport/internet/tls/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"crypto/hmac"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/base64"
Expand Down Expand Up @@ -303,6 +304,14 @@ func (c *Config) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Cert
return nil
}

type RandCarrier struct {
ServerNameToVerify string
}

func (r *RandCarrier) Read(p []byte) (n int, err error) {
return rand.Read(p)
}

// GetTLSConfig converts this Config into tls.Config.
func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
root, err := c.getCertPool()
Expand All @@ -321,6 +330,9 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
}

config := &tls.Config{
Rand: &RandCarrier{
ServerNameToVerify: c.ServerNameToVerify,
},
ClientSessionCache: globalSessionCache,
RootCAs: root,
InsecureSkipVerify: c.AllowInsecure,
Expand Down
33 changes: 22 additions & 11 deletions transport/internet/tls/config.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions transport/internet/tls/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,6 @@ message Config {

// Lists of string as CurvePreferences values.
repeated string curve_preferences = 16;

string server_name_to_verify = 17;
}
15 changes: 10 additions & 5 deletions transport/internet/tls/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,17 @@ func UClient(c net.Conn, config *tls.Config, fingerprint *utls.ClientHelloID) ne
}

func copyConfig(c *tls.Config) *utls.Config {
serverNameToVerify := ""
if r, ok := c.Rand.(*RandCarrier); ok {
serverNameToVerify = r.ServerNameToVerify
}
return &utls.Config{
RootCAs: c.RootCAs,
ServerName: c.ServerName,
InsecureSkipVerify: c.InsecureSkipVerify,
VerifyPeerCertificate: c.VerifyPeerCertificate,
KeyLogWriter: c.KeyLogWriter,
RootCAs: c.RootCAs,
ServerName: c.ServerName,
InsecureSkipVerify: c.InsecureSkipVerify,
VerifyPeerCertificate: c.VerifyPeerCertificate,
KeyLogWriter: c.KeyLogWriter,
InsecureServerNameToVerify: serverNameToVerify,
}
}

Expand Down

0 comments on commit 2522cfd

Please sign in to comment.