DNS DoH: Use Chrome's fingerprint & keepAlivePeriod, Add header padding by default

https://github.com/XTLS/Xray-core/discussions/4430#discussioncomment-12374292
This commit is contained in:
RPRX 2025-03-03 14:45:12 +00:00 committed by GitHub
parent b9cb93d3c2
commit e466b0497c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 130 additions and 162 deletions

View File

@ -45,11 +45,13 @@ func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dis
case strings.EqualFold(u.String(), "localhost"):
return NewLocalNameServer(queryStrategy), nil
case strings.EqualFold(u.Scheme, "https"): // DNS-over-HTTPS Remote mode
return NewDoHNameServer(u, dispatcher, queryStrategy, false)
return NewDoHNameServer(u, queryStrategy, dispatcher, false), nil
case strings.EqualFold(u.Scheme, "h2c"): // DNS-over-HTTPS h2c Remote mode
return NewDoHNameServer(u, dispatcher, queryStrategy, true)
return NewDoHNameServer(u, queryStrategy, dispatcher, true), nil
case strings.EqualFold(u.Scheme, "https+local"): // DNS-over-HTTPS Local mode
return NewDoHLocalNameServer(u, queryStrategy), nil
return NewDoHNameServer(u, queryStrategy, nil, false), nil
case strings.EqualFold(u.Scheme, "h2c+local"): // DNS-over-HTTPS h2c Local mode
return NewDoHNameServer(u, queryStrategy, nil, true), nil
case strings.EqualFold(u.Scheme, "quic+local"): // DNS-over-QUIC Local mode
return NewQUICNameServer(u, queryStrategy)
case strings.EqualFold(u.Scheme, "tcp"): // DNS-over-TCP Remote mode

View File

@ -8,10 +8,13 @@ import (
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
utls "github.com/refraction-networking/utls"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net"
@ -31,7 +34,6 @@ import (
// which is compatible with traditional dns over udp(RFC1035),
// thus most of the DOH implementation is copied from udpns.go
type DoHNameServer struct {
dispatcher routing.Dispatcher
sync.RWMutex
ips map[string]*record
pub *pubsub.Service
@ -42,108 +44,18 @@ type DoHNameServer struct {
queryStrategy QueryStrategy
}
// NewDoHNameServer creates DOH server object for remote resolving.
func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, queryStrategy QueryStrategy, h2c bool) (*DoHNameServer, error) {
// NewDoHNameServer creates DOH/DOHL client object for remote/local resolving.
func NewDoHNameServer(url *url.URL, queryStrategy QueryStrategy, dispatcher routing.Dispatcher, h2c bool) *DoHNameServer {
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
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
dest, err := net.ParseDestination(network + ":" + addr)
if err != nil {
return nil, err
}
dnsCtx := toDnsContext(ctx, s.dohURL)
if h2c {
dnsCtx = session.ContextWithMitmAlpn11(dnsCtx, false) // for insurance
dnsCtx = session.ContextWithMitmServerName(dnsCtx, url.Hostname())
}
link, err := s.dispatcher.Dispatch(dnsCtx, dest)
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
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
mode := "DOH"
if dispatcher == nil {
mode = "DOHL"
}
s.httpClient = &http.Client{
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
}
// NewDoHLocalNameServer creates DOH client object for local resolving
func NewDoHLocalNameServer(url *url.URL, queryStrategy QueryStrategy) *DoHNameServer {
url.Scheme = "https"
s := baseDOHNameServer(url, "DOHL", queryStrategy)
tr := &http.Transport{
IdleConnTimeout: 90 * 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
}
conn, err := internet.DialSystem(ctx, dest, nil)
log.Record(&log.AccessMessage{
From: "DNS",
To: s.dohURL,
Status: log.AccessAccepted,
Detour: "local",
})
if err != nil {
return nil, err
}
return conn, nil
},
}
s.httpClient = &http.Client{
Timeout: time.Second * 180,
Transport: tr,
}
errors.LogInfo(context.Background(), "DNS: created Local DNS-over-HTTPS client for ", url.String())
return s
}
func baseDOHNameServer(url *url.URL, prefix string, queryStrategy QueryStrategy) *DoHNameServer {
errors.LogInfo(context.Background(), "DNS: created ", mode, " client for ", url.String(), ", with h2c ", h2c)
s := &DoHNameServer{
ips: make(map[string]*record),
pub: pubsub.NewService(),
name: prefix + "//" + url.Host,
name: mode + "//" + url.Host,
dohURL: url.String(),
queryStrategy: queryStrategy,
}
@ -151,6 +63,65 @@ func baseDOHNameServer(url *url.URL, prefix string, queryStrategy QueryStrategy)
Interval: time.Minute,
Execute: s.Cleanup,
}
s.httpClient = &http.Client{
Transport: &http2.Transport{
IdleConnTimeout: net.ConnIdleTimeout,
ReadIdleTimeout: net.ChromeH2KeepAlivePeriod,
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
dest, err := net.ParseDestination(network + ":" + addr)
if err != nil {
return nil, err
}
var conn net.Conn
if dispatcher != nil {
dnsCtx := toDnsContext(ctx, s.dohURL)
if h2c {
dnsCtx = session.ContextWithMitmAlpn11(dnsCtx, false) // for insurance
dnsCtx = session.ContextWithMitmServerName(dnsCtx, url.Hostname())
}
link, err := dispatcher.Dispatch(dnsCtx, dest)
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
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)
}
conn = cnc.NewConnection(
cnc.ConnectionInputMulti(link.Writer),
cnc.ConnectionOutputMulti(link.Reader),
cnc.ConnectionOnClose(cc),
)
} else {
log.Record(&log.AccessMessage{
From: "DNS",
To: s.dohURL,
Status: log.AccessAccepted,
Detour: "local",
})
conn, err = internet.DialSystem(ctx, dest, nil)
if err != nil {
return nil, err
}
}
if !h2c {
conn = utls.UClient(conn, &utls.Config{ServerName: url.Hostname()}, utls.HelloChrome_Auto)
if err := conn.(*utls.UConn).HandshakeContext(ctx); err != nil {
return nil, err
}
}
return conn, nil
},
},
}
return s
}
@ -310,6 +281,8 @@ func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte,
req.Header.Add("Accept", "application/dns-message")
req.Header.Add("Content-Type", "application/dns-message")
req.Header.Set("X-Padding", strings.Repeat("X", int(crypto.RandBetween(100, 1000))))
hc := s.httpClient
resp, err := hc.Do(req.WithContext(ctx))

View File

@ -17,7 +17,7 @@ func TestDOHNameServer(t *testing.T) {
url, err := url.Parse("https+local://1.1.1.1/dns-query")
common.Must(err)
s := NewDoHLocalNameServer(url, QueryStrategy_USE_IP)
s := NewDoHNameServer(url, QueryStrategy_USE_IP, nil, false)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,
@ -34,7 +34,7 @@ func TestDOHNameServerWithCache(t *testing.T) {
url, err := url.Parse("https+local://1.1.1.1/dns-query")
common.Must(err)
s := NewDoHLocalNameServer(url, QueryStrategy_USE_IP)
s := NewDoHNameServer(url, QueryStrategy_USE_IP, nil, false)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,
@ -62,7 +62,7 @@ func TestDOHNameServerWithIPv4Override(t *testing.T) {
url, err := url.Parse("https+local://1.1.1.1/dns-query")
common.Must(err)
s := NewDoHLocalNameServer(url, QueryStrategy_USE_IP4)
s := NewDoHNameServer(url, QueryStrategy_USE_IP4, nil, false)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,
@ -85,7 +85,7 @@ func TestDOHNameServerWithIPv6Override(t *testing.T) {
url, err := url.Parse("https+local://1.1.1.1/dns-query")
common.Must(err)
s := NewDoHLocalNameServer(url, QueryStrategy_USE_IP6)
s := NewDoHNameServer(url, QueryStrategy_USE_IP6, nil, false)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,

View File

@ -1,2 +1,15 @@
// Package crypto provides common crypto libraries for Xray.
package crypto // import "github.com/xtls/xray-core/common/crypto"
import (
"crypto/rand"
"math/big"
)
func RandBetween(from int64, to int64) int64 {
if from == to {
return from
}
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
return from + bigInt.Int64()
}

View File

@ -1,2 +1,14 @@
// Package net is a drop-in replacement to Golang's net package, with some more functionalities.
package net // import "github.com/xtls/xray-core/common/net"
import "time"
// defines the maximum time an idle TCP session can survive in the tunnel, so
// it should be consistent across HTTP versions and with other transports.
const ConnIdleTimeout = 300 * time.Second
// consistent with quic-go
const QuicgoH3KeepAlivePeriod = 10 * time.Second
// consistent with chrome
const ChromeH2KeepAlivePeriod = 45 * time.Second

View File

@ -4,12 +4,12 @@ import (
"context"
"crypto/rand"
"io"
"math/big"
"time"
"github.com/pires/go-proxyproto"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
@ -414,7 +414,7 @@ func (w *NoisePacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
noise = n.Packet
} else {
//Random noise
noise, err = GenerateRandomBytes(randBetween(int64(n.LengthMin),
noise, err = GenerateRandomBytes(crypto.RandBetween(int64(n.LengthMin),
int64(n.LengthMax)))
}
if err != nil {
@ -423,7 +423,7 @@ func (w *NoisePacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
w.Writer.WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(noise)})
if n.DelayMin != 0 || n.DelayMax != 0 {
time.Sleep(time.Duration(randBetween(int64(n.DelayMin), int64(n.DelayMax))) * time.Millisecond)
time.Sleep(time.Duration(crypto.RandBetween(int64(n.DelayMin), int64(n.DelayMax))) * time.Millisecond)
}
}
@ -452,7 +452,7 @@ func (f *FragmentWriter) Write(b []byte) (int, error) {
buf := make([]byte, 1024)
var hello []byte
for from := 0; ; {
to := from + int(randBetween(int64(f.fragment.LengthMin), int64(f.fragment.LengthMax)))
to := from + int(crypto.RandBetween(int64(f.fragment.LengthMin), int64(f.fragment.LengthMax)))
if to > len(data) {
to = len(data)
}
@ -466,7 +466,7 @@ func (f *FragmentWriter) Write(b []byte) (int, error) {
hello = append(hello, buf[:5+l]...)
} else {
_, err := f.writer.Write(buf[:5+l])
time.Sleep(time.Duration(randBetween(int64(f.fragment.IntervalMin), int64(f.fragment.IntervalMax))) * time.Millisecond)
time.Sleep(time.Duration(crypto.RandBetween(int64(f.fragment.IntervalMin), int64(f.fragment.IntervalMax))) * time.Millisecond)
if err != nil {
return 0, err
}
@ -493,13 +493,13 @@ func (f *FragmentWriter) Write(b []byte) (int, error) {
return f.writer.Write(b)
}
for from := 0; ; {
to := from + int(randBetween(int64(f.fragment.LengthMin), int64(f.fragment.LengthMax)))
to := from + int(crypto.RandBetween(int64(f.fragment.LengthMin), int64(f.fragment.LengthMax)))
if to > len(b) {
to = len(b)
}
n, err := f.writer.Write(b[from:to])
from += n
time.Sleep(time.Duration(randBetween(int64(f.fragment.IntervalMin), int64(f.fragment.IntervalMax))) * time.Millisecond)
time.Sleep(time.Duration(crypto.RandBetween(int64(f.fragment.IntervalMin), int64(f.fragment.IntervalMax))) * time.Millisecond)
if err != nil {
return from, err
}
@ -509,14 +509,6 @@ func (f *FragmentWriter) Write(b []byte) (int, error) {
}
}
// stolen from github.com/xtls/xray-core/transport/internet/reality
func randBetween(left int64, right int64) int64 {
if left == right {
return left
}
bigInt, _ := rand.Int(rand.Reader, big.NewInt(right-left))
return left + bigInt.Int64()
}
func GenerateRandomBytes(n int64) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)

View File

@ -8,7 +8,6 @@ import (
"crypto/ecdh"
"crypto/ed25519"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
gotls "crypto/tls"
@ -16,7 +15,6 @@ import (
"encoding/binary"
"fmt"
"io"
"math/big"
"net/http"
"reflect"
"regexp"
@ -27,6 +25,7 @@ import (
utls "github.com/refraction-networking/utls"
"github.com/xtls/reality"
"github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/core"
@ -213,13 +212,13 @@ func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destinati
}
times := 1
if !first {
times = int(randBetween(config.SpiderY[4], config.SpiderY[5]))
times = int(crypto.RandBetween(config.SpiderY[4], config.SpiderY[5]))
}
for j := 0; j < times; j++ {
if !first && j == 0 {
req.Header.Set("Referer", firstURL)
}
req.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", int(randBetween(config.SpiderY[0], config.SpiderY[1])))})
req.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", int(crypto.RandBetween(config.SpiderY[0], config.SpiderY[1])))})
if resp, err = client.Do(req); err != nil {
break
}
@ -243,18 +242,18 @@ func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destinati
}
maps.Unlock()
if !first {
time.Sleep(time.Duration(randBetween(config.SpiderY[6], config.SpiderY[7])) * time.Millisecond) // interval
time.Sleep(time.Duration(crypto.RandBetween(config.SpiderY[6], config.SpiderY[7])) * time.Millisecond) // interval
}
}
}
get(true)
concurrency := int(randBetween(config.SpiderY[2], config.SpiderY[3]))
concurrency := int(crypto.RandBetween(config.SpiderY[2], config.SpiderY[3]))
for i := 0; i < concurrency; i++ {
go get(false)
}
// Do not close the connection
}()
time.Sleep(time.Duration(randBetween(config.SpiderY[8], config.SpiderY[9])) * time.Millisecond) // return
time.Sleep(time.Duration(crypto.RandBetween(config.SpiderY[8], config.SpiderY[9])) * time.Millisecond) // return
return nil, errors.New("REALITY: processed invalid connection").AtWarning()
}
return uConn, nil
@ -271,7 +270,7 @@ var maps struct {
}
func getPathLocked(paths map[string]struct{}) string {
stopAt := int(randBetween(0, int64(len(paths)-1)))
stopAt := int(crypto.RandBetween(0, int64(len(paths)-1)))
i := 0
for s := range paths {
if i == stopAt {
@ -281,11 +280,3 @@ func getPathLocked(paths map[string]struct{}) string {
}
return "/"
}
func randBetween(left int64, right int64) int64 {
if left == right {
return left
}
bigInt, _ := rand.Int(rand.Reader, big.NewInt(right-left))
return left + bigInt.Int64()
}

View File

@ -1,13 +1,12 @@
package splithttp
import (
"crypto/rand"
"math/big"
"net/http"
"net/url"
"strings"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/transport/internet"
)
@ -184,9 +183,5 @@ func init() {
}
func (c RangeConfig) rand() int32 {
if c.From == c.To {
return c.From
}
bigInt, _ := rand.Int(rand.Reader, big.NewInt(int64(c.To-c.From)))
return c.From + int32(bigInt.Int64())
return int32(crypto.RandBetween(int64(c.From), int64(c.To)))
}

View File

@ -30,16 +30,6 @@ import (
"golang.org/x/net/http2"
)
// defines the maximum time an idle TCP session can survive in the tunnel, so
// it should be consistent across HTTP versions and with other transports.
const connIdleTimeout = 300 * time.Second
// consistent with quic-go
const quicgoH3KeepAlivePeriod = 10 * time.Second
// consistent with chrome
const chromeH2KeepAlivePeriod = 45 * time.Second
type dialerConf struct {
net.Destination
*internet.MemoryStreamConfig
@ -154,13 +144,13 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
if httpVersion == "3" {
if keepAlivePeriod == 0 {
keepAlivePeriod = quicgoH3KeepAlivePeriod
keepAlivePeriod = net.QuicgoH3KeepAlivePeriod
}
if keepAlivePeriod < 0 {
keepAlivePeriod = 0
}
quicConfig := &quic.Config{
MaxIdleTimeout: connIdleTimeout,
MaxIdleTimeout: net.ConnIdleTimeout,
// these two are defaults of quic-go/http3. the default of quic-go (no
// http3) is different, so it is hardcoded here for clarity.
@ -168,7 +158,7 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
MaxIncomingStreams: -1,
KeepAlivePeriod: keepAlivePeriod,
}
transport = &http3.RoundTripper{
transport = &http3.Transport{
QUICConfig: quicConfig,
TLSClientConfig: gotlsConfig,
Dial: func(ctx context.Context, addr string, tlsCfg *gotls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
@ -198,7 +188,7 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
return nil, err
}
default:
udpConn = &internet.FakePacketConn{c}
udpConn = &internet.FakePacketConn{Conn: c}
udpAddr, err = net.ResolveUDPAddr("udp", c.RemoteAddr().String())
if err != nil {
return nil, err
@ -210,7 +200,7 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
}
} else if httpVersion == "2" {
if keepAlivePeriod == 0 {
keepAlivePeriod = chromeH2KeepAlivePeriod
keepAlivePeriod = net.ChromeH2KeepAlivePeriod
}
if keepAlivePeriod < 0 {
keepAlivePeriod = 0
@ -219,7 +209,7 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
DialTLSContext: func(ctxInner context.Context, network string, addr string, cfg *gotls.Config) (net.Conn, error) {
return dialContext(ctxInner)
},
IdleConnTimeout: connIdleTimeout,
IdleConnTimeout: net.ConnIdleTimeout,
ReadIdleTimeout: keepAlivePeriod,
}
} else {
@ -230,7 +220,7 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
transport = &http.Transport{
DialTLSContext: httpDialContext,
DialContext: httpDialContext,
IdleConnTimeout: connIdleTimeout,
IdleConnTimeout: net.ConnIdleTimeout,
// chunked transfer download with KeepAlives is buggy with
// http.Client and our custom dial context.
DisableKeepAlives: true,

View File

@ -207,7 +207,7 @@ type Config struct {
// @Critical
PinnedPeerCertificateChainSha256 [][]byte `protobuf:"bytes,13,rep,name=pinned_peer_certificate_chain_sha256,json=pinnedPeerCertificateChainSha256,proto3" json:"pinned_peer_certificate_chain_sha256,omitempty"`
// @Document Some certificate public key sha256 hashes.
// @Document After normal validation (required), if the verified cert's public key hash does not match any of these values, the connection will be aborted.
// @Document After normal validation (required), if one of certs in verified chain matches one of these values, the connection will be eventually accepted.
// @Critical
PinnedPeerCertificatePublicKeySha256 [][]byte `protobuf:"bytes,14,rep,name=pinned_peer_certificate_public_key_sha256,json=pinnedPeerCertificatePublicKeySha256,proto3" json:"pinned_peer_certificate_public_key_sha256,omitempty"`
MasterKeyLog string `protobuf:"bytes,15,opt,name=master_key_log,json=masterKeyLog,proto3" json:"master_key_log,omitempty"`

View File

@ -76,7 +76,7 @@ message Config {
repeated bytes pinned_peer_certificate_chain_sha256 = 13;
/* @Document Some certificate public key sha256 hashes.
@Document After normal validation (required), if the verified cert's public key hash does not match any of these values, the connection will be aborted.
@Document After normal validation (required), if one of certs in verified chain matches one of these values, the connection will be eventually accepted.
@Critical
*/
repeated bytes pinned_peer_certificate_public_key_sha256 = 14;