mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-26 21:16:57 +00:00
XDNS finalmask: Support DoH ("https://", tlsConfig, domain fronting, etc., quicConfig, Chrome Client Hello by default) with empty HTTP User-Agent (#7140)
https://github.com/XTLS/Xray-core/pull/7103#issuecomment-6118333973 https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6161808702 https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6167254749 https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6185965142 https://github.com/XTLS/Xray-core/pull/7140#issuecomment-6200775375
This commit is contained in:
@@ -8,6 +8,8 @@ package xdns
|
||||
|
||||
import (
|
||||
_ "github.com/xtls/xray-core/common/serial"
|
||||
internet "github.com/xtls/xray-core/transport/internet"
|
||||
tls "github.com/xtls/xray-core/transport/internet/tls"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
@@ -102,6 +104,9 @@ type ResolverProto struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Addr string `protobuf:"bytes,2,opt,name=addr,proto3" json:"addr,omitempty"`
|
||||
Worker int32 `protobuf:"varint,3,opt,name=worker,proto3" json:"worker,omitempty"`
|
||||
TlsConfig *tls.Config `protobuf:"bytes,4,opt,name=tls_config,json=tlsConfig,proto3" json:"tls_config,omitempty"`
|
||||
QuicConfig *internet.QuicParams `protobuf:"bytes,5,opt,name=quic_config,json=quicConfig,proto3" json:"quic_config,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -150,6 +155,27 @@ func (x *ResolverProto) GetAddr() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ResolverProto) GetWorker() int32 {
|
||||
if x != nil {
|
||||
return x.Worker
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ResolverProto) GetTlsConfig() *tls.Config {
|
||||
if x != nil {
|
||||
return x.TlsConfig
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ResolverProto) GetQuicConfig() *internet.QuicParams {
|
||||
if x != nil {
|
||||
return x.QuicConfig
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Domains []*DomainProto `protobuf:"bytes,1,rep,name=domains,proto3" json:"domains,omitempty"`
|
||||
@@ -214,17 +240,22 @@ var File_transport_internet_finalmask_xdns_config_proto protoreflect.FileDescrip
|
||||
|
||||
const file_transport_internet_finalmask_xdns_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\x1a!common/serial/typed_message.proto\"\x8b\x01\n" +
|
||||
".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\x1a!common/serial/typed_message.proto\x1a#transport/internet/tls/config.proto\x1a\x1ftransport/internet/config.proto\"\x8b\x01\n" +
|
||||
"\vDomainProto\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1b\n" +
|
||||
"\tlen_limit\x18\x02 \x01(\x05R\blenLimit\x12\x1f\n" +
|
||||
"\vlabel_limit\x18\x03 \x01(\x05R\n" +
|
||||
"labelLimit\x12\x14\n" +
|
||||
"\x05types\x18\x04 \x03(\x05R\x05types\x12\x14\n" +
|
||||
"\x05edns0\x18\x05 \x01(\x05R\x05edns0\"7\n" +
|
||||
"\x05edns0\x18\x05 \x01(\x05R\x05edns0\"\xd9\x01\n" +
|
||||
"\rResolverProto\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
|
||||
"\x04addr\x18\x02 \x01(\tR\x04addr\"\xcb\x01\n" +
|
||||
"\x04addr\x18\x02 \x01(\tR\x04addr\x12\x16\n" +
|
||||
"\x06worker\x18\x03 \x01(\x05R\x06worker\x12B\n" +
|
||||
"\n" +
|
||||
"tls_config\x18\x04 \x01(\v2#.xray.transport.internet.tls.ConfigR\ttlsConfig\x12D\n" +
|
||||
"\vquic_config\x18\x05 \x01(\v2#.xray.transport.internet.QuicParamsR\n" +
|
||||
"quicConfig\"\xcb\x01\n" +
|
||||
"\x06Config\x12M\n" +
|
||||
"\adomains\x18\x01 \x03(\v23.xray.transport.internet.finalmask.xdns.DomainProtoR\adomains\x12S\n" +
|
||||
"\tresolvers\x18\x02 \x03(\v25.xray.transport.internet.finalmask.xdns.ResolverProtoR\tresolvers\x12\x1d\n" +
|
||||
@@ -246,18 +277,22 @@ func file_transport_internet_finalmask_xdns_config_proto_rawDescGZIP() []byte {
|
||||
|
||||
var file_transport_internet_finalmask_xdns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||
var file_transport_internet_finalmask_xdns_config_proto_goTypes = []any{
|
||||
(*DomainProto)(nil), // 0: xray.transport.internet.finalmask.xdns.DomainProto
|
||||
(*ResolverProto)(nil), // 1: xray.transport.internet.finalmask.xdns.ResolverProto
|
||||
(*Config)(nil), // 2: xray.transport.internet.finalmask.xdns.Config
|
||||
(*DomainProto)(nil), // 0: xray.transport.internet.finalmask.xdns.DomainProto
|
||||
(*ResolverProto)(nil), // 1: xray.transport.internet.finalmask.xdns.ResolverProto
|
||||
(*Config)(nil), // 2: xray.transport.internet.finalmask.xdns.Config
|
||||
(*tls.Config)(nil), // 3: xray.transport.internet.tls.Config
|
||||
(*internet.QuicParams)(nil), // 4: xray.transport.internet.QuicParams
|
||||
}
|
||||
var file_transport_internet_finalmask_xdns_config_proto_depIdxs = []int32{
|
||||
0, // 0: xray.transport.internet.finalmask.xdns.Config.domains:type_name -> xray.transport.internet.finalmask.xdns.DomainProto
|
||||
1, // 1: xray.transport.internet.finalmask.xdns.Config.resolvers:type_name -> xray.transport.internet.finalmask.xdns.ResolverProto
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
3, // 0: xray.transport.internet.finalmask.xdns.ResolverProto.tls_config:type_name -> xray.transport.internet.tls.Config
|
||||
4, // 1: xray.transport.internet.finalmask.xdns.ResolverProto.quic_config:type_name -> xray.transport.internet.QuicParams
|
||||
0, // 2: xray.transport.internet.finalmask.xdns.Config.domains:type_name -> xray.transport.internet.finalmask.xdns.DomainProto
|
||||
1, // 3: xray.transport.internet.finalmask.xdns.Config.resolvers:type_name -> xray.transport.internet.finalmask.xdns.ResolverProto
|
||||
4, // [4:4] is the sub-list for method output_type
|
||||
4, // [4:4] is the sub-list for method input_type
|
||||
4, // [4:4] is the sub-list for extension type_name
|
||||
4, // [4:4] is the sub-list for extension extendee
|
||||
0, // [0:4] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_transport_internet_finalmask_xdns_config_proto_init() }
|
||||
|
||||
@@ -7,6 +7,8 @@ option java_package = "com.xray.transport.internet.finalmask.xdns";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common/serial/typed_message.proto";
|
||||
import "transport/internet/tls/config.proto";
|
||||
import "transport/internet/config.proto";
|
||||
|
||||
message DomainProto {
|
||||
string name = 1;
|
||||
@@ -19,6 +21,9 @@ message DomainProto {
|
||||
message ResolverProto {
|
||||
string type = 1;
|
||||
string addr = 2;
|
||||
int32 worker = 3;
|
||||
xray.transport.internet.tls.Config tls_config = 4;
|
||||
xray.transport.internet.QuicParams quic_config = 5;
|
||||
}
|
||||
|
||||
message Config {
|
||||
|
||||
@@ -20,6 +20,8 @@ func NewResolver(config *ResolverProto, dialer *finalmask.Dialer) (Resolver, err
|
||||
return NewTCPResolver(config, dialer)
|
||||
case "udp":
|
||||
return NewUDPResolver(config, dialer)
|
||||
case "https":
|
||||
return NewDOHResolver(config, dialer)
|
||||
default:
|
||||
return nil, errors.New("unknown type")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
package xdns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
gotls "crypto/tls"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/apernet/quic-go/http3"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/congestion"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
)
|
||||
|
||||
type DOHResolver struct {
|
||||
ctx context.Context
|
||||
cancel func()
|
||||
client *http.Client
|
||||
url string
|
||||
time time.Time
|
||||
|
||||
udpAddr *net.UDPAddr
|
||||
readCh chan []byte
|
||||
sendCh chan []byte
|
||||
closeCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewDOHResolver(config *ResolverProto, dialer *finalmask.Dialer) (Resolver, error) {
|
||||
if config.Worker < 1 {
|
||||
return nil, errors.New("invalid worker")
|
||||
}
|
||||
if config.TlsConfig == nil {
|
||||
return nil, errors.New("empty tls config")
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var tr http.RoundTripper
|
||||
var setaddr func(addr net.Addr)
|
||||
switch {
|
||||
case len(config.TlsConfig.NextProtocol) == 1 && config.TlsConfig.NextProtocol[0] == "h3":
|
||||
quicParams := config.QuicConfig
|
||||
if quicParams == nil {
|
||||
quicParams = &internet.QuicParams{}
|
||||
}
|
||||
quicConfig := &quic.Config{
|
||||
InitialStreamReceiveWindow: quicParams.InitStreamReceiveWindow,
|
||||
MaxStreamReceiveWindow: quicParams.MaxStreamReceiveWindow,
|
||||
InitialConnectionReceiveWindow: quicParams.InitConnReceiveWindow,
|
||||
MaxConnectionReceiveWindow: quicParams.MaxConnReceiveWindow,
|
||||
MaxIdleTimeout: time.Duration(quicParams.MaxIdleTimeout) * time.Second,
|
||||
KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second,
|
||||
DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"),
|
||||
ChromeParrot: !quicParams.DisableChromeParrot,
|
||||
}
|
||||
tr = &http3.Transport{
|
||||
TLSClientConfig: config.TlsConfig.GetTLSConfig(),
|
||||
QUICConfig: quicConfig,
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *gotls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
||||
dest, err := net.ParseDestination("udp:" + addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := dialer.DialUDP(dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setaddr(conn.RemoteAddr())
|
||||
tr := &quic.Transport{Conn: conn.(*net.PacketConnWrapper).PacketConn, DisableGSO: quicParams.DisableGSO}
|
||||
if !quicParams.DisableChromeParrot {
|
||||
tr.ConnectionIDGenerator = quic.ZeroLengthConnectionIDGenerator{}
|
||||
tlsCfg.GetCertificate = nil
|
||||
}
|
||||
qc, err := tr.Dial(ctx, conn.RemoteAddr(), tlsCfg, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
context.AfterFunc(qc.Context(), func() { tr.Close(); conn.Close() })
|
||||
switch quicParams.Congestion {
|
||||
case "", "reno":
|
||||
case "bbr":
|
||||
congestion.UseBBR(qc, bbr.Profile(quicParams.BbrProfile))
|
||||
case "force-brutal":
|
||||
congestion.UseBrutal(qc, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation)
|
||||
default:
|
||||
panic(quicParams.Congestion)
|
||||
}
|
||||
return qc, nil
|
||||
},
|
||||
}
|
||||
default:
|
||||
tlsConfig := config.TlsConfig.GetTLSConfig()
|
||||
tr = &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dest, err := net.ParseDestination("tcp:" + addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := dialer.DialTCP(dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setaddr(conn.RemoteAddr())
|
||||
var tlsConn net.Conn
|
||||
if fingerprint := tls.GetFingerprint(config.TlsConfig.Fingerprint); fingerprint != nil {
|
||||
tlsConn = tls.UClient(conn, tlsConfig, fingerprint)
|
||||
err = tlsConn.(*tls.UConn).HandshakeContext(ctx)
|
||||
} else {
|
||||
tlsConn = gotls.Client(conn, tlsConfig)
|
||||
err = tlsConn.(*gotls.Conn).HandshakeContext(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
r := &DOHResolver{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
client: client,
|
||||
url: config.Addr,
|
||||
|
||||
udpAddr: &net.UDPAddr{IP: []byte{0, 0, 0, 0}},
|
||||
readCh: make(chan []byte),
|
||||
sendCh: make(chan []byte),
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
for range config.Worker {
|
||||
r.wg.Add(1)
|
||||
go r.worker()
|
||||
}
|
||||
setaddr = r.setaddr
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *DOHResolver) closed() bool {
|
||||
select {
|
||||
case <-r.closeCh:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DOHResolver) setaddr(addr net.Addr) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
switch v := addr.(type) {
|
||||
case *net.TCPAddr:
|
||||
r.udpAddr = &net.UDPAddr{IP: v.IP, Port: v.Port}
|
||||
case *net.UDPAddr:
|
||||
r.udpAddr = v
|
||||
default:
|
||||
panic(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DOHResolver) cooldown(t time.Time) bool {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if t == (time.Time{}) {
|
||||
return time.Now().Before(r.time)
|
||||
}
|
||||
if t.After(r.time) {
|
||||
r.time = t
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *DOHResolver) do(p []byte) error {
|
||||
req := common.Must2(http.NewRequestWithContext(r.ctx, "POST", r.url, bytes.NewReader(p)))
|
||||
req.Header.Set("Accept", "application/dns-message")
|
||||
req.Header.Set("Content-Type", "application/dns-message")
|
||||
req.Header.Set("User-Agent", "")
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
p := pool4K.Get().([]byte)
|
||||
off := 0
|
||||
for {
|
||||
n, err := resp.Body.Read(p[off:])
|
||||
if n > 0 {
|
||||
off += n
|
||||
}
|
||||
if off == len(p) || err != nil {
|
||||
if err != io.EOF {
|
||||
pool4K.Put(p[:cap(p)])
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-r.closeCh:
|
||||
pool4K.Put(p[:cap(p)])
|
||||
case r.readCh <- p[:off]:
|
||||
}
|
||||
return nil
|
||||
case http.StatusTooManyRequests:
|
||||
if value := resp.Header.Get("Retry-After"); value != "" {
|
||||
if t, err := http.ParseTime(value); err == nil {
|
||||
r.cooldown(t)
|
||||
} else if i, err := strconv.ParseUint(value, 10, 32); err == nil {
|
||||
r.cooldown(time.Now().Add(time.Duration(i) * time.Second))
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.New("code ", resp.StatusCode, " header ", resp.Header)
|
||||
}
|
||||
|
||||
func (r *DOHResolver) worker() {
|
||||
defer r.wg.Done()
|
||||
|
||||
var p []byte
|
||||
for {
|
||||
select {
|
||||
case <-r.closeCh:
|
||||
return
|
||||
default:
|
||||
select {
|
||||
case <-r.closeCh:
|
||||
return
|
||||
case p = <-r.sendCh:
|
||||
}
|
||||
}
|
||||
if r.cooldown(time.Time{}) {
|
||||
continue
|
||||
}
|
||||
if err := r.do(p); err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "do err")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DOHResolver) Addr() *net.UDPAddr {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.udpAddr
|
||||
}
|
||||
|
||||
func (r *DOHResolver) Read(p []byte) (n int, err error) {
|
||||
packet, ok := <-r.readCh
|
||||
if ok {
|
||||
n = copy(p, packet)
|
||||
pool4K.Put(packet[:cap(packet)])
|
||||
return n, nil
|
||||
}
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
func (r *DOHResolver) Send(p []byte) {
|
||||
select {
|
||||
case <-r.closeCh:
|
||||
return
|
||||
default:
|
||||
b := make([]byte, len(p))
|
||||
copy(b, p)
|
||||
select {
|
||||
case <-r.closeCh:
|
||||
return
|
||||
case r.sendCh <- b:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DOHResolver) Close() {
|
||||
r.mu.Lock()
|
||||
if r.closed() {
|
||||
r.mu.Unlock()
|
||||
return
|
||||
}
|
||||
close(r.closeCh)
|
||||
r.mu.Unlock()
|
||||
r.cancel()
|
||||
r.wg.Wait()
|
||||
r.client.CloseIdleConnections()
|
||||
close(r.readCh)
|
||||
// close(r.sendCh)
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// grpcUtlsInfo contains the auth information for a TLS authenticated connection.
|
||||
// It implements the AuthInfo interface.
|
||||
type grpcUtlsInfo struct {
|
||||
State utls.ConnectionState
|
||||
State gotls.ConnectionState
|
||||
credentials.CommonAuthInfo
|
||||
// This API is experimental.
|
||||
SPIFFEID *url.URL
|
||||
|
||||
@@ -154,6 +154,24 @@ func (c *UConn) NegotiatedProtocol() string {
|
||||
return state.NegotiatedProtocol
|
||||
}
|
||||
|
||||
func (c *UConn) ConnectionState() tls.ConnectionState {
|
||||
s := c.UConn.ConnectionState()
|
||||
var state tls.ConnectionState
|
||||
state.HandshakeComplete = s.HandshakeComplete
|
||||
state.Version = s.Version
|
||||
state.NegotiatedProtocol = s.NegotiatedProtocol
|
||||
state.DidResume = s.DidResume
|
||||
state.ServerName = s.ServerName
|
||||
state.CipherSuite = s.CipherSuite
|
||||
state.PeerCertificates = s.PeerCertificates
|
||||
state.VerifiedChains = s.VerifiedChains
|
||||
state.SignedCertificateTimestamps = s.SignedCertificateTimestamps
|
||||
state.OCSPResponse = s.OCSPResponse
|
||||
state.TLSUnique = s.TLSUnique
|
||||
state.ECHAccepted = s.ECHAccepted
|
||||
return state
|
||||
}
|
||||
|
||||
func UClient(c net.Conn, config *tls.Config, fingerprint *utls.ClientHelloID) net.Conn {
|
||||
utlsConn := utls.UClient(c, copyConfig(config), *fingerprint)
|
||||
return &UConn{UConn: utlsConn}
|
||||
|
||||
Reference in New Issue
Block a user