Files

238 lines
7.0 KiB
Go

package masque
import (
"context"
"net/netip"
"reflect"
"runtime"
"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"
)
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)
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)
}
conn, err := establish(ctx, qconn, config, authority(config, gotlsConfig.ServerName, dest.Port))
if err != nil {
qconn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeNoError), "")
return nil, err
}
return conn, nil
}
func establish(ctx context.Context, qconn *quic.Conn, config *Config, host string) (*Conn, error) {
stop := context.AfterFunc(ctx, func() {
qconn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeRequestCanceled), "")
})
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")
}
cc := (&http3.Transport{EnableDatagrams: true, DisableCompression: true}).NewClientConn(qconn)
ipConn, _, err := connectip.NewClientConn(cc).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,
quicConn: qconn,
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))
}