diff --git a/go.mod b/go.mod index b56e9c4bf..bc2c23844 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/xtls/xray-core go 1.26 require ( - github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 + github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e github.com/cloudflare/circl v1.6.5 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/golang/mock v1.7.0-rc.1 @@ -11,6 +11,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/klauspost/cpuid/v2 v2.4.0 + github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138 github.com/miekg/dns v1.1.73 github.com/pelletier/go-toml v1.9.5 github.com/pion/stun/v3 v3.1.7 @@ -41,9 +42,14 @@ require ( require ( github.com/andybalholm/brotli v1.0.6 // indirect github.com/google/btree v1.1.2 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/huin/goupnp v1.2.0 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/juju/ratelimit v1.0.2 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/koron/go-ssdp v0.0.4 // indirect github.com/kr/text v0.2.0 // indirect + github.com/libp2p/go-netroute v0.2.1 // indirect github.com/pion/dtls/v3 v3.1.5 // indirect github.com/pion/logging v0.2.4 // indirect github.com/pion/transport/v4 v4.1.0 // indirect diff --git a/go.sum b/go.sum index 29504f710..a7ac551bf 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I= -github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= +github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg= +github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA= @@ -21,22 +21,34 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI= github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c= +github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY= +github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= +github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138 h1:YohuNPT/1k3VcThCQlBZ43PCPWPfMRS1zcxWBF2SLK8= +github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138/go.mod h1:TXQg5tfSy+bUjnhT5728j5j/MBj7keIYqqZ1+8k/ui8= +github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= +github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= @@ -53,6 +65,8 @@ github.com/pion/transport/v4 v4.1.0 h1:8S+nF2reM2cJuqC6g78OVy2BBgmbdns+acx3jA97B github.com/pion/transport/v4 v4.1.0/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI= github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24= +github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= +github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs= @@ -100,6 +114,8 @@ golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -129,6 +145,7 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go index e4c579dda..db57fdbbd 100644 --- a/infra/conf/transport_finalmask.go +++ b/infra/conf/transport_finalmask.go @@ -816,9 +816,11 @@ func (c *Xicmp) Build() (proto.Message, error) { } type Realm struct { - Url string `json:"url"` - StunServers []string `json:"stunServers"` - TlsConfig *TLSConfig `json:"tlsConfig"` + Url string `json:"url"` + StunServers []string `json:"stunServers"` + TlsConfig *TLSConfig `json:"tlsConfig"` + IPMode string `json:"ipMode"` + PortMapping *realm.PortMapping `json:"portMapping"` } func (c *Realm) Build() (proto.Message, error) { @@ -898,6 +900,8 @@ func (c *Realm) Build() (proto.Message, error) { ID: id, StunServers: stunServers, TlsConfig: tlsConfig, + IPMode: strings.ToLower(c.IPMode), + PortMapping: c.PortMapping, }, nil } @@ -928,20 +932,24 @@ func (c *Mask) Build(tcp bool) (proto.Message, error) { } type QuicParamsConfig struct { - Congestion string `json:"congestion"` - Debug bool `json:"debug"` - BbrProfile string `json:"bbrProfile"` - BrutalUp Bandwidth `json:"brutalUp"` - BrutalDown Bandwidth `json:"brutalDown"` - UdpHop UdpHop `json:"udpHop"` - InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"` - MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"` - InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"` - MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"` - MaxIdleTimeout int64 `json:"maxIdleTimeout"` - KeepAlivePeriod int64 `json:"keepAlivePeriod"` - DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"` - MaxIncomingStreams int64 `json:"maxIncomingStreams"` + Congestion string `json:"congestion"` + Debug bool `json:"debug"` + BbrProfile string `json:"bbrProfile"` + BrutalUp Bandwidth `json:"brutalUp"` + BrutalDown Bandwidth `json:"brutalDown"` + BrutalDisableLossCompensation bool `json:"brutalDisableLossCompensation"` + UdpHop UdpHop `json:"udpHop"` + InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"` + MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"` + InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"` + MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"` + MaxIdleTimeout int64 `json:"maxIdleTimeout"` + KeepAlivePeriod int64 `json:"keepAlivePeriod"` + DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"` + DisableChromeParrot bool `json:"disableChromeParrot"` + DisableGSO bool `json:"disableGSO"` + MaxIncomingStreams int64 `json:"maxIncomingStreams"` + DisableStatelessReset bool `json:"disableStatelessReset"` } type FinalMask struct { diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 24b5fba7c..1ef986950 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -285,10 +285,11 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) { } config.QuicParams = &internet.QuicParams{ - Congestion: c.FinalMask.QuicParams.Congestion, - BbrProfile: profile, - BrutalUp: up, - BrutalDown: down, + Congestion: c.FinalMask.QuicParams.Congestion, + BbrProfile: profile, + BrutalUp: up, + BrutalDown: down, + BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation, UdpHop: &internet.UdpHop{ Ports: c.FinalMask.QuicParams.UdpHop.PortList.Build().Ports(), IntervalMin: int64(c.FinalMask.QuicParams.UdpHop.Interval.From), @@ -301,7 +302,10 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) { MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout, KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod, DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery, + DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot, + DisableGSO: c.FinalMask.QuicParams.DisableGSO, MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams, + DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset, } } } diff --git a/infra/conf/transport_method.go b/infra/conf/transport_method.go index 19cb496e3..cff77da83 100644 --- a/infra/conf/transport_method.go +++ b/infra/conf/transport_method.go @@ -751,6 +751,7 @@ type Masquerade struct { Url string `json:"url"` RewriteHost bool `json:"rewriteHost"` + XForwarded bool `json:"xForwarded"` Insecure bool `json:"insecure"` Content string `json:"content"` @@ -791,6 +792,7 @@ func (c *HysteriaConfig) Build() (proto.Message, error) { config.MasqFile = c.Masquerade.Dir config.MasqUrl = c.Masquerade.Url config.MasqUrlRewriteHost = c.Masquerade.RewriteHost + config.MasqUrlXForwarded = c.Masquerade.XForwarded config.MasqUrlInsecure = c.Masquerade.Insecure config.MasqString = c.Masquerade.Content config.MasqStringHeaders = c.Masquerade.Headers diff --git a/transport/internet/config.pb.go b/transport/internet/config.pb.go index ee696b43b..f747998e1 100644 --- a/transport/internet/config.pb.go +++ b/transport/internet/config.pb.go @@ -443,22 +443,26 @@ func (x *UdpHop) GetIntervalMax() int64 { } type QuicParams struct { - state protoimpl.MessageState `protogen:"open.v1"` - Congestion string `protobuf:"bytes,1,opt,name=congestion,proto3" json:"congestion,omitempty"` - BbrProfile string `protobuf:"bytes,2,opt,name=bbr_profile,json=bbrProfile,proto3" json:"bbr_profile,omitempty"` - BrutalUp uint64 `protobuf:"varint,3,opt,name=brutal_up,json=brutalUp,proto3" json:"brutal_up,omitempty"` - BrutalDown uint64 `protobuf:"varint,4,opt,name=brutal_down,json=brutalDown,proto3" json:"brutal_down,omitempty"` - UdpHop *UdpHop `protobuf:"bytes,5,opt,name=udp_hop,json=udpHop,proto3" json:"udp_hop,omitempty"` - InitStreamReceiveWindow uint64 `protobuf:"varint,6,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"` - MaxStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"` - InitConnReceiveWindow uint64 `protobuf:"varint,8,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"` - MaxConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"` - MaxIdleTimeout int64 `protobuf:"varint,10,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"` - KeepAlivePeriod int64 `protobuf:"varint,11,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"` - DisablePathMtuDiscovery bool `protobuf:"varint,12,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"` - MaxIncomingStreams int64 `protobuf:"varint,13,opt,name=max_incoming_streams,json=maxIncomingStreams,proto3" json:"max_incoming_streams,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Congestion string `protobuf:"bytes,1,opt,name=congestion,proto3" json:"congestion,omitempty"` + BbrProfile string `protobuf:"bytes,2,opt,name=bbr_profile,json=bbrProfile,proto3" json:"bbr_profile,omitempty"` + BrutalUp uint64 `protobuf:"varint,3,opt,name=brutal_up,json=brutalUp,proto3" json:"brutal_up,omitempty"` + BrutalDown uint64 `protobuf:"varint,4,opt,name=brutal_down,json=brutalDown,proto3" json:"brutal_down,omitempty"` + BrutalDisableLossCompensation bool `protobuf:"varint,5,opt,name=brutal_disable_loss_compensation,json=brutalDisableLossCompensation,proto3" json:"brutal_disable_loss_compensation,omitempty"` + UdpHop *UdpHop `protobuf:"bytes,6,opt,name=udp_hop,json=udpHop,proto3" json:"udp_hop,omitempty"` + InitStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"` + MaxStreamReceiveWindow uint64 `protobuf:"varint,8,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"` + InitConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"` + MaxConnReceiveWindow uint64 `protobuf:"varint,10,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"` + MaxIdleTimeout int64 `protobuf:"varint,11,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"` + KeepAlivePeriod int64 `protobuf:"varint,12,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"` + DisablePathMtuDiscovery bool `protobuf:"varint,13,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"` + DisableChromeParrot bool `protobuf:"varint,14,opt,name=disable_chrome_parrot,json=disableChromeParrot,proto3" json:"disable_chrome_parrot,omitempty"` + DisableGSO bool `protobuf:"varint,15,opt,name=disableGSO,proto3" json:"disableGSO,omitempty"` + MaxIncomingStreams int64 `protobuf:"varint,16,opt,name=max_incoming_streams,json=maxIncomingStreams,proto3" json:"max_incoming_streams,omitempty"` + DisableStatelessReset bool `protobuf:"varint,17,opt,name=disable_stateless_reset,json=disableStatelessReset,proto3" json:"disable_stateless_reset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *QuicParams) Reset() { @@ -519,6 +523,13 @@ func (x *QuicParams) GetBrutalDown() uint64 { return 0 } +func (x *QuicParams) GetBrutalDisableLossCompensation() bool { + if x != nil { + return x.BrutalDisableLossCompensation + } + return false +} + func (x *QuicParams) GetUdpHop() *UdpHop { if x != nil { return x.UdpHop @@ -575,6 +586,20 @@ func (x *QuicParams) GetDisablePathMtuDiscovery() bool { return false } +func (x *QuicParams) GetDisableChromeParrot() bool { + if x != nil { + return x.DisableChromeParrot + } + return false +} + +func (x *QuicParams) GetDisableGSO() bool { + if x != nil { + return x.DisableGSO + } + return false +} + func (x *QuicParams) GetMaxIncomingStreams() int64 { if x != nil { return x.MaxIncomingStreams @@ -582,6 +607,13 @@ func (x *QuicParams) GetMaxIncomingStreams() int64 { return 0 } +func (x *QuicParams) GetDisableStatelessReset() bool { + if x != nil { + return x.DisableStatelessReset + } + return false +} + type ProxyConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` @@ -1020,7 +1052,7 @@ const file_transport_internet_config_proto_rawDesc = "" + "\x06UdpHop\x12\x14\n" + "\x05ports\x18\x01 \x03(\rR\x05ports\x12!\n" + "\finterval_min\x18\x02 \x01(\x03R\vintervalMin\x12!\n" + - "\finterval_max\x18\x03 \x01(\x03R\vintervalMax\"\xf2\x04\n" + + "\finterval_max\x18\x03 \x01(\x03R\vintervalMax\"\xc7\x06\n" + "\n" + "QuicParams\x12\x1e\n" + "\n" + @@ -1030,17 +1062,23 @@ const file_transport_internet_config_proto_rawDesc = "" + "bbrProfile\x12\x1b\n" + "\tbrutal_up\x18\x03 \x01(\x04R\bbrutalUp\x12\x1f\n" + "\vbrutal_down\x18\x04 \x01(\x04R\n" + - "brutalDown\x128\n" + - "\audp_hop\x18\x05 \x01(\v2\x1f.xray.transport.internet.UdpHopR\x06udpHop\x12;\n" + - "\x1ainit_stream_receive_window\x18\x06 \x01(\x04R\x17initStreamReceiveWindow\x129\n" + - "\x19max_stream_receive_window\x18\a \x01(\x04R\x16maxStreamReceiveWindow\x127\n" + - "\x18init_conn_receive_window\x18\b \x01(\x04R\x15initConnReceiveWindow\x125\n" + - "\x17max_conn_receive_window\x18\t \x01(\x04R\x14maxConnReceiveWindow\x12(\n" + - "\x10max_idle_timeout\x18\n" + - " \x01(\x03R\x0emaxIdleTimeout\x12*\n" + - "\x11keep_alive_period\x18\v \x01(\x03R\x0fkeepAlivePeriod\x12;\n" + - "\x1adisable_path_mtu_discovery\x18\f \x01(\bR\x17disablePathMtuDiscovery\x120\n" + - "\x14max_incoming_streams\x18\r \x01(\x03R\x12maxIncomingStreams\"Q\n" + + "brutalDown\x12G\n" + + " brutal_disable_loss_compensation\x18\x05 \x01(\bR\x1dbrutalDisableLossCompensation\x128\n" + + "\audp_hop\x18\x06 \x01(\v2\x1f.xray.transport.internet.UdpHopR\x06udpHop\x12;\n" + + "\x1ainit_stream_receive_window\x18\a \x01(\x04R\x17initStreamReceiveWindow\x129\n" + + "\x19max_stream_receive_window\x18\b \x01(\x04R\x16maxStreamReceiveWindow\x127\n" + + "\x18init_conn_receive_window\x18\t \x01(\x04R\x15initConnReceiveWindow\x125\n" + + "\x17max_conn_receive_window\x18\n" + + " \x01(\x04R\x14maxConnReceiveWindow\x12(\n" + + "\x10max_idle_timeout\x18\v \x01(\x03R\x0emaxIdleTimeout\x12*\n" + + "\x11keep_alive_period\x18\f \x01(\x03R\x0fkeepAlivePeriod\x12;\n" + + "\x1adisable_path_mtu_discovery\x18\r \x01(\bR\x17disablePathMtuDiscovery\x122\n" + + "\x15disable_chrome_parrot\x18\x0e \x01(\bR\x13disableChromeParrot\x12\x1e\n" + + "\n" + + "disableGSO\x18\x0f \x01(\bR\n" + + "disableGSO\x120\n" + + "\x14max_incoming_streams\x18\x10 \x01(\x03R\x12maxIncomingStreams\x126\n" + + "\x17disable_stateless_reset\x18\x11 \x01(\bR\x15disableStatelessReset\"Q\n" + "\vProxyConfig\x12\x10\n" + "\x03tag\x18\x01 \x01(\tR\x03tag\x120\n" + "\x13transportLayerProxy\x18\x02 \x01(\bR\x13transportLayerProxy\"\x93\x01\n" + diff --git a/transport/internet/config.proto b/transport/internet/config.proto index c139111aa..58b4f21b2 100644 --- a/transport/internet/config.proto +++ b/transport/internet/config.proto @@ -75,15 +75,19 @@ message QuicParams { string bbr_profile = 2; uint64 brutal_up = 3; uint64 brutal_down = 4; - UdpHop udp_hop = 5; - uint64 init_stream_receive_window = 6; - uint64 max_stream_receive_window = 7; - uint64 init_conn_receive_window = 8; - uint64 max_conn_receive_window = 9; - int64 max_idle_timeout = 10; - int64 keep_alive_period = 11; - bool disable_path_mtu_discovery = 12; - int64 max_incoming_streams = 13; + bool brutal_disable_loss_compensation = 5; + UdpHop udp_hop = 6; + uint64 init_stream_receive_window = 7; + uint64 max_stream_receive_window = 8; + uint64 init_conn_receive_window = 9; + uint64 max_conn_receive_window = 10; + int64 max_idle_timeout = 11; + int64 keep_alive_period = 12; + bool disable_path_mtu_discovery = 13; + bool disable_chrome_parrot = 14; + bool disableGSO = 15; + int64 max_incoming_streams = 16; + bool disable_stateless_reset = 17; } message ProxyConfig { diff --git a/transport/internet/finalmask/realm/client.go b/transport/internet/finalmask/realm/client.go index 5b2354a58..b47b9d57e 100644 --- a/transport/internet/finalmask/realm/client.go +++ b/transport/internet/finalmask/realm/client.go @@ -7,6 +7,7 @@ import ( "net/netip" "slices" "strings" + "sync" "time" "github.com/pion/stun/v3" @@ -15,35 +16,67 @@ import ( ) type realmConnClient struct { + wg sync.WaitGroup + ctx context.Context + cancel context.CancelFunc net.PacketConn peer *net.UDPAddr realmClient *Client realmID string stunServers []string + family Family + mapper *PortMapper stunTimeout time.Duration punchTimeout time.Duration punchInterval time.Duration } func NewConnClient(config *Config, raw net.PacketConn) (net.PacketConn, error) { + ctx, cancel := context.WithCancel(context.Background()) + + family := Family_Dual + switch config.IPMode { + case "dual": + case "v4": + family = Family_V4 + case "v6": + family = Family_V6 + } + + var mapper *PortMapper + if config.PortMapping != nil && config.PortMapping.Enabled { + var err error + start := time.Now() + mapper, err = NewPortMapper(context.Background(), raw.LocalAddr().(*net.UDPAddr).Port, PortMapConfig{Timeout: time.Duration(config.PortMapping.Timeout) * time.Second, Lifetime: time.Duration(config.PortMapping.Lifetime) * time.Second}) + if err != nil { + errors.LogErrorInner(context.Background(), err, "[realm] [port mapping] [", raw.LocalAddr().(*net.UDPAddr).Port, "] init failed after ", time.Since(start)) + } else { + errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] gateway ", mapper.GatewayType(), ", external ", mapper.ExternalAddr()) + errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] init success with ", time.Since(start)) + } + } + conn := &realmConnClient{ + ctx: ctx, + cancel: cancel, PacketConn: raw, realmClient: NewClient(config.Scheme, config.Host, config.Port, config.Token, config.TlsConfig), realmID: config.ID, stunServers: config.StunServers, + family: family, + mapper: mapper, stunTimeout: defaultSTUNTimeout, punchTimeout: defaultPunchTimeout, punchInterval: defaultPunchInterval, } - return conn.getpeer() } func (c *realmConnClient) getpeer() (net.PacketConn, error) { start := time.Now() - servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers) + servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers, c.family) errors.LogDebug(context.Background(), "[realm] update stun servers ", servers, " with ", time.Since(start)) if len(servers) == 0 { return nil, errors.New("empty locals") @@ -70,7 +103,7 @@ func (c *realmConnClient) getpeer() (net.PacketConn, error) { peers, _ := parseAddrPorts(resp.Addresses) errors.LogDebug(context.Background(), "[realm] update peers ", peers) - filteredPeers, seen := candidatePunchAddrs(locals, peers) + filteredPeers, seen := candidatePunchAddrs(locals, peers, c.family) errors.LogDebug(context.Background(), "[realm] filtered peers ", filteredPeers) expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen) errors.LogDebug(context.Background(), "[realm] expanded peers ", expandedPeers) @@ -86,6 +119,11 @@ func (c *realmConnClient) getpeer() (net.PacketConn, error) { } errors.LogDebug(context.Background(), "[realm] punch peer ", peer, " with ", time.Since(start)) + if c.mapper != nil { + c.wg.Add(1) + go portMapLoop(c.ctx, c.mapper, c.wg.Done) + } + c.peer = peer return c, nil } @@ -116,10 +154,12 @@ func (c *realmConnClient) discover(servers []*net.UDPAddr) []netip.AddrPort { } } c.PacketConn.SetReadDeadline(time.Time{}) + if c.mapper != nil { + results = insertAddr(results, c.mapper.ExternalAddr()) + } slices.SortFunc(results, func(a, b netip.AddrPort) int { return strings.Compare(a.String(), b.String()) }) - return results } @@ -169,3 +209,48 @@ func (c *realmConnClient) punch(meta PunchMetadata, peers []netip.AddrPort) (*ne func (c *realmConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { return c.PacketConn.WriteTo(p, c.peer) } + +func (c *realmConnClient) Close() error { + // Sadly, closing the core does not first close the sockets created for outbound connections + c.cancel() + c.wg.Wait() + return nil +} + +func portMapLoop(ctx context.Context, mapper *PortMapper, done func()) { + defer func() { + err := mapper.Close() + done() + errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] removed with ", err) + }() + interval := mapper.Lifetime() / 2 + if interval <= 0 { + interval = time.Minute + } + t := time.NewTicker(interval) + defer t.Stop() + failing := false + for { + select { + case <-ctx.Done(): + return + case <-t.C: + changed, err := mapper.Renew(ctx) + if err != nil { + if ctx.Err() != nil { + return + } + if !failing { + errors.LogError(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] renewal failed") + failing = true + } + continue + } + errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] external ", mapper.ExternalAddr(), ", changed ", changed) + if failing { + errors.LogError(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] recovered") + failing = false + } + } + } +} diff --git a/transport/internet/finalmask/realm/config.pb.go b/transport/internet/finalmask/realm/config.pb.go index ee273e732..35c94d551 100644 --- a/transport/internet/finalmask/realm/config.pb.go +++ b/transport/internet/finalmask/realm/config.pb.go @@ -22,6 +22,115 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type Family int32 + +const ( + Family_Dual Family = 0 + Family_V4 Family = 1 + Family_V6 Family = 2 +) + +// Enum value maps for Family. +var ( + Family_name = map[int32]string{ + 0: "Dual", + 1: "V4", + 2: "V6", + } + Family_value = map[string]int32{ + "Dual": 0, + "V4": 1, + "V6": 2, + } +) + +func (x Family) Enum() *Family { + p := new(Family) + *p = x + return p +} + +func (x Family) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Family) Descriptor() protoreflect.EnumDescriptor { + return file_transport_internet_finalmask_realm_config_proto_enumTypes[0].Descriptor() +} + +func (Family) Type() protoreflect.EnumType { + return &file_transport_internet_finalmask_realm_config_proto_enumTypes[0] +} + +func (x Family) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Family.Descriptor instead. +func (Family) EnumDescriptor() ([]byte, []int) { + return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{0} +} + +type PortMapping struct { + state protoimpl.MessageState `protogen:"open.v1"` + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + Timeout int64 `protobuf:"varint,2,opt,name=timeout,proto3" json:"timeout,omitempty"` + Lifetime int64 `protobuf:"varint,3,opt,name=lifetime,proto3" json:"lifetime,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PortMapping) Reset() { + *x = PortMapping{} + mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PortMapping) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortMapping) ProtoMessage() {} + +func (x *PortMapping) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PortMapping.ProtoReflect.Descriptor instead. +func (*PortMapping) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{0} +} + +func (x *PortMapping) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *PortMapping) GetTimeout() int64 { + if x != nil { + return x.Timeout + } + return 0 +} + +func (x *PortMapping) GetLifetime() int64 { + if x != nil { + return x.Lifetime + } + return 0 +} + type Config struct { state protoimpl.MessageState `protogen:"open.v1"` Scheme string `protobuf:"bytes,1,opt,name=scheme,proto3" json:"scheme,omitempty"` @@ -31,13 +140,15 @@ type Config struct { ID string `protobuf:"bytes,5,opt,name=ID,proto3" json:"ID,omitempty"` StunServers []string `protobuf:"bytes,6,rep,name=stun_servers,json=stunServers,proto3" json:"stun_servers,omitempty"` TlsConfig *tls.Config `protobuf:"bytes,7,opt,name=tls_config,json=tlsConfig,proto3" json:"tls_config,omitempty"` + IPMode string `protobuf:"bytes,8,opt,name=IPMode,proto3" json:"IPMode,omitempty"` + PortMapping *PortMapping `protobuf:"bytes,9,opt,name=port_mapping,json=portMapping,proto3" json:"port_mapping,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Config) Reset() { *x = Config{} - mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0] + mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -49,7 +160,7 @@ func (x *Config) String() string { func (*Config) ProtoMessage() {} func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[0] + mi := &file_transport_internet_finalmask_realm_config_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -62,7 +173,7 @@ func (x *Config) ProtoReflect() protoreflect.Message { // Deprecated: Use Config.ProtoReflect.Descriptor instead. func (*Config) Descriptor() ([]byte, []int) { - return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{0} + return file_transport_internet_finalmask_realm_config_proto_rawDescGZIP(), []int{1} } func (x *Config) GetScheme() string { @@ -114,11 +225,29 @@ func (x *Config) GetTlsConfig() *tls.Config { return nil } +func (x *Config) GetIPMode() string { + if x != nil { + return x.IPMode + } + return "" +} + +func (x *Config) GetPortMapping() *PortMapping { + if x != nil { + return x.PortMapping + } + return nil +} + var File_transport_internet_finalmask_realm_config_proto protoreflect.FileDescriptor const file_transport_internet_finalmask_realm_config_proto_rawDesc = "" + "\n" + - "/transport/internet/finalmask/realm/config.proto\x12'xray.transport.internet.finalmask.realm\x1a#transport/internet/tls/config.proto\"\xd5\x01\n" + + "/transport/internet/finalmask/realm/config.proto\x12'xray.transport.internet.finalmask.realm\x1a#transport/internet/tls/config.proto\"]\n" + + "\vPortMapping\x12\x18\n" + + "\aenabled\x18\x01 \x01(\bR\aenabled\x12\x18\n" + + "\atimeout\x18\x02 \x01(\x03R\atimeout\x12\x1a\n" + + "\blifetime\x18\x03 \x01(\x03R\blifetime\"\xc6\x02\n" + "\x06Config\x12\x16\n" + "\x06scheme\x18\x01 \x01(\tR\x06scheme\x12\x12\n" + "\x04host\x18\x02 \x01(\tR\x04host\x12\x12\n" + @@ -127,7 +256,13 @@ const file_transport_internet_finalmask_realm_config_proto_rawDesc = "" + "\x02ID\x18\x05 \x01(\tR\x02ID\x12!\n" + "\fstun_servers\x18\x06 \x03(\tR\vstunServers\x12B\n" + "\n" + - "tls_config\x18\a \x01(\v2#.xray.transport.internet.tls.ConfigR\ttlsConfigB\x97\x01\n" + + "tls_config\x18\a \x01(\v2#.xray.transport.internet.tls.ConfigR\ttlsConfig\x12\x16\n" + + "\x06IPMode\x18\b \x01(\tR\x06IPMode\x12W\n" + + "\fport_mapping\x18\t \x01(\v24.xray.transport.internet.finalmask.realm.PortMappingR\vportMapping*\"\n" + + "\x06Family\x12\b\n" + + "\x04Dual\x10\x00\x12\x06\n" + + "\x02V4\x10\x01\x12\x06\n" + + "\x02V6\x10\x02B\x97\x01\n" + "+com.xray.transport.internet.finalmask.realmP\x01Z xray.transport.internet.tls.Config - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 3, // 0: xray.transport.internet.finalmask.realm.Config.tls_config:type_name -> xray.transport.internet.tls.Config + 1, // 1: xray.transport.internet.finalmask.realm.Config.port_mapping:type_name -> xray.transport.internet.finalmask.realm.PortMapping + 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 } func init() { file_transport_internet_finalmask_realm_config_proto_init() } @@ -166,13 +305,14 @@ func file_transport_internet_finalmask_realm_config_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_realm_config_proto_rawDesc), len(file_transport_internet_finalmask_realm_config_proto_rawDesc)), - NumEnums: 0, - NumMessages: 1, + NumEnums: 1, + NumMessages: 2, NumExtensions: 0, NumServices: 0, }, GoTypes: file_transport_internet_finalmask_realm_config_proto_goTypes, DependencyIndexes: file_transport_internet_finalmask_realm_config_proto_depIdxs, + EnumInfos: file_transport_internet_finalmask_realm_config_proto_enumTypes, MessageInfos: file_transport_internet_finalmask_realm_config_proto_msgTypes, }.Build() File_transport_internet_finalmask_realm_config_proto = out.File diff --git a/transport/internet/finalmask/realm/config.proto b/transport/internet/finalmask/realm/config.proto index 62dce86e3..75bf0f43e 100644 --- a/transport/internet/finalmask/realm/config.proto +++ b/transport/internet/finalmask/realm/config.proto @@ -8,6 +8,18 @@ option java_multiple_files = true; import "transport/internet/tls/config.proto"; +enum Family { + Dual = 0; + V4 = 1; + V6 = 2; +} + +message PortMapping { + bool enabled = 1; + int64 timeout = 2; + int64 lifetime = 3; +} + message Config { string scheme = 1; string host = 2; @@ -16,4 +28,6 @@ message Config { string ID = 5; repeated string stun_servers = 6; xray.transport.internet.tls.Config tls_config = 7; + string IPMode = 8; + PortMapping port_mapping = 9; } \ No newline at end of file diff --git a/transport/internet/finalmask/realm/portmap.go b/transport/internet/finalmask/realm/portmap.go new file mode 100644 index 000000000..edd4d172d --- /dev/null +++ b/transport/internet/finalmask/realm/portmap.go @@ -0,0 +1,139 @@ +package realm + +import ( + "context" + "errors" + "fmt" + "net/netip" + "sync" + "time" + + "github.com/libp2p/go-nat" +) + +const ( + defaultPortMapTimeout = 10 * time.Second + defaultPortMapLifetime = 10 * time.Minute + + portMapDescription = "hysteria-realm" + portMapProtocol = "udp" +) + +var ErrInvalidPortMapConfig = errors.New("invalid port mapping config") + +type PortMapConfig struct { + Timeout time.Duration + Lifetime time.Duration +} + +func (c PortMapConfig) withDefaults() (PortMapConfig, error) { + if c.Timeout == 0 { + c.Timeout = defaultPortMapTimeout + } + if c.Timeout < 0 { + return c, fmt.Errorf("%w: timeout must not be negative", ErrInvalidPortMapConfig) + } + if c.Lifetime == 0 { + c.Lifetime = defaultPortMapLifetime + } + if c.Lifetime < 0 { + return c, fmt.Errorf("%w: lifetime must not be negative", ErrInvalidPortMapConfig) + } + return c, nil +} + +// PortMapper maintains a UDP port mapping on the local gateway via UPnP or +// NAT-PMP. It does not renew the mapping by itself; the caller is expected +// to call Renew periodically (typically every Lifetime/2). +type PortMapper struct { + gateway nat.NAT + internalPort int + config PortMapConfig + + mu sync.Mutex + externalAddr netip.AddrPort +} + +// NewPortMapper discovers the local gateway and maps internalPort for UDP. +// It blocks for up to 2x config.Timeout (discovery + mapping). +func NewPortMapper(ctx context.Context, internalPort int, config PortMapConfig) (*PortMapper, error) { + if internalPort <= 0 || internalPort > 65535 { + return nil, fmt.Errorf("%w: invalid internal port %d", ErrInvalidPortMapConfig, internalPort) + } + config, err := config.withDefaults() + if err != nil { + return nil, err + } + + discoverCtx, cancel := context.WithTimeout(ctx, config.Timeout) + gateway, err := nat.DiscoverGateway(discoverCtx) + cancel() + if err != nil { + return nil, fmt.Errorf("gateway discovery failed: %w", err) + } + + m := &PortMapper{ + gateway: gateway, + internalPort: internalPort, + config: config, + } + if _, err := m.Renew(ctx); err != nil { + return nil, err + } + return m, nil +} + +// Renew (re-)requests the port mapping and refreshes the external address. +// It reports whether the external address changed since the last call. +func (m *PortMapper) Renew(ctx context.Context) (bool, error) { + opCtx, cancel := context.WithTimeout(ctx, m.config.Timeout) + defer cancel() + externalPort, err := m.gateway.AddPortMapping(opCtx, portMapProtocol, m.internalPort, portMapDescription, m.config.Lifetime) + if err != nil { + return false, fmt.Errorf("add port mapping failed: %w", err) + } + externalIP, err := m.gateway.GetExternalAddress() + if err != nil { + return false, fmt.Errorf("get external address failed: %w", err) + } + addr, ok := netip.AddrFromSlice(externalIP) + if !ok || addr.IsUnspecified() || addr.IsLoopback() { + return false, fmt.Errorf("gateway returned unusable external address: %s", externalIP) + } + externalAddr := netip.AddrPortFrom(addr.Unmap(), uint16(externalPort)) + + m.mu.Lock() + changed := externalAddr != m.externalAddr + m.externalAddr = externalAddr + m.mu.Unlock() + return changed, nil +} + +// ExternalAddr returns the gateway's external IP and the mapped external port. +func (m *PortMapper) ExternalAddr() netip.AddrPort { + m.mu.Lock() + defer m.mu.Unlock() + return m.externalAddr +} + +// InternalPort returns the mapped local UDP port. +func (m *PortMapper) InternalPort() int { + return m.internalPort +} + +// Lifetime returns the effective mapping lease duration. +func (m *PortMapper) Lifetime() time.Duration { + return m.config.Lifetime +} + +// GatewayType returns the protocol used to talk to the gateway ("UPnP" or "NAT-PMP"). +func (m *PortMapper) GatewayType() string { + return m.gateway.Type() +} + +// Close removes the port mapping from the gateway. Best-effort. +func (m *PortMapper) Close() error { + ctx, cancel := context.WithTimeout(context.Background(), m.config.Timeout) + defer cancel() + return m.gateway.DeletePortMapping(ctx, portMapProtocol, m.internalPort) +} diff --git a/transport/internet/finalmask/realm/server.go b/transport/internet/finalmask/realm/server.go index aacd4d0f5..1cca66586 100644 --- a/transport/internet/finalmask/realm/server.go +++ b/transport/internet/finalmask/realm/server.go @@ -33,14 +33,16 @@ type STUNPacketEvent struct { } type realmConnServer struct { - cleaned chan struct{} - ctx context.Context - cancel context.CancelFunc + wg sync.WaitGroup + ctx context.Context + cancel context.CancelFunc net.PacketConn realmClient *Client realmID string stunServers []string + family Family + mapper *PortMapper stunTimeout time.Duration punchTimeout time.Duration punchInterval time.Duration @@ -57,8 +59,29 @@ type realmConnServer struct { func NewConnServer(config *Config, raw net.PacketConn) (net.PacketConn, error) { ctx, cancel := context.WithCancel(context.Background()) + family := Family_Dual + switch config.IPMode { + case "dual": + case "v4": + family = Family_V4 + case "v6": + family = Family_V6 + } + + var mapper *PortMapper + if config.PortMapping != nil && config.PortMapping.Enabled { + var err error + start := time.Now() + mapper, err = NewPortMapper(context.Background(), raw.LocalAddr().(*net.UDPAddr).Port, PortMapConfig{Timeout: time.Duration(config.PortMapping.Timeout) * time.Second, Lifetime: time.Duration(config.PortMapping.Lifetime) * time.Second}) + if err != nil { + errors.LogErrorInner(context.Background(), err, "[realm] [port mapping] [", raw.LocalAddr().(*net.UDPAddr).Port, "] init failed after ", time.Since(start)) + } else { + errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] gateway ", mapper.GatewayType(), ", external ", mapper.ExternalAddr()) + errors.LogDebug(context.Background(), "[realm] [port mapping] [", mapper.InternalPort(), "] init success with ", time.Since(start)) + } + } + conn := &realmConnServer{ - cleaned: make(chan struct{}), ctx: ctx, cancel: cancel, PacketConn: raw, @@ -66,6 +89,8 @@ func NewConnServer(config *Config, raw net.PacketConn) (net.PacketConn, error) { realmClient: NewClient(config.Scheme, config.Host, config.Port, config.Token, config.TlsConfig), realmID: config.ID, stunServers: config.StunServers, + family: family, + mapper: mapper, stunTimeout: defaultSTUNTimeout, punchTimeout: defaultPunchTimeout, punchInterval: defaultPunchInterval, @@ -74,6 +99,12 @@ func NewConnServer(config *Config, raw net.PacketConn) (net.PacketConn, error) { stun: make(chan STUNPacketEvent, defaultEventBuffer), } + if mapper != nil { + conn.wg.Add(1) + go portMapLoop(ctx, mapper, conn.wg.Done) + } + + conn.wg.Add(1) go conn.run() return conn, nil @@ -137,6 +168,8 @@ func (c *realmConnServer) discover(servers []*net.UDPAddr) []netip.AddrPort { results := make([]netip.AddrPort, 0, len(servers)) for len(transactionIDs) > 0 { select { + case <-c.ctx.Done(): + goto end case <-deadline.C: goto end case ev := <-c.stun: @@ -148,6 +181,9 @@ func (c *realmConnServer) discover(servers []*net.UDPAddr) []netip.AddrPort { } end: deadline.Stop() + if c.mapper != nil { + results = insertAddr(results, c.mapper.ExternalAddr()) + } slices.SortFunc(results, func(a, b netip.AddrPort) int { return strings.Compare(a.String(), b.String()) }) @@ -159,7 +195,7 @@ func (c *realmConnServer) getlocals(force bool) []netip.AddrPort { c.localsMu.Lock() if force || time.Since(c.localsLast) > defaultStunCacheTTL { start := time.Now() - servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers) + servers := resolveSTUNServers(c.PacketConn.LocalAddr().(*net.UDPAddr).IP, c.stunServers, c.family) errors.LogDebug(context.Background(), "[realm] update stun servers ", servers, " with ", time.Since(start)) if len(servers) > 0 { start = time.Now() @@ -232,7 +268,7 @@ retry: if err != nil { errors.LogErrorInner(context.Background(), err, "[realm] ", c.realmID, " register session err retry in ", backoff) if c.waitctx(c.ctx, backoff) { - close(c.cleaned) + c.wg.Done() return } backoff *= 2 @@ -259,7 +295,7 @@ retry: case <-c.ctx.Done(): _ = c.realmClient.Deregister(context.Background(), c.realmID, resp.SessionID) errors.LogDebug(context.Background(), "[realm] ", c.realmID, " ", resp.SessionID, " deregistered") - close(c.cleaned) + c.wg.Done() return default: goto retry @@ -360,7 +396,7 @@ func (c *realmConnServer) punchEvent(ctx context.Context, sid string, ev *PunchE peers, _ := parseAddrPorts(ev.Addresses) errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " update peers ", peers) - filteredPeers, seen := candidatePunchAddrs(locals, peers) + filteredPeers, seen := candidatePunchAddrs(locals, peers, c.family) errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " filtered peers ", filteredPeers) expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen) errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " expanded peers ", expandedPeers) @@ -398,6 +434,6 @@ func (c *realmConnServer) ReadFrom(p []byte) (int, net.Addr, error) { func (c *realmConnServer) Close() error { c.cancel() - <-c.cleaned + c.wg.Wait() return c.PacketConn.Close() } diff --git a/transport/internet/finalmask/realm/stun.go b/transport/internet/finalmask/realm/stun.go index f490cbe03..d7dbe3f61 100644 --- a/transport/internet/finalmask/realm/stun.go +++ b/transport/internet/finalmask/realm/stun.go @@ -23,14 +23,23 @@ const ( symmetricNATMaxPortsPerHost = 32 ) -func resolveSTUNServers(local net.IP, servers []string) []*net.UDPAddr { +func resolveSTUNServers(local net.IP, servers []string, family Family) []*net.UDPAddr { var network string - if local.IsUnspecified() { - network = "ip" - } else { - if local.To4() != nil { - network = "ip4" + if family == Family_Dual { + if local.IsUnspecified() { + network = "ip" } else { + if local.To4() != nil { + network = "ip4" + } else { + network = "ip6" + } + } + } else { + if family == Family_V4 { + network = "ip4" + } + if family == Family_V6 { network = "ip6" } } @@ -104,7 +113,7 @@ func netIPPortToAddrPort(ip net.IP, port int) (netip.AddrPort, error) { return netip.AddrPortFrom(netip.AddrFrom16(addr), uint16(port)), nil } -func candidatePunchAddrs(locals, peers []netip.AddrPort) ([]netip.AddrPort, map[netip.AddrPort]struct{}) { +func candidatePunchAddrs(locals, peers []netip.AddrPort, family Family) ([]netip.AddrPort, map[netip.AddrPort]struct{}) { var allow4, allow6 bool for _, local := range locals { if local.Addr().Is4() { @@ -116,6 +125,12 @@ func candidatePunchAddrs(locals, peers []netip.AddrPort) ([]netip.AddrPort, map[ break } } + if family == Family_V4 { + allow6 = false + } + if family == Family_V6 { + allow4 = false + } seen := make(map[netip.AddrPort]struct{}, len(peers)) candidates := make([]netip.AddrPort, 0, len(peers)) for _, peer := range peers { @@ -218,3 +233,17 @@ func parseAddrPorts(addrs []string) ([]netip.AddrPort, error) { } return out, nil } + +func insertAddr(addrs []netip.AddrPort, addr netip.AddrPort) []netip.AddrPort { + if !addr.IsValid() { + return addrs + } + out := append([]netip.AddrPort(nil), addrs...) + i, found := slices.BinarySearchFunc(out, addr, func(a, b netip.AddrPort) int { + return strings.Compare(a.String(), b.String()) + }) + if found { + return out + } + return slices.Insert(out, i, addr) +} diff --git a/transport/internet/hysteria/config.pb.go b/transport/internet/hysteria/config.pb.go index 2a2cf1a2f..8e32b8fc5 100644 --- a/transport/internet/hysteria/config.pb.go +++ b/transport/internet/hysteria/config.pb.go @@ -29,10 +29,11 @@ type Config struct { MasqFile string `protobuf:"bytes,5,opt,name=masq_file,json=masqFile,proto3" json:"masq_file,omitempty"` MasqUrl string `protobuf:"bytes,6,opt,name=masq_url,json=masqUrl,proto3" json:"masq_url,omitempty"` MasqUrlRewriteHost bool `protobuf:"varint,7,opt,name=masq_url_rewrite_host,json=masqUrlRewriteHost,proto3" json:"masq_url_rewrite_host,omitempty"` - MasqUrlInsecure bool `protobuf:"varint,8,opt,name=masq_url_insecure,json=masqUrlInsecure,proto3" json:"masq_url_insecure,omitempty"` - MasqString string `protobuf:"bytes,9,opt,name=masq_string,json=masqString,proto3" json:"masq_string,omitempty"` - MasqStringHeaders map[string]string `protobuf:"bytes,10,rep,name=masq_string_headers,json=masqStringHeaders,proto3" json:"masq_string_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - MasqStringStatusCode int32 `protobuf:"varint,11,opt,name=masq_string_status_code,json=masqStringStatusCode,proto3" json:"masq_string_status_code,omitempty"` + MasqUrlXForwarded bool `protobuf:"varint,8,opt,name=masq_url_x_forwarded,json=masqUrlXForwarded,proto3" json:"masq_url_x_forwarded,omitempty"` + MasqUrlInsecure bool `protobuf:"varint,9,opt,name=masq_url_insecure,json=masqUrlInsecure,proto3" json:"masq_url_insecure,omitempty"` + MasqString string `protobuf:"bytes,10,opt,name=masq_string,json=masqString,proto3" json:"masq_string,omitempty"` + MasqStringHeaders map[string]string `protobuf:"bytes,11,rep,name=masq_string_headers,json=masqStringHeaders,proto3" json:"masq_string_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + MasqStringStatusCode int32 `protobuf:"varint,12,opt,name=masq_string_status_code,json=masqStringStatusCode,proto3" json:"masq_string_status_code,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -109,6 +110,13 @@ func (x *Config) GetMasqUrlRewriteHost() bool { return false } +func (x *Config) GetMasqUrlXForwarded() bool { + if x != nil { + return x.MasqUrlXForwarded + } + return false +} + func (x *Config) GetMasqUrlInsecure() bool { if x != nil { return x.MasqUrlInsecure @@ -141,20 +149,21 @@ var File_transport_internet_hysteria_config_proto protoreflect.FileDescriptor const file_transport_internet_hysteria_config_proto_rawDesc = "" + "\n" + - "(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\x8f\x04\n" + + "(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\xc0\x04\n" + "\x06Config\x12\x12\n" + "\x04auth\x18\x02 \x01(\tR\x04auth\x12(\n" + "\x10udp_idle_timeout\x18\x03 \x01(\x03R\x0eudpIdleTimeout\x12\x1b\n" + "\tmasq_type\x18\x04 \x01(\tR\bmasqType\x12\x1b\n" + "\tmasq_file\x18\x05 \x01(\tR\bmasqFile\x12\x19\n" + "\bmasq_url\x18\x06 \x01(\tR\amasqUrl\x121\n" + - "\x15masq_url_rewrite_host\x18\a \x01(\bR\x12masqUrlRewriteHost\x12*\n" + - "\x11masq_url_insecure\x18\b \x01(\bR\x0fmasqUrlInsecure\x12\x1f\n" + - "\vmasq_string\x18\t \x01(\tR\n" + + "\x15masq_url_rewrite_host\x18\a \x01(\bR\x12masqUrlRewriteHost\x12/\n" + + "\x14masq_url_x_forwarded\x18\b \x01(\bR\x11masqUrlXForwarded\x12*\n" + + "\x11masq_url_insecure\x18\t \x01(\bR\x0fmasqUrlInsecure\x12\x1f\n" + + "\vmasq_string\x18\n" + + " \x01(\tR\n" + "masqString\x12o\n" + - "\x13masq_string_headers\x18\n" + - " \x03(\v2?.xray.transport.internet.hysteria.Config.MasqStringHeadersEntryR\x11masqStringHeaders\x125\n" + - "\x17masq_string_status_code\x18\v \x01(\x05R\x14masqStringStatusCode\x1aD\n" + + "\x13masq_string_headers\x18\v \x03(\v2?.xray.transport.internet.hysteria.Config.MasqStringHeadersEntryR\x11masqStringHeaders\x125\n" + + "\x17masq_string_status_code\x18\f \x01(\x05R\x14masqStringStatusCode\x1aD\n" + "\x16MasqStringHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02B\x82\x01\n" + diff --git a/transport/internet/hysteria/config.proto b/transport/internet/hysteria/config.proto index 8eaac87c1..279784acd 100644 --- a/transport/internet/hysteria/config.proto +++ b/transport/internet/hysteria/config.proto @@ -11,12 +11,17 @@ message Config { string auth = 2; int64 udp_idle_timeout = 3; + string masq_type = 4; + string masq_file = 5; + string masq_url = 6; bool masq_url_rewrite_host = 7; - bool masq_url_insecure = 8; - string masq_string = 9; - map masq_string_headers = 10; - int32 masq_string_status_code = 11; + bool masq_url_x_forwarded = 8; + bool masq_url_insecure = 9; + + string masq_string = 10; + map masq_string_headers = 11; + int32 masq_string_status_code = 12; } \ No newline at end of file diff --git a/transport/internet/hysteria/congestion/brutal/brutal.go b/transport/internet/hysteria/congestion/brutal/brutal.go index 969923a51..c6b1fcbf4 100644 --- a/transport/internet/hysteria/congestion/brutal/brutal.go +++ b/transport/internet/hysteria/congestion/brutal/brutal.go @@ -33,6 +33,8 @@ type BrutalSender struct { pktInfoSlots [pktInfoSlotCount]pktInfo ackRate float64 + disableLossCompensation bool + debug bool lastAckPrintTimestamp int64 } @@ -43,13 +45,14 @@ type pktInfo struct { LossCount uint64 } -func NewBrutalSender(bps uint64) *BrutalSender { +func NewBrutalSender(bps uint64, disableLossCompensation bool) *BrutalSender { debug, _ := strconv.ParseBool(os.Getenv(debugEnv)) bs := &BrutalSender{ - bps: congestion.ByteCount(bps), - maxDatagramSize: congestion.InitialPacketSize, - ackRate: 1, - debug: debug, + bps: congestion.ByteCount(bps), + maxDatagramSize: congestion.InitialPacketSize, + ackRate: 1, + disableLossCompensation: disableLossCompensation, + debug: debug, } bs.pacer = common.NewPacer(func() congestion.ByteCount { return congestion.ByteCount(float64(bs.bps) / bs.ackRate) @@ -127,6 +130,10 @@ func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) { } func (b *BrutalSender) updateAckRate(currentTimestamp int64) { + if b.disableLossCompensation { + b.ackRate = 1 + return + } minTimestamp := currentTimestamp - pktInfoSlotCount var ackCount, lossCount uint64 for _, info := range b.pktInfoSlots { diff --git a/transport/internet/hysteria/congestion/brutal/brutal_test.go b/transport/internet/hysteria/congestion/brutal/brutal_test.go new file mode 100644 index 000000000..2565beb49 --- /dev/null +++ b/transport/internet/hysteria/congestion/brutal/brutal_test.go @@ -0,0 +1,45 @@ +package brutal + +import ( + "testing" + "time" + + "github.com/apernet/quic-go/congestion" + "github.com/apernet/quic-go/monotime" +) + +// feedAckRate drives a single sampling slot with the given number of acked and +// lost packets and returns the resulting ackRate. +func feedAckRate(disableLossCompensation bool, ackCount, lossCount int) float64 { + b := NewBrutalSender(1000000, disableLossCompensation) + acked := make([]congestion.AckedPacketInfo, ackCount) + lost := make([]congestion.LostPacketInfo, lossCount) + // eventTime lands in a fixed slot; a single event carries enough samples. + b.OnCongestionEventEx(0, monotime.Time(5*time.Second), acked, lost) + return b.ackRate +} + +func TestBrutalLossCompensation(t *testing.T) { + tests := []struct { + name string + ack, loss int + want float64 // expected ackRate when compensation is ENABLED + }{ + {"no loss", 100, 0, 1.0}, + {"20% loss", 80, 20, 0.8}, + {"50% loss clamps to floor", 50, 50, minAckRate}, // 0.5 clamped up to 0.8 + {"few samples stays 1", 10, 5, 1.0}, // below minSampleCount + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Compensation enabled (default behavior): ackRate reacts to loss. + if got := feedAckRate(false, tt.ack, tt.loss); got != tt.want { + t.Errorf("compensation on: ackRate = %v, want %v", got, tt.want) + } + // Compensation disabled: ackRate must stay pinned at 1 regardless. + if got := feedAckRate(true, tt.ack, tt.loss); got != 1.0 { + t.Errorf("compensation off: ackRate = %v, want 1.0", got) + } + }) + } +} diff --git a/transport/internet/hysteria/congestion/utils.go b/transport/internet/hysteria/congestion/utils.go index 0f04318db..87293722d 100644 --- a/transport/internet/hysteria/congestion/utils.go +++ b/transport/internet/hysteria/congestion/utils.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/apernet/quic-go" + "github.com/apernet/quic-go/congestion" "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" "github.com/xtls/xray-core/transport/internet/hysteria/congestion/brutal" ) @@ -36,13 +37,29 @@ func NormalizeBBRProfile(profile string) (string, error) { func UseBBR(conn *quic.Conn, profile bbr.Profile) { conn.SetCongestionControl(bbr.NewBbrSender( bbr.DefaultClock{}, - bbr.GetInitialPacketSize(conn.RemoteAddr()), + seedPacketSize(conn.InitialPacketSize(), bbr.GetInitialPacketSize(conn.RemoteAddr())), profile, )) } -func UseBrutal(conn *quic.Conn, tx uint64) { - conn.SetCongestionControl(brutal.NewBrutalSender(tx)) +// seedPacketSize picks the datagram size to seed a replacement congestion +// controller with, given the size QUIC itself starts at and the guess derived +// from the remote address. +// +// The seed must not exceed what QUIC actually starts at. If it does, the first +// path MTU probe can land between the two: QUIC sees an increase and reports +// it, but the controller sees a decrease, which it cannot represent. Taking the +// smaller of the two keeps the address-based guess as a floor for connections +// whose path we can't reason about, while never seeding above QUIC. +func seedPacketSize(quicSize, byAddr congestion.ByteCount) congestion.ByteCount { + if quicSize <= 0 { + return byAddr + } + return min(quicSize, byAddr) +} + +func UseBrutal(conn *quic.Conn, tx uint64, disableLossCompensation bool) { + conn.SetCongestionControl(brutal.NewBrutalSender(tx, disableLossCompensation)) } func UseConfigured(conn *quic.Conn, congestionType, bbrProfile string) { diff --git a/transport/internet/hysteria/dialer.go b/transport/internet/hysteria/dialer.go index ed7dd4b2c..0307df576 100644 --- a/transport/internet/hysteria/dialer.go +++ b/transport/internet/hysteria/dialer.go @@ -90,6 +90,7 @@ func (c *client) dial(ctx context.Context) error { 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, EnableDatagrams: true, MaxDatagramFrameSize: MaxDatagramFrameSize, OmitMaxDatagramFrameSize: time.Now().After(time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)), @@ -172,7 +173,12 @@ func (c *client) dial(ctx context.Context) error { pktConn = newConn } - tr := &quic.Transport{Conn: pktConn} + tr := &quic.Transport{Conn: pktConn, DisableGSO: quicParams.DisableGSO} + + if !quicParams.DisableChromeParrot { + tr.ConnectionIDGenerator = quic.ZeroLengthConnectionIDGenerator{} + c.tlsConfig.GetCertificate = nil + } var conn *quic.Conn rt := &http3.Transport{ @@ -219,6 +225,7 @@ func (c *client) dial(ctx context.Context) error { // udp, _ := strconv.ParseBool(resp.Header.Get(ResponseHeaderUDPEnabled)) down, _ := strconv.ParseUint(resp.Header.Get(CommonHeaderCCRX), 10, 64) + errors.LogDebug(context.Background(), "ECHAccepted ", conn.ConnectionState().TLS.ECHAccepted) switch quicParams.Congestion { case "reno": @@ -228,10 +235,10 @@ func (c *client) dial(ctx context.Context) error { if quicParams.BrutalUp == 0 || down == 0 { congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile)) } else { - congestion.UseBrutal(conn, min(quicParams.BrutalUp, down)) + congestion.UseBrutal(conn, min(quicParams.BrutalUp, down), quicParams.BrutalDisableLossCompensation) } case "force-brutal": - congestion.UseBrutal(conn, quicParams.BrutalUp) + congestion.UseBrutal(conn, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation) default: panic(quicParams.Congestion) } diff --git a/transport/internet/hysteria/hub.go b/transport/internet/hysteria/hub.go index 59bcd7d42..c0cc641ba 100644 --- a/transport/internet/hysteria/hub.go +++ b/transport/internet/hysteria/hub.go @@ -2,6 +2,7 @@ package hysteria import ( "context" + "crypto/rand" gotls "crypto/tls" "net/http" "net/http/httputil" @@ -78,10 +79,10 @@ func (h *httpHandler) AuthHTTP(w http.ResponseWriter, r *http.Request) bool { if quicParams.BrutalUp == 0 || down == 0 { congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile)) } else { - congestion.UseBrutal(conn, min(quicParams.BrutalUp, down)) + congestion.UseBrutal(conn, min(quicParams.BrutalUp, down), quicParams.BrutalDisableLossCompensation) } case "force-brutal": - congestion.UseBrutal(conn, quicParams.BrutalUp) + congestion.UseBrutal(conn, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation) default: panic(quicParams.Congestion) } @@ -165,8 +166,9 @@ func (l *Listener) handleClient(conn *quic.Conn) { Handler: handler, StreamDispatcher: handler.StreamDispatcher, } - _ = h3s.ServeQUICConn(conn) + err := h3s.ServeQUICConn(conn) _ = conn.CloseWithError(closeErrCodeOK, "") + errors.LogDebug(context.Background(), conn.RemoteAddr(), " ServeQUICConn exited with ", err) } func (l *Listener) keepAccepting() { @@ -219,21 +221,43 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti return nil, err } transport := http.DefaultTransport.(*http.Transport) - if config.MasqUrlInsecure { - transport = transport.Clone() - transport.TLSClientConfig = &gotls.Config{ - InsecureSkipVerify: true, + switch u.Scheme { + case "http", "https": + if config.MasqUrlInsecure { + transport = transport.Clone() + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &gotls.Config{} + } + transport.TLSClientConfig.InsecureSkipVerify = true } + case "", "unix": + u = &url.URL{Scheme: "http", Host: "localhost"} + path := u.Path + dialer := &net.Dialer{Timeout: 30 * time.Second} + transport = transport.Clone() + transport.Proxy = nil + transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.DialContext(ctx, "unix", path) + } + transport.MaxIdleConns = masqueradeProxyMaxIdleConnections + transport.MaxIdleConnsPerHost = masqueradeProxyMaxIdleConnsPerHost + default: + return nil, errors.New("unknown scheme") } masqHandler = &httputil.ReverseProxy{ - Rewrite: func(pr *httputil.ProxyRequest) { - pr.SetURL(u) + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(u) if !config.MasqUrlRewriteHost { - pr.Out.Host = pr.In.Host + r.Out.Host = r.In.Host + } + if config.MasqUrlXForwarded { + r.SetXForwarded() } }, - Transport: transport, + Transport: transport, + BufferPool: newMasqueradeProxyBufferPool(), ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + errors.LogErrorInner(context.Background(), err, "HTTP reverse proxy error") w.WriteHeader(http.StatusBadGateway) }, } @@ -307,7 +331,13 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti pktConn = newConn } - tr := &quic.Transport{Conn: pktConn} + var k *quic.StatelessResetKey + if !quicParams.DisableStatelessReset { + k = &quic.StatelessResetKey{} + common.Must2(rand.Read((*k)[:])) + } + + tr := &quic.Transport{Conn: pktConn, DisableGSO: quicParams.DisableGSO, StatelessResetKey: k} listener, err := tr.Listen(tlsConfig.GetTLSConfig(tls.WithNextProto("h3")), quicConfig) if err != nil { @@ -336,3 +366,34 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti func init() { common.Must(internet.RegisterTransportListener(protocolName, Listen)) } + +const ( + masqueradeProxyBufferSize = 32 * 1024 + masqueradeProxyMaxIdleConnections = 100 + masqueradeProxyMaxIdleConnsPerHost = 32 +) + +type masqueradeProxyBufferPool struct { + pool sync.Pool +} + +func newMasqueradeProxyBufferPool() *masqueradeProxyBufferPool { + return &masqueradeProxyBufferPool{ + pool: sync.Pool{ + New: func() any { + return make([]byte, masqueradeProxyBufferSize) + }, + }, + } +} + +func (p *masqueradeProxyBufferPool) Get() []byte { + return p.pool.Get().([]byte) +} + +func (p *masqueradeProxyBufferPool) Put(buf []byte) { + if cap(buf) < masqueradeProxyBufferSize { + return + } + p.pool.Put(buf[:masqueradeProxyBufferSize]) +} diff --git a/transport/internet/splithttp/dialer.go b/transport/internet/splithttp/dialer.go index d2908aa1b..3c0f1a573 100644 --- a/transport/internet/splithttp/dialer.go +++ b/transport/internet/splithttp/dialer.go @@ -175,6 +175,7 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second, MaxIncomingStreams: quicParams.MaxIncomingStreams, DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"), + ChromeParrot: !quicParams.DisableChromeParrot, } if quicParams.MaxIdleTimeout == 0 { quicConfig.MaxIdleTimeout = net.ConnIdleTimeout @@ -255,18 +256,25 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea pktConn = newConn } - conn, err := quic.DialEarly(ctx, pktConn, udpAddr, tlsCfg, cfg) + tr := &quic.Transport{Conn: pktConn, DisableGSO: quicParams.DisableGSO} + + if !quicParams.DisableChromeParrot { + tr.ConnectionIDGenerator = quic.ZeroLengthConnectionIDGenerator{} + tlsCfg.GetCertificate = nil + } + + conn, err := tr.DialEarly(ctx, udpAddr, tlsCfg, cfg) if err != nil { return nil, err } - context.AfterFunc(conn.Context(), func() { pktConn.Close() }) + context.AfterFunc(conn.Context(), func() { tr.Close(); pktConn.Close() }) switch quicParams.Congestion { case "reno": case "", "bbr": congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile)) case "force-brutal": - congestion.UseBrutal(conn, quicParams.BrutalUp) + congestion.UseBrutal(conn, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation) default: panic(quicParams.Congestion) } diff --git a/transport/internet/splithttp/hub.go b/transport/internet/splithttp/hub.go index d664c86c4..c542a325b 100644 --- a/transport/internet/splithttp/hub.go +++ b/transport/internet/splithttp/hub.go @@ -3,6 +3,7 @@ package splithttp import ( "bytes" "context" + "crypto/rand" gotls "crypto/tls" "encoding/base64" "fmt" @@ -506,7 +507,15 @@ func ListenXH(ctx context.Context, address net.Address, port net.Port, streamSet DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"), } - l.h3listener, err = quic.ListenEarly(Conn, tlsConfig, quicConfig) + var k *quic.StatelessResetKey + if !quicParams.DisableStatelessReset { + k = &quic.StatelessResetKey{} + common.Must2(rand.Read((*k)[:])) + } + + tr := &quic.Transport{Conn: Conn, DisableGSO: quicParams.DisableGSO, StatelessResetKey: k} + + l.h3listener, err = tr.ListenEarly(tlsConfig, quicConfig) if err != nil { return nil, errors.New("failed to listen QUIC for XHTTP/3 on ", address, ":", port).Base(err) } @@ -525,6 +534,8 @@ func ListenXH(ctx context.Context, address net.Address, port net.Port, streamSet if err := l.h3server.ServeListener(l.h3listener); err != nil { errors.LogErrorInner(ctx, err, "failed to serve HTTP/3 for XHTTP/3") } + _ = tr.Close() + _ = Conn.Close() }() } else { // tcp l.listener, err = internet.ListenSystem(ctx, &net.TCPAddr{ @@ -622,7 +633,7 @@ func (l *QListener) Accept(ctx context.Context) (*quic.Conn, error) { case "", "bbr": congestion.UseBBR(conn, bbr.Profile(l.quicParams.BbrProfile)) case "force-brutal": - congestion.UseBrutal(conn, l.quicParams.BrutalUp) + congestion.UseBrutal(conn, l.quicParams.BrutalUp, l.quicParams.BrutalDisableLossCompensation) default: panic(l.quicParams.Congestion) }