mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-30 13:05:43 +00:00
https://github.com/XTLS/Xray-core/pull/6807#issuecomment-5808933074 https://github.com/XTLS/Xray-core/pull/6810#issuecomment-5842441136
295 lines
8.8 KiB
Go
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))
|
|
}
|