Files
Xray-core/transport/internet/masque/dialer.go
T

295 lines
8.8 KiB
Go

package masque
import (
"context"
"net/http"
"net/netip"
"reflect"
"runtime"
"slices"
"strconv"
"strings"
"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/common/net/cnc"
"github.com/xtls/xray-core/common/utils"
"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/masque/connectip"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
"golang.org/x/net/http2"
)
const (
MinPacketSize = 1280
initialPacketSize = 1350
)
func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
tlsConfig := tls.ConfigFromStreamSettings(streamSettings)
if tlsConfig == nil {
return nil, errors.New("tls config is nil")
}
config := streamSettings.ProtocolSettings.(*Config)
if usesHTTP2(tlsConfig) {
return dialHTTP2(ctx, dest, streamSettings, tlsConfig, config)
}
dest.Network = net.Network_UDP
gotlsConfig := tlsConfig.GetTLSConfig(tls.WithDestination(dest))
gotlsConfig.NextProtos = []string{http3.NextProtoH3}
quicParams := streamSettings.QuicParams
if quicParams == nil {
quicParams = &internet.QuicParams{
BbrProfile: string(bbr.ProfileStandard),
}
}
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,
MaxIncomingStreams: -1,
InitialPacketSize: initialPacketSize,
DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"),
EnableDatagrams: true,
DisablePathManager: true,
}
if quicParams.MaxIdleTimeout == 0 {
quicConfig.MaxIdleTimeout = 30 * time.Second
}
if quicParams.KeepAlivePeriod == 0 {
quicConfig.KeepAlivePeriod = net.QuicgoH3KeepAlivePeriod
}
var pktConn net.PacketConn
var udpAddr net.Addr
if streamSettings.FinalMask != nil {
conn, err := streamSettings.FinalMask.DialUDP(ctx, dest)
if err != nil {
return nil, errors.New("failed to dial to dest").Base(err)
}
pktConn = conn.(*finalmask.PacketConnWrapper).PacketConn
udpAddr = conn.RemoteAddr()
} else {
conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
if err != nil {
return nil, errors.New("failed to dial to dest").Base(err)
}
switch c := conn.(type) {
case *internet.PacketConnWrapper:
pktConn = c.PacketConn
udpAddr = c.RemoteAddr()
case *cnc.Connection:
pktConn = &internet.FakePacketConn{Conn: c}
udpAddr = &net.UDPAddr{IP: []byte{0, 0, 0, 0}}
default:
panic(reflect.TypeOf(c))
}
}
tr := &quic.Transport{Conn: pktConn, DisableGSO: quicParams.DisableGSO}
qconn, err := tr.Dial(ctx, udpAddr, gotlsConfig, quicConfig)
if err != nil {
tr.Close()
pktConn.Close()
return nil, err
}
context.AfterFunc(qconn.Context(), func() { tr.Close(); pktConn.Close() })
switch quicParams.Congestion {
case "reno":
case "", "bbr", "brutal":
congestion.UseBBR(qconn, bbr.Profile(quicParams.BbrProfile))
case "force-brutal":
congestion.UseBrutal(qconn, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation)
default:
qconn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeNoError), "")
return nil, errors.New("unknown congestion control: ", quicParams.Congestion)
}
cc := (&http3.Transport{EnableDatagrams: true, DisableCompression: true}).NewClientConn(qconn)
conn, err := establish(ctx, connectip.NewClientConn(cc), quicConn{qconn}, func() {
qconn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeRequestCanceled), "")
}, config, authority(config, gotlsConfig.ServerName, dest.Port))
if err != nil {
qconn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeNoError), "")
return nil, err
}
return conn, nil
}
func usesHTTP2(config *tls.Config) bool {
return slices.Contains(config.NextProtocol, http2.NextProtoTLS) && !slices.Contains(config.NextProtocol, http3.NextProtoH3)
}
func dialHTTP2(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig, tlsConfig *tls.Config, config *Config) (stat.Connection, error) {
dest.Network = net.Network_TCP
gotlsConfig := tlsConfig.GetTLSConfig(tls.WithDestination(dest))
var conn net.Conn
var err error
if streamSettings.FinalMask != nil {
conn, err = streamSettings.FinalMask.DialTCP(ctx, dest)
} else {
conn, err = internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
}
if err != nil {
return nil, errors.New("failed to dial to dest").Base(err)
}
if fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint); fingerprint != nil {
conn = tls.UClient(conn, gotlsConfig, fingerprint)
} else {
conn = tls.Client(conn, gotlsConfig)
}
tlsConn := conn.(tls.Interface)
if err := tlsConn.HandshakeContext(ctx); err != nil {
conn.Close()
return nil, err
}
if protocol := tlsConn.NegotiatedProtocol(); protocol != http2.NextProtoTLS {
conn.Close()
return nil, errors.New("the server negotiated ", strconv.Quote(protocol), " instead of h2")
}
cc, err := newHTTP2ClientConn(conn)
if err != nil {
conn.Close()
return nil, err
}
mconn, err := establish(ctx, connectip.NewHTTP2ClientConn(cc), cc, func() { cc.Close() }, config, authority(config, gotlsConfig.ServerName, dest.Port))
if err != nil {
cc.Close()
return nil, err
}
return mconn, nil
}
type tunnelClient interface {
Dial(*connectip.Request) (*connectip.Conn, *http.Response, error)
}
func establish(ctx context.Context, client tunnelClient, hconn httpConn, abort func(), config *Config, host string) (*Conn, error) {
stop := context.AfterFunc(ctx, abort)
defer stop()
req, err := connectip.NewRequest(ctx, "https://"+host+config.Path)
if err != nil {
return nil, err
}
header := req.Header()
for k, v := range config.Headers {
header.Set(k, v)
}
switch header.Get("User-Agent") {
case "":
header["User-Agent"] = nil
case "chrome":
header.Set("User-Agent", utils.ChromeUA)
case "firefox":
header.Set("User-Agent", utils.FirefoxUA)
case "safari":
header.Set("User-Agent", utils.SafariUA)
case "edge":
header.Set("User-Agent", utils.MSEdgeUA)
case "curl":
header.Set("User-Agent", utils.CurlUA)
case "golang":
header.Del("User-Agent")
}
ipConn, _, err := client.Dial(req)
if err != nil {
if ctx.Err() != nil {
err = context.Cause(ctx)
}
return nil, errors.New("CONNECT-IP request failed").Base(err)
}
if n := ipConn.MaxPacketSize(); n < MinPacketSize {
ipConn.Close()
return nil, errors.New("the tunnel can only carry ", n, "-byte packets, less than ", MinPacketSize)
}
if _, err := ipConn.RequestAddresses([]netip.Prefix{
netip.PrefixFrom(netip.IPv4Unspecified(), 32),
netip.PrefixFrom(netip.IPv6Unspecified(), 128),
}); err != nil {
ipConn.Close()
return nil, err
}
var local []netip.Addr
for len(local) == 0 {
assigned, err := ipConn.ReceiveAddressAssignment(ctx)
if err != nil {
ipConn.Close()
return nil, errors.New("no address assigned").Base(err)
}
local = localAddrs(assigned)
}
if !stop() {
ipConn.Close()
return nil, errors.New("no address assigned").Base(context.Cause(ctx))
}
conn := &Conn{
ipConn: ipConn,
httpConn: hconn,
local: local,
}
go conn.serveAddressAssignments()
go conn.serveAddressRequests()
return conn, nil
}
func localAddrs(assigned []connectip.AssignedAddress) []netip.Addr {
var local []netip.Addr
var has4, has6 bool
for _, a := range assigned {
if a.Rejected() {
continue
}
addr := a.IPPrefix.Addr()
if a.IPPrefix.Bits() != addr.BitLen() {
addr = a.IPPrefix.Masked().Addr().Next()
}
if addr.Is4() && !has4 {
has4 = true
local = append(local, addr)
} else if addr.Is6() && !has6 {
has6 = true
local = append(local, addr)
}
}
return local
}
func authority(config *Config, serverName string, port net.Port) string {
if config.Host != "" {
return config.Host
}
host := strings.TrimSuffix(strings.TrimPrefix(serverName, "["), "]")
if port == 443 {
if addr, err := netip.ParseAddr(host); err == nil && addr.Is6() {
return "[" + host + "]"
}
return host
}
return net.JoinHostPort(host, port.String())
}
func init() {
common.Must(internet.RegisterTransportDialer(protocolName, Dial))
}