Compare commits

...
Author SHA1 Message Date
patterniha 813f3810ef Direct/Freedom outbound: Skip domain resolution and finalRules with dialerProxy
https://github.com/XTLS/Xray-core/pull/6058#issuecomment-5578800461

When using dialerProxy, the target domain should not be resolved to an IP. Also, with dialerProxy freedom is no longer the final outbound, which makes finalRules invalid, so finalRules should not be applied at all.
2026-09-08 20:28:22 +03:30
RPRXandGitHub 37ceb8b4b6 Xray-core v26.9.8
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067
VLESS NFT: https://opensea.io/collection/vless

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-09-08 09:47:24 +00:00
RPRXandGitHub fd2ca74822 go.mod: Specify Go 1.27.x
https://github.com/XTLS/Xray-core/pull/6327#issuecomment-5581777988
2026-09-08 08:44:33 +00:00
RPRX 47cfe9994a Update github.com/xtls/reality to 20260908062103
https://github.com/XTLS/REALITY/commit/8cdf7bf9c7f09cb9814bf08c3eb877f68b85fba8
https://github.com/XTLS/REALITY/commit/e1986a4d31ca33c087a72ab4644aef1295693b69
https://github.com/XTLS/REALITY/commit/393f8de3ee2d685271d79ee608334e441b2db324
2026-09-08 07:22:54 +00:00
MeowandGitHub 3e2f040cd8 Direct/Freedom outbound: Better Compatibility (#6058)
Fixes https://github.com/XTLS/Xray-core/issues/6393
2026-09-08 03:31:25 +00:00
LjhAUMEMandGitHub c7245c0336 TUN inbound: Refine Windows support (#6478)
https://github.com/XTLS/Xray-core/pull/6478#issuecomment-5489052152
2026-09-08 02:51:35 +00:00
eef6e63bc1 XHTTP client: Fix a data race in WaitReadCloser (#6694)
https://github.com/XTLS/Xray-core/pull/6694#issuecomment-5575759500

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-09-07 22:38:16 +00:00
Hossin AsaadiandGitHub 6ce8dc53e7 common/buf/writer.go: Fix buffered writes failing when payload exceeds remaining capacity (#6723)
Fixes https://github.com/XTLS/Xray-core/issues/5287
2026-09-07 21:14:41 +00:00
01a034be53 Blackhole outbound: Add customizable response data support (#6713)
https://github.com/XTLS/Xray-core/pull/6711#issuecomment-5508683394

---------

Co-authored-by: Raaad1on <djilyawhite@gmail.com>
2026-09-07 21:09:58 +00:00
XXcipherXandGitHub de2caf3cef Hysteria server: Fix Unix masquerade socket path (#6705)
https://github.com/XTLS/Xray-core/pull/6705#issuecomment-5489788730
2026-09-07 21:01:45 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cecc88f43c Bump google.golang.org/grpc from 1.83.1 to 1.83.2 (#6704)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.83.1 to 1.83.2.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.83.1...v1.83.2)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 20:42:22 +00:00
pexcnandGitHub cd4ce973e9 WebSocket client: Avoid panic before real dialing in delayDialConn (#6544)
Fixes https://github.com/teddysun/xray-plugin-android/issues/11
2026-09-01 04:57:10 +00:00
patternihaandGitHub fc7b980636 Sockopt: Fix indentation of the CustomSockopt block for Linux (#6568)
https://github.com/XTLS/Xray-core/pull/6568#issuecomment-5488145252

Fixes https://github.com/XTLS/Xray-core/issues/6564
Fixes https://github.com/XTLS/Xray-core/issues/6602
2026-09-01 04:47:55 +00:00
YounesandRPRX 8ee131cbbb HTTP inbound: Fix a potential panic in readResponseAndHandle100Continue() (#6545) 2026-09-01 02:59:56 +03:00
Jorik_SmithandGitHub 2776ea6d74 SS2022 outbound: Close connection to server on early returns (#6698)
https://github.com/XTLS/Xray-core/pull/6698#issuecomment-5468662565
2026-08-31 23:59:04 +00:00
meanwhile131andGitHub 5e245b082e Routing: Export HealthCheckSettings (#6680) 2026-08-30 12:19:54 +00:00
d9c54026c5 Sniffing: Support QUICv2 (#6695)
Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-08-30 12:15:25 +00:00
brookwkoandGitHub c1958dba04 TUN inbound: Support autoSystemRoutingTable and autoOutboundsInterface on FreeBSD as well (#6691)
https://github.com/XTLS/Xray-core/pull/6691#issuecomment-5445818753

Closes https://github.com/XTLS/Xray-core/issues/5594#issuecomment-4789101025
2026-08-27 22:32:45 +00:00
540b9070f5 Transport: Bind the UDP outbound socket in the destination family (#6688)
https://github.com/XTLS/Xray-core/pull/6688#issuecomment-5445559197

Completes https://github.com/XTLS/Xray-core/pull/6624#issuecomment-5430488664

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-08-27 21:46:26 +00:00
LjhAUMEMandGitHub ada99a4eb0 Hysteria: Upgrade to official v2.12.2 (#6565)
https://github.com/XTLS/Xray-core/pull/6565#issuecomment-5202554275

Fixes https://github.com/XTLS/Xray-core/issues/6408
Fixes https://github.com/XTLS/Xray-core/issues/6598
...
2026-08-27 17:25:56 +00:00
风扇滑翔翼 65458e919f Config: Fix some issues (#6640)
https://github.com/XTLS/Xray-core/pull/6640#issuecomment-5420106315

Fixes https://github.com/XTLS/Xray-core/issues/6636
Fixes https://github.com/XTLS/Xray-core/issues/6600
...
2026-08-27 20:03:59 +08:00
JidosandGitHub aa3d6589da TUN inbound: Wait() blocks via kqueue instead of busy-spinning on Darwin (#6580)
Fixes https://github.com/XTLS/Xray-core/issues/6579
2026-08-26 21:29:58 +00:00
Maksim VarentsovandGitHub 25c11e2d2b Tunnel inbound: SO_REUSEPORT fix and IPv6 support for the OpenBSD transparent proxy (#6624)
Completes https://github.com/XTLS/Xray-core/pull/6546
2026-08-26 21:05:05 +00:00
dffc7ada5e XHTTP client: Define Request.GetBody() for packet-up so h2 can replay after GOAWAY (#6632)
https://github.com/XTLS/Xray-core/pull/6632#issuecomment-5430735467

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-08-26 20:41:43 +00:00
77f98eba09 XHTTP client: Fix a race condition and a data race (#6665)
https://github.com/XTLS/Xray-core/pull/6665#issuecomment-5429028477

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-08-26 19:19:41 +00:00
风扇滑翔翼andGitHub f124daf5a3 Observatory: Fix consuming 100% CPU when no outbound matches subjectSelector (#6669)
Fixes https://github.com/XTLS/Xray-core/issues/6666
2026-08-25 18:30:33 +00:00
74 changed files with 2978 additions and 1030 deletions
+6
View File
@@ -78,6 +78,12 @@ func (o *Observer) background() {
sleepTime = time.Duration(o.config.ProbeInterval) sleepTime = time.Duration(o.config.ProbeInterval)
} }
if len(outbounds) == 0 {
errors.LogWarning(o.ctx, "no outbound matches subjectSelector ", o.config.SubjectSelector)
time.Sleep(sleepTime)
continue
}
if !o.config.EnableConcurrency { if !o.config.EnableConcurrency {
sort.Strings(outbounds) sort.Strings(outbounds)
for _, v := range outbounds { for _, v := range outbounds {
+11 -22
View File
@@ -330,7 +330,6 @@ type SenderConfig struct {
// Send traffic through the given IP. Only IP is allowed. // Send traffic through the given IP. Only IP is allowed.
Via *net.IPOrDomain `protobuf:"bytes,1,opt,name=via,proto3" json:"via,omitempty"` Via *net.IPOrDomain `protobuf:"bytes,1,opt,name=via,proto3" json:"via,omitempty"`
StreamSettings *internet.StreamConfig `protobuf:"bytes,2,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"` StreamSettings *internet.StreamConfig `protobuf:"bytes,2,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"`
ProxySettings *internet.ProxyConfig `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"`
MultiplexSettings *MultiplexingConfig `protobuf:"bytes,4,opt,name=multiplex_settings,json=multiplexSettings,proto3" json:"multiplex_settings,omitempty"` MultiplexSettings *MultiplexingConfig `protobuf:"bytes,4,opt,name=multiplex_settings,json=multiplexSettings,proto3" json:"multiplex_settings,omitempty"`
ViaCidr string `protobuf:"bytes,5,opt,name=via_cidr,json=viaCidr,proto3" json:"via_cidr,omitempty"` ViaCidr string `protobuf:"bytes,5,opt,name=via_cidr,json=viaCidr,proto3" json:"via_cidr,omitempty"`
TargetStrategy internet.DomainStrategy `protobuf:"varint,6,opt,name=target_strategy,json=targetStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"target_strategy,omitempty"` TargetStrategy internet.DomainStrategy `protobuf:"varint,6,opt,name=target_strategy,json=targetStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"target_strategy,omitempty"`
@@ -382,13 +381,6 @@ func (x *SenderConfig) GetStreamSettings() *internet.StreamConfig {
return nil return nil
} }
func (x *SenderConfig) GetProxySettings() *internet.ProxyConfig {
if x != nil {
return x.ProxySettings
}
return nil
}
func (x *SenderConfig) GetMultiplexSettings() *MultiplexingConfig { func (x *SenderConfig) GetMultiplexSettings() *MultiplexingConfig {
if x != nil { if x != nil {
return x.MultiplexSettings return x.MultiplexSettings
@@ -506,14 +498,13 @@ const file_app_proxyman_config_proto_rawDesc = "" +
"\x03tag\x18\x01 \x01(\tR\x03tag\x12M\n" + "\x03tag\x18\x01 \x01(\tR\x03tag\x12M\n" +
"\x11receiver_settings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\x10receiverSettings\x12G\n" + "\x11receiver_settings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\x10receiverSettings\x12G\n" +
"\x0eproxy_settings\x18\x03 \x01(\v2 .xray.common.serial.TypedMessageR\rproxySettings\"\x10\n" + "\x0eproxy_settings\x18\x03 \x01(\v2 .xray.common.serial.TypedMessageR\rproxySettings\"\x10\n" +
"\x0eOutboundConfig\"\x9d\x03\n" + "\x0eOutboundConfig\"\xd6\x02\n" +
"\fSenderConfig\x12-\n" + "\fSenderConfig\x12-\n" +
"\x03via\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x03via\x12N\n" + "\x03via\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x03via\x12N\n" +
"\x0fstream_settings\x18\x02 \x01(\v2%.xray.transport.internet.StreamConfigR\x0estreamSettings\x12K\n" + "\x0fstream_settings\x18\x02 \x01(\v2%.xray.transport.internet.StreamConfigR\x0estreamSettings\x12T\n" +
"\x0eproxy_settings\x18\x03 \x01(\v2$.xray.transport.internet.ProxyConfigR\rproxySettings\x12T\n" +
"\x12multiplex_settings\x18\x04 \x01(\v2%.xray.app.proxyman.MultiplexingConfigR\x11multiplexSettings\x12\x19\n" + "\x12multiplex_settings\x18\x04 \x01(\v2%.xray.app.proxyman.MultiplexingConfigR\x11multiplexSettings\x12\x19\n" +
"\bvia_cidr\x18\x05 \x01(\tR\aviaCidr\x12P\n" + "\bvia_cidr\x18\x05 \x01(\tR\aviaCidr\x12P\n" +
"\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategy\"\xa4\x01\n" + "\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategyJ\x04\b\x03\x10\x04\"\xa4\x01\n" +
"\x12MultiplexingConfig\x12\x18\n" + "\x12MultiplexingConfig\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\x12 \n" + "\aenabled\x18\x01 \x01(\bR\aenabled\x12 \n" +
"\vconcurrency\x18\x02 \x01(\x05R\vconcurrency\x12(\n" + "\vconcurrency\x18\x02 \x01(\x05R\vconcurrency\x12(\n" +
@@ -548,8 +539,7 @@ var file_app_proxyman_config_proto_goTypes = []any{
(*net.IPOrDomain)(nil), // 10: xray.common.net.IPOrDomain (*net.IPOrDomain)(nil), // 10: xray.common.net.IPOrDomain
(*internet.StreamConfig)(nil), // 11: xray.transport.internet.StreamConfig (*internet.StreamConfig)(nil), // 11: xray.transport.internet.StreamConfig
(*serial.TypedMessage)(nil), // 12: xray.common.serial.TypedMessage (*serial.TypedMessage)(nil), // 12: xray.common.serial.TypedMessage
(*internet.ProxyConfig)(nil), // 13: xray.transport.internet.ProxyConfig (internet.DomainStrategy)(0), // 13: xray.transport.internet.DomainStrategy
(internet.DomainStrategy)(0), // 14: xray.transport.internet.DomainStrategy
} }
var file_app_proxyman_config_proto_depIdxs = []int32{ var file_app_proxyman_config_proto_depIdxs = []int32{
7, // 0: xray.app.proxyman.SniffingConfig.domains_excluded:type_name -> xray.common.geodata.DomainRule 7, // 0: xray.app.proxyman.SniffingConfig.domains_excluded:type_name -> xray.common.geodata.DomainRule
@@ -562,14 +552,13 @@ var file_app_proxyman_config_proto_depIdxs = []int32{
12, // 7: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage 12, // 7: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage
10, // 8: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain 10, // 8: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain
11, // 9: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig 11, // 9: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig
13, // 10: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig 6, // 10: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig
6, // 11: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig 13, // 11: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy
14, // 12: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy 12, // [12:12] is the sub-list for method output_type
13, // [13:13] is the sub-list for method output_type 12, // [12:12] is the sub-list for method input_type
13, // [13:13] is the sub-list for method input_type 12, // [12:12] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension type_name 12, // [12:12] is the sub-list for extension extendee
13, // [13:13] is the sub-list for extension extendee 0, // [0:12] is the sub-list for field type_name
0, // [0:13] is the sub-list for field type_name
} }
func init() { file_app_proxyman_config_proto_init() } func init() { file_app_proxyman_config_proto_init() }
+1 -1
View File
@@ -57,7 +57,7 @@ message SenderConfig {
// Send traffic through the given IP. Only IP is allowed. // Send traffic through the given IP. Only IP is allowed.
xray.common.net.IPOrDomain via = 1; xray.common.net.IPOrDomain via = 1;
xray.transport.internet.StreamConfig stream_settings = 2; xray.transport.internet.StreamConfig stream_settings = 2;
xray.transport.internet.ProxyConfig proxy_settings = 3; reserved 3;
MultiplexingConfig multiplex_settings = 4; MultiplexingConfig multiplex_settings = 4;
string via_cidr = 5; string via_cidr = 5;
xray.transport.internet.DomainStrategy target_strategy = 6; xray.transport.internet.DomainStrategy target_strategy = 6;
+11 -57
View File
@@ -15,7 +15,6 @@ import (
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/mux" "github.com/xtls/xray-core/common/mux"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/net/cnc"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
@@ -26,8 +25,6 @@ import (
"github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
"github.com/xtls/xray-core/transport/pipe"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -63,7 +60,6 @@ type Handler struct {
streamSettings *internet.MemoryStreamConfig streamSettings *internet.MemoryStreamConfig
proxyConfig proto.Message proxyConfig proto.Message
proxy proxy.Outbound proxy proxy.Outbound
outboundManager outbound.Manager
mux *mux.ClientManager mux *mux.ClientManager
xudp *mux.ClientManager xudp *mux.ClientManager
udp443 string udp443 string
@@ -77,7 +73,6 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
uplinkCounter, downlinkCounter := getStatCounter(v, config.Tag) uplinkCounter, downlinkCounter := getStatCounter(v, config.Tag)
h := &Handler{ h := &Handler{
tag: config.Tag, tag: config.Tag,
outboundManager: v.GetFeature(outbound.ManagerType()).(outbound.Manager),
uplinkCounter: uplinkCounter, uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter, downlinkCounter: downlinkCounter,
} }
@@ -108,9 +103,11 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
ctx = session.ContextWithFullHandler(ctx, h) ctx = session.ContextWithFullHandler(ctx, h)
newCtx := session.ContextWithStreamSettings(ctx, h.streamSettings) if h.streamSettings != nil {
ctx = session.ContextWithStreamSettings(ctx, h.streamSettings)
}
rawProxyHandler, err := common.CreateObject(newCtx, proxyConfig) rawProxyHandler, err := common.CreateObject(ctx, proxyConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -197,7 +194,6 @@ func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
common.Interrupt(link.Reader) common.Interrupt(link.Reader)
return return
} }
} else { } else {
unchangedDomain := ob.Target.Address.Domain() unchangedDomain := ob.Target.Address.Domain()
ob.Target.Address = net.IPAddress(ips[dice.Roll(len(ips))]) ob.Target.Address = net.IPAddress(ips[dice.Roll(len(ips))])
@@ -270,66 +266,26 @@ func (h *Handler) DestIpAddress() net.IP {
// Dial implements internet.Dialer. // Dial implements internet.Dialer.
func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) { func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) {
if h.senderSettings != nil { if h.senderSettings != nil && h.senderSettings.Via != nil {
outbounds := session.OutboundsFromContext(ctx)
if h.senderSettings.ProxySettings.HasTag() { ob := outbounds[len(outbounds)-1]
h.SetOutboundGateway(ctx, ob)
tag := h.senderSettings.ProxySettings.Tag
handler := h.outboundManager.GetHandler(tag)
if handler != nil {
errors.LogDebug(ctx, "proxying to ", tag, " for dest ", dest)
outbounds := session.OutboundsFromContext(ctx)
ctx = session.ContextWithOutbounds(ctx, append(outbounds, &session.Outbound{
Target: dest,
Tag: tag,
})) // add another outbound in session ctx
opts := pipe.OptionsFromContext(ctx)
uplinkReader, uplinkWriter := pipe.New(opts...)
downlinkReader, downlinkWriter := pipe.New(opts...)
go handler.Dispatch(ctx, &transport.Link{Reader: uplinkReader, Writer: downlinkWriter})
conn := cnc.NewConnection(cnc.ConnectionInputMulti(uplinkWriter), cnc.ConnectionOutputMulti(downlinkReader))
if config := tls.ConfigFromStreamSettings(h.streamSettings); config != nil {
tlsConfig := config.GetTLSConfig(tls.WithDestination(dest))
conn = tls.Client(conn, tlsConfig)
}
return h.getStatCouterConnection(conn), nil
}
errors.LogError(ctx, "failed to get outbound handler with tag: ", tag)
return nil, errors.New("failed to get outbound handler with tag: " + tag)
}
if h.senderSettings.Via != nil {
outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1]
h.SetOutboundGateway(ctx, ob)
}
} }
conn, err := internet.Dial(ctx, dest, h.streamSettings) conn, err := internet.Dial(ctx, dest, h.streamSettings)
conn = h.getStatCouterConnection(conn) conn = h.getStatCouterConnection(conn)
outbounds := session.OutboundsFromContext(ctx)
if outbounds != nil {
ob := outbounds[len(outbounds)-1]
ob.Conn = conn
} else {
// for Vision's pre-connect
}
return conn, err return conn, err
} }
func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound) { func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound) {
if ob.Gateway == nil && h.senderSettings != nil && h.senderSettings.Via != nil && !h.senderSettings.ProxySettings.HasTag() && (h.streamSettings.SocketSettings == nil || len(h.streamSettings.SocketSettings.DialerProxy) == 0) { if ob.Gateway == nil && h.senderSettings != nil && h.senderSettings.Via != nil &&
(h.streamSettings.SocketSettings == nil || len(h.streamSettings.SocketSettings.DialerProxy) == 0) {
var domain string var domain string
addr := h.senderSettings.Via.AsAddress() addr := h.senderSettings.Via.AsAddress()
domain = h.senderSettings.Via.GetDomain() domain = h.senderSettings.Via.GetDomain()
switch { switch {
case h.senderSettings.ViaCidr != "": case h.senderSettings.ViaCidr != "":
ob.Gateway = ParseRandomIP(addr, h.senderSettings.ViaCidr) ob.Gateway = ParseRandomIP(addr, h.senderSettings.ViaCidr)
case domain == "origin": case domain == "origin":
if inbound := session.InboundFromContext(ctx); inbound != nil { if inbound := session.InboundFromContext(ctx); inbound != nil {
if inbound.Local.IsValid() && inbound.Local.Address.Family().IsIP() { if inbound.Local.IsValid() && inbound.Local.Address.Family().IsIP() {
@@ -344,11 +300,9 @@ func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound)
errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String()) errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String())
} }
} }
// case addr.Family().IsDomain(): default: // case addr.Family().IsDomain():
default:
ob.Gateway = addr ob.Gateway = addr
} }
} }
} }
+3 -1
View File
@@ -118,7 +118,9 @@ func (w *BufferedWriter) Write(b []byte) (int, error) {
nBytes, err := w.buffer.Write(b) nBytes, err := w.buffer.Write(b)
totalBytes += nBytes totalBytes += nBytes
if err != nil {
// ErrBufferFull means a partial write, so flush below and continue
if err != nil && err != ErrBufferFull {
return totalBytes, err return totalBytes, err
} }
if !w.buffered || w.buffer.IsFull() { if !w.buffered || w.buffer.IsFull() {
-8
View File
@@ -1,18 +1,10 @@
package quic package quic
import ( import (
"crypto"
"crypto/cipher" "crypto/cipher"
_ "crypto/tls" _ "crypto/tls"
_ "unsafe" _ "unsafe"
) )
type CipherSuiteTLS13 struct {
ID uint16
KeyLen int
AEAD func(key, fixedNonce []byte) cipher.AEAD
Hash crypto.Hash
}
//go:linkname AEADAESGCMTLS13 crypto/tls.aeadAESGCMTLS13 //go:linkname AEADAESGCMTLS13 crypto/tls.aeadAESGCMTLS13
func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD
+89 -74
View File
@@ -3,7 +3,6 @@ package quic
import ( import (
"crypto" "crypto"
"crypto/aes" "crypto/aes"
"crypto/tls"
"encoding/binary" "encoding/binary"
"io" "io"
@@ -28,22 +27,43 @@ func (s SniffHeader) Domain() string {
return s.domain return s.domain
} }
const ( var (
versionDraft29 uint32 = 0xff00001d errNotQUIC = errors.New("not quic")
version1 uint32 = 0x1 errNotQUICInitial = errors.New("not initial packet")
) )
type quicVersionSpec struct {
ver uint32
typeInitial byte
initialSalt []byte
labelPrefix string
}
var ( var (
quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99} quicDraft29 = quicVersionSpec{
quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} ver: 0xff00001d,
initialSuite = &CipherSuiteTLS13{ typeInitial: 0b00,
ID: tls.TLS_AES_128_GCM_SHA256, initialSalt: []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99},
KeyLen: 16, labelPrefix: "quic",
AEAD: AEADAESGCMTLS13, }
Hash: crypto.SHA256, quicV1 = quicVersionSpec{
ver: 0x1,
typeInitial: 0b00,
initialSalt: []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a},
labelPrefix: "quic",
}
quicV2 = quicVersionSpec{
ver: 0x6b3343cf,
typeInitial: 0b01,
initialSalt: []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9},
labelPrefix: "quicv2",
}
quicVersionSpecMap = map[uint32]*quicVersionSpec{
quicDraft29.ver: &quicDraft29,
quicV1.ver: &quicV1,
quicV2.ver: &quicV2,
} }
errNotQuic = errors.New("not quic")
errNotQuicInitial = errors.New("not initial packet")
) )
func SniffQUIC(b []byte) (*SniffHeader, error) { func SniffQUIC(b []byte) (*SniffHeader, error) {
@@ -63,60 +83,61 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
buffer := buf.FromBytes(b) buffer := buf.FromBytes(b)
typeByte, err := buffer.ReadByte() typeByte, err := buffer.ReadByte()
if err != nil { if err != nil {
return nil, errNotQuic return nil, errNotQUIC
} }
isLongHeader := typeByte&0x80 > 0 isLongHeader := typeByte&0x80 > 0
if !isLongHeader || typeByte&0x40 == 0 { if !isLongHeader || typeByte&0x40 == 0 {
return nil, errNotQuicInitial return nil, errNotQUICInitial
} }
vb, err := buffer.ReadBytes(4) vb, err := buffer.ReadBytes(4)
if err != nil { if err != nil {
return nil, errNotQuic return nil, errNotQUIC
} }
versionNumber := binary.BigEndian.Uint32(vb) versionNumber := binary.BigEndian.Uint32(vb)
if versionNumber != 0 && typeByte&0x40 == 0 { var s *quicVersionSpec
return nil, errNotQuic if v, ok := quicVersionSpecMap[versionNumber]; ok {
} else if versionNumber != versionDraft29 && versionNumber != version1 { s = v
return nil, errNotQuic } else {
return nil, errNotQUIC
} }
packetType := (typeByte & 0x30) >> 4
isQuicInitial := packetType == 0x0
var destConnID []byte var destConnID []byte
if l, err := buffer.ReadByte(); err != nil { if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic return nil, errNotQUIC
} else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil { } else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil {
return nil, errNotQuic return nil, errNotQUIC
} }
if l, err := buffer.ReadByte(); err != nil { if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic return nil, errNotQUIC
} else if common.Error2(buffer.ReadBytes(int32(l))) != nil { } else if common.Error2(buffer.ReadBytes(int32(l))) != nil {
return nil, errNotQuic return nil, errNotQUIC
} }
if isQuicInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2 packetType := (typeByte & 0x30) >> 4
tokenLen, err := readShortQuicVarint(buffer) isQUICInitial := packetType == s.typeInitial
if isQUICInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
tokenLen, err := readShortQUICVarint(buffer)
if err != nil || tokenLen > int32(len(b)) { if err != nil || tokenLen > int32(len(b)) {
return nil, errNotQuic return nil, errNotQUIC
} }
if _, err = buffer.ReadBytes(tokenLen); err != nil { if _, err = buffer.ReadBytes(tokenLen); err != nil {
return nil, errNotQuic return nil, errNotQUIC
} }
} }
packetLen, err := readShortQuicVarint(buffer) packetLen, err := readShortQUICVarint(buffer)
if err != nil { if err != nil {
return nil, errNotQuic return nil, errNotQUIC
} }
// packetLen is impossible to be shorter than this // packetLen is impossible to be shorter than this
if packetLen < 4 { if packetLen < 4 {
return nil, errNotQuic return nil, errNotQUIC
} }
hdrLen := len(b) - int(buffer.Len()) hdrLen := len(b) - int(buffer.Len())
@@ -125,26 +146,22 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
} }
restPayload := b[hdrLen+int(packetLen):] restPayload := b[hdrLen+int(packetLen):]
if !isQuicInitial { // Skip this packet if it's not initial packet if !isQUICInitial { // Skip this packet if it's not initial packet
b = restPayload b = restPayload
continue continue
} }
var salt []byte salt := s.initialSalt
if versionNumber == version1 { label := s.labelPrefix
salt = quicSalt
} else {
salt = quicSaltOld
}
initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt) initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt)
secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size()) secret := hkdfExpandLabel(initialSecret, "client in", crypto.SHA256.Size())
hpKey := hkdfExpandLabel(initialSuite.Hash, secret, []byte{}, "quic hp", initialSuite.KeyLen) hpKey := hkdfExpandLabel(secret, label+" hp", 16)
block, err := aes.NewCipher(hpKey) block, err := aes.NewCipher(hpKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(b) < hdrLen+4+block.BlockSize() { if len(b) < hdrLen+4+block.BlockSize() {
return nil, errNotQuic return nil, errNotQUIC
} }
cache.Clear() cache.Clear()
mask := cache.Extend(int32(block.BlockSize())) mask := cache.Extend(int32(block.BlockSize()))
@@ -155,8 +172,8 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
b[hdrLen+i] ^= mask[i+1] b[hdrLen+i] ^= mask[i+1]
} }
key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16) key := hkdfExpandLabel(secret, label+" key", 16)
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12) iv := hkdfExpandLabel(secret, label+" iv", 12)
cipher := AEADAESGCMTLS13(key, iv) cipher := AEADAESGCMTLS13(key, iv)
nonce := cache.Extend(int32(cipher.NonceSize())) nonce := cache.Extend(int32(cipher.NonceSize()))
@@ -181,44 +198,44 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
case 0x00: // PADDING frame case 0x00: // PADDING frame
case 0x01: // PING frame case 0x01: // PING frame
case 0x02, 0x03: // ACK frame case 0x02, 0x03: // ACK frame
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Largest Acknowledged if _, err = readShortQUICVarint(buffer); err != nil { // Field: Largest Acknowledged
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Delay if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Delay
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
ackRangeCount, err := readShortQuicVarint(buffer) // Field: ACK Range Count ackRangeCount, err := readShortQUICVarint(buffer) // Field: ACK Range Count
if err != nil { if err != nil {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = readShortQuicVarint(buffer); err != nil { // Field: First ACK Range if _, err = readShortQUICVarint(buffer); err != nil { // Field: First ACK Range
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> Gap if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> Gap
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
} }
if frameType == 0x03 { if frameType == 0x03 {
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = readShortQuicVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count if _, err = readShortQUICVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
} }
case 0x06: // CRYPTO frame, we will use this frame case 0x06: // CRYPTO frame, we will use this frame
offset, err := readShortQuicVarint(buffer) // Field: Offset offset, err := readShortQUICVarint(buffer) // Field: Offset
if err != nil { if err != nil {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
length, err := readShortQuicVarint(buffer) // Field: Length length, err := readShortQUICVarint(buffer) // Field: Length
if err != nil || length > buffer.Len() { if err != nil || length > buffer.Len() {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
@@ -234,13 +251,13 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Error Code if _, err = readShortQUICVarint(buffer); err != nil { // Field: Error Code
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Frame Type if _, err = readShortQUICVarint(buffer); err != nil { // Field: Frame Type
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
length, err := readShortQuicVarint(buffer) // Field: Reason Phrase Length length, err := readShortQUICVarint(buffer) // Field: Reason Phrase Length
if err != nil { if err != nil {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
@@ -250,7 +267,7 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
default: default:
// Only above frame types are permitted in initial packet. // Only above frame types are permitted in initial packet.
// See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8 // See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8
return nil, errNotQuicInitial return nil, errNotQUICInitial
} }
} }
@@ -268,35 +285,33 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, protocol.ErrProtoNeedMoreData return nil, protocol.ErrProtoNeedMoreData
} }
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte { func hkdfExpandLabel(secret []byte, label string, length int) []byte {
b := make([]byte, 3, 3+6+len(label)+1+len(context)) b := make([]byte, 0, 2+1+6+len(label)+1)
binary.BigEndian.PutUint16(b, uint16(length)) b = binary.BigEndian.AppendUint16(b, uint16(length))
b[2] = uint8(6 + len(label)) b = append(b, byte(6+len(label)))
b = append(b, []byte("tls13 ")...) b = append(b, "tls13 "...)
b = append(b, []byte(label)...) b = append(b, label...)
b = b[:3+6+len(label)+1] b = append(b, 0) // context
b[3+6+len(label)] = uint8(len(context))
b = append(b, context...)
out := make([]byte, length) out := make([]byte, length)
n, err := hkdf.Expand(hash.New, secret, b).Read(out) n, err := hkdf.Expand(crypto.SHA256.New, secret, b).Read(out)
if err != nil || n != length { if err != nil || n != length {
panic("quic: HKDF-Expand-Label invocation failed unexpectedly") panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
} }
return out return out
} }
// readShortQuicVarint wraps quicvarint.Read with a max limit for length related fields. // readShortQUICVarint wraps quicvarint.Read with a max limit for length related fields.
// we only handle QUIC Initial so these numbers should not exceed 65535 // we only handle QUIC Initial so these numbers should not exceed 65535
// returns int32 to reduce type conversion // returns int32 to reduce type conversion
func readShortQuicVarint(reader io.ByteReader) (int32, error) { func readShortQUICVarint(reader io.ByteReader) (int32, error) {
v, err := quicvarint.Read(reader) v, err := quicvarint.Read(reader)
if err != nil { if err != nil {
return 0, err return 0, err
} }
if v > 65535 { if v > 65535 {
// not used( // not used(
return 0, errNotQuicInitial return 0, errNotQUICInitial
} }
return int32(v), nil return int32(v), nil
} }
File diff suppressed because one or more lines are too long
+1
View File
@@ -207,6 +207,7 @@ func getConfig() string {
"tag": "XHTTP_IN", "tag": "XHTTP_IN",
"streamSettings": { "streamSettings": {
"network": "xhttp", "network": "xhttp",
"security": "tls",
"xhttpSettings": { "xhttpSettings": {
"host": "bing.com", "host": "bing.com",
"path": "/xhttp_client_upload", "path": "/xhttp_client_upload",
-2
View File
@@ -70,8 +70,6 @@ type Outbound struct {
Tag string Tag string
// Name of the outbound proxy that handles the connection. // Name of the outbound proxy that handles the connection.
Name string Name string
// Unused. Conn is actually internet.Connection. May be nil. It is currently nil for outbound with proxySettings
Conn net.Conn
// CanSpliceCopy is a property for this connection // CanSpliceCopy is a property for this connection
// 1 = can, 2 = after processing protocol info should be able to, 3 = cannot // 1 = can, 2 = after processing protocol info should be able to, 3 = cannot
CanSpliceCopy int CanSpliceCopy int
+2 -2
View File
@@ -19,8 +19,8 @@ import (
var ( var (
Version_x byte = 26 Version_x byte = 26
Version_y byte = 7 Version_y byte = 9
Version_z byte = 28 Version_z byte = 8
) )
var ( var (
+10 -4
View File
@@ -1,9 +1,9 @@
module github.com/xtls/xray-core module github.com/xtls/xray-core
go 1.26 go 1.27
require ( 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/cloudflare/circl v1.6.5
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1 github.com/golang/mock v1.7.0-rc.1
@@ -11,6 +11,7 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/klauspost/cpuid/v2 v2.4.0 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/miekg/dns v1.1.73
github.com/pelletier/go-toml v1.9.5 github.com/pelletier/go-toml v1.9.5
github.com/pion/stun/v3 v3.1.7 github.com/pion/stun/v3 v3.1.7
@@ -21,7 +22,7 @@ require (
github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks v0.2.7
github.com/stretchr/testify v1.12.1 github.com/stretchr/testify v1.12.1
github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.55.0 golang.org/x/crypto v0.55.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
@@ -31,7 +32,7 @@ require (
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.0.1 golang.zx2c4.com/wireguard/windows v1.0.1
google.golang.org/grpc v1.83.1 google.golang.org/grpc v1.83.2
google.golang.org/protobuf v1.36.12 google.golang.org/protobuf v1.36.12
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3 h12.io/socks v1.0.3
@@ -41,9 +42,14 @@ require (
require ( require (
github.com/andybalholm/brotli v1.0.6 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
github.com/google/btree v1.1.2 // 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/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.17.4 // 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/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/dtls/v3 v3.1.5 // indirect
github.com/pion/logging v0.2.4 // indirect github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.1.0 // indirect github.com/pion/transport/v4 v4.1.0 // indirect
+23 -6
View File
@@ -1,7 +1,7 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 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.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
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/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA= 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/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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI=
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c= 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 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= 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 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 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 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= 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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ= github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 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/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 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24= 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 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= 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= github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
@@ -73,8 +87,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8= github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0 h1:rb+fKQFhz+5I2PPuQsNYxI5mUU840XWYtRF0ZBjvkws=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI= github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
@@ -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/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 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 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/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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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/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-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-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/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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -143,8 +160,8 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+20 -30
View File
@@ -1,52 +1,42 @@
package conf package conf
import ( import (
"encoding/json" "encoding/base64"
"strings"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/proxy/blackhole" "github.com/xtls/xray-core/proxy/blackhole"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
type NoneResponse struct{} type ResponseConfig struct {
Type string `json:"type"`
func (*NoneResponse) Build() (proto.Message, error) { CustomResponseData string `json:"customResponseData"`
return new(blackhole.NoneResponse), nil
}
type HTTPResponse struct{}
func (*HTTPResponse) Build() (proto.Message, error) {
return new(blackhole.HTTPResponse), nil
} }
type BlackholeConfig struct { type BlackholeConfig struct {
Response json.RawMessage `json:"response"` Response *ResponseConfig `json:"response"`
} }
func (v *BlackholeConfig) Build() (proto.Message, error) { func (v *BlackholeConfig) Build() (proto.Message, error) {
config := new(blackhole.Config) config := new(blackhole.Config)
if v.Response != nil { if v.Response != nil {
response, _, err := configLoader.Load(v.Response) responseName := strings.ToLower(v.Response.Type)
if err != nil { switch responseName {
return nil, errors.New("Config: Failed to parse Blackhole response config.").Base(err) case "none", "":
config.Response = &blackhole.Response{Type: "none"}
case "http":
config.Response = &blackhole.Response{Type: "http"}
case "custom":
data, err := base64.StdEncoding.DecodeString(v.Response.CustomResponseData)
if err != nil {
return nil, errors.New("failed to decode custom response data: " + err.Error())
}
config.Response = &blackhole.Response{Type: "custom", CustomResponseData: data}
default:
return nil, errors.New("unknown blackhole response: " + responseName)
} }
responseSettings, err := response.(Buildable).Build()
if err != nil {
return nil, err
}
config.Response = serial.ToTypedMessage(responseSettings)
} }
return config, nil return config, nil
} }
var configLoader = NewJSONConfigLoader(
ConfigCreatorCache{
"none": func() interface{} { return new(NoneResponse) },
"http": func() interface{} { return new(HTTPResponse) },
},
"type",
"",
)
+25 -2
View File
@@ -3,7 +3,6 @@ package conf_test
import ( import (
"testing" "testing"
"github.com/xtls/xray-core/common/serial"
. "github.com/xtls/xray-core/infra/conf" . "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/blackhole" "github.com/xtls/xray-core/proxy/blackhole"
) )
@@ -22,7 +21,7 @@ func TestHTTPResponseJSON(t *testing.T) {
}`, }`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &blackhole.Config{ Output: &blackhole.Config{
Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}), Response: &blackhole.Response{Type: "http"},
}, },
}, },
{ {
@@ -32,3 +31,27 @@ func TestHTTPResponseJSON(t *testing.T) {
}, },
}) })
} }
func TestCustomResponseJSON(t *testing.T) {
creator := func() Buildable {
return new(BlackholeConfig)
}
runMultiTestCase(t, []TestCase{
{
Input: `{
"response": {
"type": "custom",
"customResponseData": "Y3VzdG9tIHJlc3BvbnNl"
}
}`,
Parser: loadJSON(creator),
Output: &blackhole.Config{
Response: &blackhole.Response{
Type: "custom",
CustomResponseData: []byte("custom response"),
},
},
},
})
}
+3
View File
@@ -65,6 +65,9 @@ func (v *Address) UnmarshalJSON(data []byte) error {
} }
func (v *Address) Build() *net.IPOrDomain { func (v *Address) Build() *net.IPOrDomain {
if v == nil {
return nil
}
return net.NewIPOrDomain(v.Address) return net.NewIPOrDomain(v.Address)
} }
+1 -1
View File
@@ -23,7 +23,7 @@ func (o *ObservatoryConfig) Build() (proto.Message, error) {
type BurstObservatoryConfig struct { type BurstObservatoryConfig struct {
SubjectSelector []string `json:"subjectSelector"` SubjectSelector []string `json:"subjectSelector"`
// health check settings // health check settings
HealthCheck *healthCheckSettings `json:"pingConfig,omitempty"` HealthCheck *HealthCheckSettings `json:"pingConfig,omitempty"`
} }
func (b BurstObservatoryConfig) Build() (proto.Message, error) { func (b BurstObservatoryConfig) Build() (proto.Message, error) {
+3 -3
View File
@@ -43,8 +43,8 @@ type strategyLeastLoadConfig struct {
Tolerance float64 `json:"tolerance,omitempty"` Tolerance float64 `json:"tolerance,omitempty"`
} }
// healthCheckSettings holds settings for health Checker // HealthCheckSettings holds settings for health Checker
type healthCheckSettings struct { type HealthCheckSettings struct {
Destination string `json:"destination"` Destination string `json:"destination"`
Connectivity string `json:"connectivity"` Connectivity string `json:"connectivity"`
Interval duration.Duration `json:"interval"` Interval duration.Duration `json:"interval"`
@@ -53,7 +53,7 @@ type healthCheckSettings struct {
HttpMethod string `json:"httpMethod"` HttpMethod string `json:"httpMethod"`
} }
func (h healthCheckSettings) Build() (proto.Message, error) { func (h HealthCheckSettings) Build() (proto.Message, error) {
var httpMethod string var httpMethod string
if h.HttpMethod == "" { if h.HttpMethod == "" {
httpMethod = "HEAD" httpMethod = "HEAD"
+25 -17
View File
@@ -816,9 +816,11 @@ func (c *Xicmp) Build() (proto.Message, error) {
} }
type Realm struct { type Realm struct {
Url string `json:"url"` Url string `json:"url"`
StunServers []string `json:"stunServers"` StunServers []string `json:"stunServers"`
TlsConfig *TLSConfig `json:"tlsConfig"` TlsConfig *TLSConfig `json:"tlsConfig"`
IPMode string `json:"ipMode"`
PortMapping *realm.PortMapping `json:"portMapping"`
} }
func (c *Realm) Build() (proto.Message, error) { func (c *Realm) Build() (proto.Message, error) {
@@ -898,6 +900,8 @@ func (c *Realm) Build() (proto.Message, error) {
ID: id, ID: id,
StunServers: stunServers, StunServers: stunServers,
TlsConfig: tlsConfig, TlsConfig: tlsConfig,
IPMode: strings.ToLower(c.IPMode),
PortMapping: c.PortMapping,
}, nil }, nil
} }
@@ -928,20 +932,24 @@ func (c *Mask) Build(tcp bool) (proto.Message, error) {
} }
type QuicParamsConfig struct { type QuicParamsConfig struct {
Congestion string `json:"congestion"` Congestion string `json:"congestion"`
Debug bool `json:"debug"` Debug bool `json:"debug"`
BbrProfile string `json:"bbrProfile"` BbrProfile string `json:"bbrProfile"`
BrutalUp Bandwidth `json:"brutalUp"` BrutalUp Bandwidth `json:"brutalUp"`
BrutalDown Bandwidth `json:"brutalDown"` BrutalDown Bandwidth `json:"brutalDown"`
UdpHop UdpHop `json:"udpHop"` BrutalDisableLossCompensation bool `json:"brutalDisableLossCompensation"`
InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"` UdpHop UdpHop `json:"udpHop"`
MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"` InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"`
InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"` MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"`
MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"` InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"`
MaxIdleTimeout int64 `json:"maxIdleTimeout"` MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"`
KeepAlivePeriod int64 `json:"keepAlivePeriod"` MaxIdleTimeout int64 `json:"maxIdleTimeout"`
DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"` KeepAlivePeriod int64 `json:"keepAlivePeriod"`
MaxIncomingStreams int64 `json:"maxIncomingStreams"` DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"`
DisableChromeParrot bool `json:"disableChromeParrot"`
DisableGSO bool `json:"disableGSO"`
MaxIncomingStreams int64 `json:"maxIncomingStreams"`
DisableStatelessReset bool `json:"disableStatelessReset"`
} }
type FinalMask struct { type FinalMask struct {
+8 -22
View File
@@ -285,10 +285,11 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
} }
config.QuicParams = &internet.QuicParams{ config.QuicParams = &internet.QuicParams{
Congestion: c.FinalMask.QuicParams.Congestion, Congestion: c.FinalMask.QuicParams.Congestion,
BbrProfile: profile, BbrProfile: profile,
BrutalUp: up, BrutalUp: up,
BrutalDown: down, BrutalDown: down,
BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation,
UdpHop: &internet.UdpHop{ UdpHop: &internet.UdpHop{
Ports: c.FinalMask.QuicParams.UdpHop.PortList.Build().Ports(), Ports: c.FinalMask.QuicParams.UdpHop.PortList.Build().Ports(),
IntervalMin: int64(c.FinalMask.QuicParams.UdpHop.Interval.From), IntervalMin: int64(c.FinalMask.QuicParams.UdpHop.Interval.From),
@@ -301,28 +302,13 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout, MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod, KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery, DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot,
DisableGSO: c.FinalMask.QuicParams.DisableGSO,
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams, MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset,
} }
} }
} }
return config, nil return config, nil
} }
type ProxyConfig struct {
Tag string `json:"tag"`
// TransportLayerProxy: For compatibility.
TransportLayerProxy bool `json:"transportLayer"`
}
// Build implements Buildable.
func (v *ProxyConfig) Build() (*internet.ProxyConfig, error) {
if v.Tag == "" {
return nil, errors.New("Proxy tag is not set.")
}
return &internet.ProxyConfig{
Tag: v.Tag,
TransportLayerProxy: v.TransportLayerProxy,
}, nil
}
+2
View File
@@ -751,6 +751,7 @@ type Masquerade struct {
Url string `json:"url"` Url string `json:"url"`
RewriteHost bool `json:"rewriteHost"` RewriteHost bool `json:"rewriteHost"`
XForwarded bool `json:"xForwarded"`
Insecure bool `json:"insecure"` Insecure bool `json:"insecure"`
Content string `json:"content"` Content string `json:"content"`
@@ -791,6 +792,7 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
config.MasqFile = c.Masquerade.Dir config.MasqFile = c.Masquerade.Dir
config.MasqUrl = c.Masquerade.Url config.MasqUrl = c.Masquerade.Url
config.MasqUrlRewriteHost = c.Masquerade.RewriteHost config.MasqUrlRewriteHost = c.Masquerade.RewriteHost
config.MasqUrlXForwarded = c.Masquerade.XForwarded
config.MasqUrlInsecure = c.Masquerade.Insecure config.MasqUrlInsecure = c.Masquerade.Insecure
config.MasqString = c.Masquerade.Content config.MasqString = c.Masquerade.Content
config.MasqStringHeaders = c.Masquerade.Headers config.MasqStringHeaders = c.Masquerade.Headers
+3 -3
View File
@@ -113,10 +113,10 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
config.MinClientVer[i] = byte(u) config.MinClientVer[i] = byte(u)
} }
} }
errors.LogWarning(context.Background(), `REALITY: Changing "minClientVer" will increase the likelihood of your server's IP being blocked by the GFW`) // errors.LogWarning(context.Background(), `REALITY: Changing "minClientVer" will increase the likelihood of your server's IP being blocked by the GFW`)
} else { } else {
config.MinClientVer = []byte{26, 3, 27} // change it at your own risk: https://github.com/XTLS/Xray-core/commit/af7eb68028732a8ee3c0e5d6ab2b8a657bb2e770 // config.MinClientVer = []byte{26, 3, 27} // change it at your own risk: https://github.com/XTLS/Xray-core/commit/af7eb68028732a8ee3c0e5d6ab2b8a657bb2e770
errors.LogWarning(context.Background(), `REALITY: The default minimal client version is Xray-core v26.3.27, other clients may be refused to connect`) // errors.LogWarning(context.Background(), `REALITY: The default minimal client version is Xray-core v26.3.27, other clients may be refused to connect`)
} }
if c.MaxClientVer != "" { if c.MaxClientVer != "" {
config.MaxClientVer = make([]byte, 3) config.MaxClientVer = make([]byte, 3)
+2 -2
View File
@@ -10,7 +10,7 @@ import (
) )
type CustomSockoptConfig struct { type CustomSockoptConfig struct {
Syetem string `json:"system"` System string `json:"system"`
Network string `json:"network"` Network string `json:"network"`
Level string `json:"level"` Level string `json:"level"`
Opt string `json:"opt"` Opt string `json:"opt"`
@@ -124,7 +124,7 @@ func (c *SocketConfig) Build() (*internet.SocketConfig, error) {
for _, copt := range c.CustomSockopt { for _, copt := range c.CustomSockopt {
customSockopt := &internet.CustomSockopt{ customSockopt := &internet.CustomSockopt{
System: copt.Syetem, System: copt.System,
Network: copt.Network, Network: copt.Network,
Level: copt.Level, Level: copt.Level,
Opt: copt.Opt, Opt: copt.Opt,
+37 -40
View File
@@ -15,6 +15,7 @@ import (
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
core "github.com/xtls/xray-core/core" core "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/proxy/freedom"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
) )
@@ -140,7 +141,7 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
// TUN inbound doesn't need port configuration as it uses network interface instead // TUN inbound doesn't need port configuration as it uses network interface instead
if strings.ToLower(c.Protocol) == "tun" { if strings.ToLower(c.Protocol) == "tun" {
// Skip port validation for TUN // Skip port validation for TUN
} else if c.ListenOn == nil { } else if c.ListenOn == nil || len(c.ListenOn.String()) == 0 {
// Listen on anyip, must set PortList // Listen on anyip, must set PortList
if c.PortList == nil { if c.PortList == nil {
return nil, errors.New("Listen on AnyIP but no Port(s) set in InboundDetour.") return nil, errors.New("Listen on AnyIP but no Port(s) set in InboundDetour.")
@@ -216,21 +217,11 @@ type OutboundDetourConfig struct {
Tag string `json:"tag"` Tag string `json:"tag"`
Settings *json.RawMessage `json:"settings"` Settings *json.RawMessage `json:"settings"`
StreamSetting *StreamConfig `json:"streamSettings"` StreamSetting *StreamConfig `json:"streamSettings"`
ProxySettings *ProxyConfig `json:"proxySettings"` ProxySettings *json.RawMessage `json:"proxySettings"`
MuxSettings *MuxConfig `json:"mux"` MuxSettings *MuxConfig `json:"mux"`
TargetStrategy string `json:"targetStrategy"` TargetStrategy string `json:"targetStrategy"`
} }
func (c *OutboundDetourConfig) checkChainProxyConfig() error {
if c.StreamSetting == nil || c.ProxySettings == nil || c.StreamSetting.SocketSettings == nil {
return nil
}
if len(c.ProxySettings.Tag) > 0 && len(c.StreamSetting.SocketSettings.DialerProxy) > 0 {
return errors.New("proxySettings.tag is conflicted with sockopt.dialerProxy").AtWarning()
}
return nil
}
func requiresTransportSecurity(address *Address) bool { func requiresTransportSecurity(address *Address) bool {
if address == nil || address.Address == nil { if address == nil || address.Address == nil {
return false return false
@@ -251,13 +242,13 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
if vlessCfg.Encryption != "" && vlessCfg.Encryption != "none" { if vlessCfg.Encryption != "" && vlessCfg.Encryption != "none" {
return nil return nil
} }
if requiresTransportSecurity(vlessCfg.Address) { if requiresTransportSecurity(vlessCfg.Vnext[0].Address) {
return errors.New("vless without TLS or other encryption is prohibited unless the server address is a private IP or domain") return errors.New("vless without TLS or other encryption is prohibited unless the server address is a private IP or domain")
} }
} }
if tjCfg, ok := rawConfig.(*TrojanClientConfig); ok { if tjCfg, ok := rawConfig.(*TrojanClientConfig); ok {
if requiresTransportSecurity(tjCfg.Address) { if requiresTransportSecurity(tjCfg.Servers[0].Address) {
return errors.New("trojan without TLS is prohibited unless the server address is a private IP or domain") return errors.New("trojan without TLS is prohibited unless the server address is a private IP or domain")
} }
} }
@@ -267,6 +258,10 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
// Build implements Buildable. // Build implements Buildable.
func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) { func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if c.ProxySettings != nil {
return nil, errors.PrintRemovedFeatureError(`outbound "proxySettings"`, `"streamSettings.sockopt.dialerProxy"`)
}
senderSettings := &proxyman.SenderConfig{} senderSettings := &proxyman.SenderConfig{}
switch strings.ToLower(c.TargetStrategy) { switch strings.ToLower(c.TargetStrategy) {
case "asis", "": case "asis", "":
@@ -294,9 +289,6 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
default: default:
return nil, errors.New("unsupported target domain strategy: ", c.TargetStrategy) return nil, errors.New("unsupported target domain strategy: ", c.TargetStrategy)
} }
if err := c.checkChainProxyConfig(); err != nil {
return nil, err
}
if c.SendThrough != nil { if c.SendThrough != nil {
address := ParseSendThough(c.SendThrough) address := ParseSendThough(c.SendThrough)
@@ -322,26 +314,6 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
senderSettings.StreamSettings = ss senderSettings.StreamSettings = ss
} }
if c.ProxySettings != nil {
ps, err := c.ProxySettings.Build()
if err != nil {
return nil, errors.New("invalid outbound detour proxy settings").Base(err)
}
if ps.TransportLayerProxy {
if senderSettings.StreamSettings != nil {
if senderSettings.StreamSettings.SocketSettings != nil {
senderSettings.StreamSettings.SocketSettings.DialerProxy = ps.Tag
} else {
senderSettings.StreamSettings.SocketSettings = &internet.SocketConfig{DialerProxy: ps.Tag}
}
} else {
senderSettings.StreamSettings = &internet.StreamConfig{SocketSettings: &internet.SocketConfig{DialerProxy: ps.Tag}}
}
ps = nil
}
senderSettings.ProxySettings = ps
}
if c.MuxSettings != nil { if c.MuxSettings != nil {
ms, err := c.MuxSettings.Build() ms, err := c.MuxSettings.Build()
if err != nil { if err != nil {
@@ -358,13 +330,38 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if err != nil { if err != nil {
return nil, errors.New("failed to load outbound detour config for protocol ", c.Protocol).Base(err) return nil, errors.New("failed to load outbound detour config for protocol ", c.Protocol).Base(err)
} }
if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil {
return nil, err
}
ts, err := rawConfig.(Buildable).Build() ts, err := rawConfig.(Buildable).Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err) return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err)
} }
if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil {
return nil, err
}
if fc, ok := ts.(*freedom.Config); ok {
if senderSettings.StreamSettings != nil &&
senderSettings.StreamSettings.SocketSettings != nil &&
senderSettings.StreamSettings.SocketSettings.AddressPortStrategy != internet.AddressPortStrategy_None {
return nil, errors.New(`freedom outbound does not support "sockopt.addressPortStrategy"`)
}
var strategy internet.DomainStrategy
if strategy = senderSettings.TargetStrategy; strategy != internet.DomainStrategy_AS_IS {
errors.LogWarning(context.Background(), `The "outbound.targetStrategy" setting is not supported directly by freedom and has been automatically migrated to "sockopt.domainStrategy" with no behavior change.`)
senderSettings.TargetStrategy = internet.DomainStrategy_AS_IS
} else if strategy = fc.DomainStrategy; strategy != internet.DomainStrategy_AS_IS {
errors.LogWarning(context.Background(), `The "freedom.domainStrategy" setting is deprecated and will be removed. For compatibility, its value has been automatically migrated to "sockopt.domainStrategy". Please update your config before removal.`)
}
if strategy != internet.DomainStrategy_AS_IS {
if senderSettings.StreamSettings == nil {
senderSettings.StreamSettings = &internet.StreamConfig{}
}
if senderSettings.StreamSettings.SocketSettings == nil {
senderSettings.StreamSettings.SocketSettings = &internet.SocketConfig{}
}
senderSettings.StreamSettings.SocketSettings.DomainStrategy = strategy
}
}
return &core.OutboundHandlerConfig{ return &core.OutboundHandlerConfig{
SenderSettings: serial.ToTypedMessage(senderSettings), SenderSettings: serial.ToTypedMessage(senderSettings),
+31 -6
View File
@@ -2,12 +2,15 @@
package blackhole package blackhole
import ( import (
"bytes"
"context" "context"
"net/http"
"time" "time"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/dice" "github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal" "github.com/xtls/xray-core/common/signal"
@@ -17,14 +20,34 @@ import (
// Handler is an outbound connection that silently swallow the entire payload. // Handler is an outbound connection that silently swallow the entire payload.
type Handler struct { type Handler struct {
response ResponseConfig response []byte
}
var http403response = http.Response{
StatusCode: 403,
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{
"Connection": {"close"},
"Cache-Control": {"max-age=3600, public"},
},
} }
// New creates a new blackhole handler. // New creates a new blackhole handler.
func New(ctx context.Context, config *Config) (*Handler, error) { func New(ctx context.Context, config *Config) (*Handler, error) {
response, err := config.GetInternalResponse() response := []byte{}
if err != nil { if config.Response != nil {
return nil, err switch config.Response.Type {
case "", "none":
case "http":
var data bytes.Buffer
common.Must(http403response.Write(&data))
response = data.Bytes()
case "custom":
response = config.Response.CustomResponseData
default:
return nil, errors.New("unknown blackhole response: " + config.Response.Type)
}
} }
return &Handler{ return &Handler{
response: response, response: response,
@@ -37,8 +60,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ob := outbounds[len(outbounds)-1] ob := outbounds[len(outbounds)-1]
ob.Name = "blackhole" ob.Name = "blackhole"
nBytes := h.response.WriteTo(link.Writer) if len(h.response) > 0 {
if nBytes > 0 { mbc := buf.MultiBufferContainer{}
common.Must2(mbc.Write(h.response))
link.Writer.WriteMultiBuffer(mbc.MultiBuffer)
// Sleep a little here to make sure the response is sent to client. // Sleep a little here to make sure the response is sent to client.
time.Sleep(time.Second) time.Sleep(time.Second)
} }
+45 -11
View File
@@ -1,12 +1,15 @@
package blackhole_test package blackhole_test
import ( import (
"bufio"
"bytes"
"context" "context"
"crypto/rand"
"net/http"
"testing" "testing"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/proxy/blackhole" "github.com/xtls/xray-core/proxy/blackhole"
"github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport"
@@ -16,27 +19,58 @@ import (
func TestBlackholeHTTPResponse(t *testing.T) { func TestBlackholeHTTPResponse(t *testing.T) {
ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}}) ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}})
handler, err := blackhole.New(ctx, &blackhole.Config{ handler, err := blackhole.New(ctx, &blackhole.Config{
Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}), Response: &blackhole.Response{Type: "http"},
}) })
common.Must(err) common.Must(err)
reader, writer := pipe.New(pipe.WithoutSizeLimit()) reader, writer := pipe.New(pipe.WithoutSizeLimit())
var mb buf.MultiBuffer dataCh := make(chan buf.MultiBuffer, 1)
var rerr error
go func() { go func() {
b, e := reader.ReadMultiBuffer() mb := common.Must2(reader.ReadMultiBuffer())
mb = b dataCh <- mb
rerr = e
}() }()
link := transport.Link{ link := transport.Link{
Reader: reader, Reader: reader,
Writer: writer, Writer: writer,
} }
common.Must(handler.Process(ctx, &link, nil)) common.Must(handler.Process(ctx, &link, nil))
common.Must(rerr) mb := <-dataCh
if mb.IsEmpty() { data := make([]byte, mb.Len())
t.Error("expect http response, but nothing") mb.Copy(data)
resp := common.Must2(http.ReadResponse(bufio.NewReader(bytes.NewBuffer(data)), nil))
if resp.StatusCode != 403 {
t.Errorf("expected 403 response, got %d", resp.StatusCode)
}
}
func TestBlackholeCustomResponse(t *testing.T) {
ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}})
// slightly bigger than a buffer
expected := make([]byte, buf.Size+1000)
if _, err := rand.Read(expected); err != nil {
t.Fatal(err)
}
handler, err := blackhole.New(ctx, &blackhole.Config{
Response: &blackhole.Response{
Type: "custom",
CustomResponseData: expected,
},
})
common.Must(err)
reader, writer := pipe.New(pipe.WithoutSizeLimit())
var actual buf.MultiBuffer
var rerr error
go func() {
actual, rerr = reader.ReadMultiBuffer()
}()
link := transport.Link{Reader: reader, Writer: writer}
common.Must(handler.Process(ctx, &link, nil))
common.Must(rerr)
if actual.String() != string(expected) {
t.Errorf("custom response mismatch")
} }
} }
-47
View File
@@ -1,47 +0,0 @@
package blackhole
import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
)
const (
http403response = `HTTP/1.1 403 Forbidden
Connection: close
Cache-Control: max-age=3600, public
Content-Length: 0
`
)
// ResponseConfig is the configuration for blackhole responses.
type ResponseConfig interface {
// WriteTo writes a predefined response to the specified buffer.
WriteTo(buf.Writer) int32
}
// WriteTo implements ResponseConfig.WriteTo().
func (*NoneResponse) WriteTo(buf.Writer) int32 { return 0 }
// WriteTo implements ResponseConfig.WriteTo().
func (*HTTPResponse) WriteTo(writer buf.Writer) int32 {
b := buf.New()
common.Must2(b.WriteString(http403response))
n := b.Len()
writer.WriteMultiBuffer(buf.MultiBuffer{b})
return n
}
// GetInternalResponse converts response settings from proto to internal data structure.
func (c *Config) GetInternalResponse() (ResponseConfig, error) {
if c.GetResponse() == nil {
return new(NoneResponse), nil
}
config, err := c.GetResponse().GetInstance()
if err != nil {
return nil, err
}
return config.(ResponseConfig), nil
}
+37 -59
View File
@@ -7,7 +7,6 @@
package blackhole package blackhole
import ( import (
serial "github.com/xtls/xray-core/common/serial"
protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
@@ -22,26 +21,28 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
) )
type NoneResponse struct { type Response struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
sizeCache protoimpl.SizeCache CustomResponseData []byte `protobuf:"bytes,2,opt,name=custom_response_data,json=customResponseData,proto3" json:"custom_response_data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *NoneResponse) Reset() { func (x *Response) Reset() {
*x = NoneResponse{} *x = Response{}
mi := &file_proxy_blackhole_config_proto_msgTypes[0] mi := &file_proxy_blackhole_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
func (x *NoneResponse) String() string { func (x *Response) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*NoneResponse) ProtoMessage() {} func (*Response) ProtoMessage() {}
func (x *NoneResponse) ProtoReflect() protoreflect.Message { func (x *Response) ProtoReflect() protoreflect.Message {
mi := &file_proxy_blackhole_config_proto_msgTypes[0] mi := &file_proxy_blackhole_config_proto_msgTypes[0]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -53,57 +54,35 @@ func (x *NoneResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use NoneResponse.ProtoReflect.Descriptor instead. // Deprecated: Use Response.ProtoReflect.Descriptor instead.
func (*NoneResponse) Descriptor() ([]byte, []int) { func (*Response) Descriptor() ([]byte, []int) {
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{0} return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{0}
} }
type HTTPResponse struct { func (x *Response) GetType() string {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HTTPResponse) Reset() {
*x = HTTPResponse{}
mi := &file_proxy_blackhole_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HTTPResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HTTPResponse) ProtoMessage() {}
func (x *HTTPResponse) ProtoReflect() protoreflect.Message {
mi := &file_proxy_blackhole_config_proto_msgTypes[1]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) return x.Type
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
} }
return mi.MessageOf(x) return ""
} }
// Deprecated: Use HTTPResponse.ProtoReflect.Descriptor instead. func (x *Response) GetCustomResponseData() []byte {
func (*HTTPResponse) Descriptor() ([]byte, []int) { if x != nil {
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{1} return x.CustomResponseData
}
return nil
} }
type Config struct { type Config struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Response *serial.TypedMessage `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"` Response *Response `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *Config) Reset() { func (x *Config) Reset() {
*x = Config{} *x = Config{}
mi := &file_proxy_blackhole_config_proto_msgTypes[2] mi := &file_proxy_blackhole_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -115,7 +94,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {} func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message { func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_proxy_blackhole_config_proto_msgTypes[2] mi := &file_proxy_blackhole_config_proto_msgTypes[1]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -128,10 +107,10 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead. // Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) { func (*Config) Descriptor() ([]byte, []int) {
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{2} return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{1}
} }
func (x *Config) GetResponse() *serial.TypedMessage { func (x *Config) GetResponse() *Response {
if x != nil { if x != nil {
return x.Response return x.Response
} }
@@ -142,11 +121,12 @@ var File_proxy_blackhole_config_proto protoreflect.FileDescriptor
const file_proxy_blackhole_config_proto_rawDesc = "" + const file_proxy_blackhole_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x1cproxy/blackhole/config.proto\x12\x14xray.proxy.blackhole\x1a!common/serial/typed_message.proto\"\x0e\n" + "\x1cproxy/blackhole/config.proto\x12\x14xray.proxy.blackhole\"P\n" +
"\fNoneResponse\"\x0e\n" + "\bResponse\x12\x12\n" +
"\fHTTPResponse\"F\n" + "\x04type\x18\x01 \x01(\tR\x04type\x120\n" +
"\x06Config\x12<\n" + "\x14custom_response_data\x18\x02 \x01(\fR\x12customResponseData\"D\n" +
"\bresponse\x18\x01 \x01(\v2 .xray.common.serial.TypedMessageR\bresponseB^\n" + "\x06Config\x12:\n" +
"\bresponse\x18\x01 \x01(\v2\x1e.xray.proxy.blackhole.ResponseR\bresponseB^\n" +
"\x18com.xray.proxy.blackholeP\x01Z)github.com/xtls/xray-core/proxy/blackhole\xaa\x02\x14Xray.Proxy.Blackholeb\x06proto3" "\x18com.xray.proxy.blackholeP\x01Z)github.com/xtls/xray-core/proxy/blackhole\xaa\x02\x14Xray.Proxy.Blackholeb\x06proto3"
var ( var (
@@ -161,15 +141,13 @@ func file_proxy_blackhole_config_proto_rawDescGZIP() []byte {
return file_proxy_blackhole_config_proto_rawDescData return file_proxy_blackhole_config_proto_rawDescData
} }
var file_proxy_blackhole_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_proxy_blackhole_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_proxy_blackhole_config_proto_goTypes = []any{ var file_proxy_blackhole_config_proto_goTypes = []any{
(*NoneResponse)(nil), // 0: xray.proxy.blackhole.NoneResponse (*Response)(nil), // 0: xray.proxy.blackhole.Response
(*HTTPResponse)(nil), // 1: xray.proxy.blackhole.HTTPResponse (*Config)(nil), // 1: xray.proxy.blackhole.Config
(*Config)(nil), // 2: xray.proxy.blackhole.Config
(*serial.TypedMessage)(nil), // 3: xray.common.serial.TypedMessage
} }
var file_proxy_blackhole_config_proto_depIdxs = []int32{ var file_proxy_blackhole_config_proto_depIdxs = []int32{
3, // 0: xray.proxy.blackhole.Config.response:type_name -> xray.common.serial.TypedMessage 0, // 0: xray.proxy.blackhole.Config.response:type_name -> xray.proxy.blackhole.Response
1, // [1:1] is the sub-list for method output_type 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 method input_type
1, // [1:1] is the sub-list for extension type_name 1, // [1:1] is the sub-list for extension type_name
@@ -188,7 +166,7 @@ func file_proxy_blackhole_config_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_blackhole_config_proto_rawDesc), len(file_proxy_blackhole_config_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_blackhole_config_proto_rawDesc), len(file_proxy_blackhole_config_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 3, NumMessages: 2,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
+5 -6
View File
@@ -6,12 +6,11 @@ option go_package = "github.com/xtls/xray-core/proxy/blackhole";
option java_package = "com.xray.proxy.blackhole"; option java_package = "com.xray.proxy.blackhole";
option java_multiple_files = true; option java_multiple_files = true;
import "common/serial/typed_message.proto"; message Response {
string type = 1;
message NoneResponse {} bytes custom_response_data = 2;
}
message HTTPResponse {}
message Config { message Config {
xray.common.serial.TypedMessage response = 1; Response response = 1;
} }
+7 -14
View File
@@ -1,26 +1,19 @@
package blackhole_test package blackhole_test
import ( import (
"bufio" "context"
"net/http"
"testing" "testing"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/proxy/blackhole"
. "github.com/xtls/xray-core/proxy/blackhole"
) )
func TestHTTPResponse(t *testing.T) { func TestHTTPResponse(t *testing.T) {
buffer := buf.New() handler, err := blackhole.New(context.Background(), &blackhole.Config{
Response: &blackhole.Response{Type: "http"},
httpResponse := new(HTTPResponse) })
httpResponse.WriteTo(buf.NewWriter(buffer))
reader := bufio.NewReader(buffer)
response, err := http.ReadResponse(reader, nil)
common.Must(err) common.Must(err)
if handler == nil {
if response.StatusCode != 403 { t.Error("expected HTTP response handler")
t.Error("expected status code 403, but got ", response.StatusCode)
} }
} }
+26 -6
View File
@@ -12,12 +12,29 @@ import (
) )
func FakeUDP(addr *net.UDPAddr, mark int) (net.PacketConn, error) { func FakeUDP(addr *net.UDPAddr, mark int) (net.PacketConn, error) {
ip4 := addr.IP.To4() domain := unix.AF_INET6
if ip4 == nil { var sockaddr unix.Sockaddr
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("IPv6 is not supported by the OpenBSD transparent UDP patch")} if ip4 := addr.IP.To4(); ip4 != nil {
domain = unix.AF_INET
sa := &unix.SockaddrInet4{Port: addr.Port}
copy(sa.Addr[:], ip4)
sockaddr = sa
} else if ip6 := addr.IP.To16(); ip6 != nil {
sa := &unix.SockaddrInet6{Port: addr.Port}
copy(sa.Addr[:], ip6)
if addr.Zone != "" {
iface, err := net.InterfaceByName(addr.Zone)
if err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("resolve zone %s: %w", addr.Zone, err)}
}
sa.ZoneId = uint32(iface.Index)
}
sockaddr = sa
} else {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("unsupported address %v", addr.IP)}
} }
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) fd, err := unix.Socket(domain, unix.SOCK_DGRAM, 0)
if err != nil { if err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("socket open: %w", err)} return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("socket open: %w", err)}
} }
@@ -35,9 +52,12 @@ func FakeUDP(addr *net.UDPAddr, mark int) (net.PacketConn, error) {
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil { if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_REUSEADDR: %w", err)} return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_REUSEADDR: %w", err)}
} }
// Several client sessions can be answered from the same original
// destination at the same time, so the address has to be shareable.
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option SO_REUSEPORT: %w", err)}
}
sockaddr := &unix.SockaddrInet4{Port: addr.Port}
copy(sockaddr.Addr[:], ip4)
if err = unix.Bind(fd, sockaddr); err != nil { if err = unix.Bind(fd, sockaddr); err != nil {
return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("bind %s: %w", addr.String(), err)} return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("bind %s: %w", addr.String(), err)}
} }
+96 -102
View File
@@ -53,6 +53,10 @@ func reloadEnvSettings() error {
func init() { func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
h := new(Handler) h := new(Handler)
if streamSettings, ok := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig); ok && streamSettings.SocketSettings != nil {
h.resolveStrategy = streamSettings.SocketSettings.DomainStrategy
h.usesDialerProxy = len(streamSettings.SocketSettings.DialerProxy) > 0
}
if err := core.RequireFeatures(ctx, func(pm policy.Manager) error { if err := core.RequireFeatures(ctx, func(pm policy.Manager) error {
return h.Init(config.(*Config), pm) return h.Init(config.(*Config), pm)
}); err != nil { }); err != nil {
@@ -89,9 +93,11 @@ type FinalRule struct {
// Handler handles Freedom connections. // Handler handles Freedom connections.
type Handler struct { type Handler struct {
policyManager policy.Manager policyManager policy.Manager
config *Config config *Config
finalRules []*FinalRule finalRules []*FinalRule
resolveStrategy internet.DomainStrategy
usesDialerProxy bool
} }
func buildFinalRule(config *FinalRuleConfig) (*FinalRule, error) { func buildFinalRule(config *FinalRuleConfig) (*FinalRule, error) {
@@ -168,22 +174,6 @@ func getDefaultFinalRule(inbound *session.Inbound) *FinalRule {
return nil return nil
} }
func (h *Handler) shouldResolveDomainBeforeFinalRules(dialDest net.Destination, defaultRule *FinalRule) bool {
if !dialDest.Address.Family().IsDomain() {
return false
}
if len(h.finalRules) > 0 {
rule := h.finalRules[0]
if rule.action == RuleAction_Allow && rule.network[dialDest.Network] && len(rule.port) == 0 && rule.ip == nil {
return false
}
}
if defaultRule != nil || len(h.finalRules) > 0 {
return true
}
return false
}
func (h *Handler) matchFinalRule(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) *FinalRule { func (h *Handler) matchFinalRule(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) *FinalRule {
for _, rule := range h.finalRules { for _, rule := range h.finalRules {
if rule.Apply(network, address, port) { if rule.Apply(network, address, port) {
@@ -196,17 +186,16 @@ func (h *Handler) matchFinalRule(network net.Network, address net.Address, port
return nil return nil
} }
func (h *Handler) applyFinalRules(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) RuleAction {
if rule := h.matchFinalRule(network, address, port, defaultRule); rule != nil {
return rule.action
}
return RuleAction_Allow
}
// Init initializes the Handler with necessary parameters. // Init initializes the Handler with necessary parameters.
func (h *Handler) Init(config *Config, pm policy.Manager) error { func (h *Handler) Init(config *Config, pm policy.Manager) error {
h.config = config h.config = config
h.policyManager = pm h.policyManager = pm
if h.usesDialerProxy { // freedom is not the final outbound, final rules do not apply
if len(config.FinalRules) > 0 {
errors.LogWarning(context.Background(), `The "finalRules" setting is ignored when "sockopt.dialerProxy" is set, since freedom is not the final outbound.`)
}
return nil
}
h.finalRules = make([]*FinalRule, 0, len(config.FinalRules)) h.finalRules = make([]*FinalRule, 0, len(config.FinalRules))
for _, rc := range config.FinalRules { for _, rc := range config.FinalRules {
rule, err := buildFinalRule(rc) rule, err := buildFinalRule(rc)
@@ -237,6 +226,20 @@ func (h *Handler) blockDelay(rule *FinalRule) time.Duration {
return time.Duration(min+uint64(dice.Roll(int(span+1)))) * time.Second return time.Duration(min+uint64(dice.Roll(int(span+1)))) * time.Second
} }
func (h *Handler) blackhole(ctx context.Context, input buf.Reader, output buf.Writer, rule *FinalRule, dest *net.Destination) error {
delay := h.blockDelay(rule)
errors.LogInfo(ctx, "blocked target: ", *dest, ", blackholing connection for ", delay)
timer := time.AfterFunc(delay, func() {
common.Interrupt(input)
common.Interrupt(output)
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *dest)
})
defer timer.Stop()
defer common.Close(output)
_ = buf.Copy(input, buf.Discard)
return nil
}
func isValidAddress(addr *net.IPOrDomain) bool { func isValidAddress(addr *net.IPOrDomain) bool {
if addr == nil { if addr == nil {
return false return false
@@ -256,7 +259,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ob.Name = "freedom" ob.Name = "freedom"
ob.CanSpliceCopy = 1 ob.CanSpliceCopy = 1
inbound := session.InboundFromContext(ctx) inbound := session.InboundFromContext(ctx)
defaultRule := getDefaultFinalRule(inbound) var defaultRule *FinalRule
if !h.usesDialerProxy { // freedom is not the final outbound, final rules do not apply (and the domain is not resolved)
defaultRule = getDefaultFinalRule(inbound)
}
destination := ob.Target destination := ob.Target
origTargetAddr := ob.OriginalTarget.Address origTargetAddr := ob.OriginalTarget.Address
@@ -284,61 +290,53 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
var conn stat.Connection var conn stat.Connection
var blockedDest *net.Destination var blockedDest *net.Destination
var blockedRule *FinalRule var blockedRule *FinalRule
firstResolve := true
err := retry.ExponentialBackoff(5, 100).On(func() error { err := retry.ExponentialBackoff(5, 100).On(func() error {
dialDest := destination if destination.Address.Family().IsDomain() {
if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() { if defaultRule != nil || len(h.finalRules) > 0 {
strategy := h.config.DomainStrategy if strategy := h.resolveStrategy; strategy.HasStrategy() {
if destination.Network == net.Network_UDP && origTargetAddr != nil && outGateway == nil { ips, err := internet.LookupForIP(destination.Address.Domain(), strategy, outGateway)
strategy = strategy.GetDynamicStrategy(origTargetAddr.Family()) if err != nil { // non-force may still dial with system DNS
} errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", destination.Address.Domain())
ips, err := internet.LookupForIP(dialDest.Address.Domain(), strategy, outGateway) if strategy.ForceIP() {
if err != nil { return err // retry
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain()) }
if h.config.DomainStrategy.ForceIP() || h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) { }
return err for _, ip := range ips {
if addr := net.IPAddress(ip); addr != nil {
if rule := h.matchFinalRule(destination.Network, addr, destination.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
blockedDest = &destination
blockedDest.Address = addr
blockedRule = rule
return nil
}
}
}
} else {
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, destination.Address.Domain())
if err != nil { // dialer may retry DNS
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", destination.Address.Domain())
}
for _, addr := range addrs {
if ipAddr := net.IPAddress(addr.IP); ipAddr != nil {
if rule := h.matchFinalRule(destination.Network, ipAddr, destination.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
blockedDest = &destination
blockedDest.Address = ipAddr
blockedRule = rule
return nil
}
}
}
} }
} else {
dialDest = net.Destination{
Network: dialDest.Network,
Address: net.IPAddress(ips[dice.Roll(len(ips))]),
Port: dialDest.Port,
}
errors.LogInfo(ctx, "dialing to ", dialDest)
} }
} else if h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) { // asis + domain + hasrules } else {
domain := dialDest.Address.Domain() if rule := h.matchFinalRule(destination.Network, destination.Address, destination.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
var ips []net.IP blockedDest = &destination
if firstResolve { blockedRule = rule
firstResolve = false return nil
supportIPv4, supportIPv6 := utils.CheckRoutes()
if supportIPv4 {
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip4", domain)
}
if len(ips) == 0 && supportIPv6 {
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip6", domain)
}
if len(ips) == 0 {
return errors.New("failed to get IP address for domain ", domain)
}
} else {
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip", domain)
} }
if len(ips) == 0 { // SRV/TXT, lookup failed
return errors.New("failed to get IP address for domain ", domain)
}
if addr := net.IPAddress(ips[dice.Roll(len(ips))]); addr != nil {
dialDest.Address = addr
errors.LogInfo(ctx, "dialing to ", dialDest)
}
}
if rule := h.matchFinalRule(dialDest.Network, dialDest.Address, dialDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
blockedDest = &dialDest
blockedRule = rule
return nil
} }
rawConn, err := dialer.Dial(ctx, dialDest) rawConn, err := dialer.Dial(ctx, destination)
if err != nil { if err != nil {
return err return err
} }
@@ -350,20 +348,17 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
return errors.New("failed to open connection to ", destination).Base(err) return errors.New("failed to open connection to ", destination).Base(err)
} }
if blockedDest != nil { if blockedDest != nil {
delay := h.blockDelay(blockedRule) return h.blackhole(ctx, input, output, blockedRule, blockedDest)
errors.LogInfo(ctx, "blocked target: ", *blockedDest, ", blackholing connection for ", delay)
timer := time.AfterFunc(delay, func() {
common.Interrupt(input)
common.Interrupt(output)
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *blockedDest)
})
defer timer.Stop()
defer common.Close(output)
if err := buf.Copy(input, buf.Discard); err != nil {
return nil
}
return nil
} }
if destination.Address.Family().IsDomain() && (defaultRule != nil || len(h.finalRules) > 0) {
// pre-check may fail or dialer may select another IP
remoteDest := net.DestinationFromAddr(conn.RemoteAddr())
if rule := h.matchFinalRule(remoteDest.Network, remoteDest.Address, remoteDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
conn.Close()
return h.blackhole(ctx, input, output, rule, &remoteDest)
}
}
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 { if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
version := byte(h.config.ProxyProtocol) version := byte(h.config.ProxyProtocol)
srcAddr := inbound.Source.RawNetAddr() srcAddr := inbound.Source.RawNetAddr()
@@ -408,7 +403,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
writer = buf.NewWriter(conn) writer = buf.NewWriter(conn)
} }
} else { } else {
writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination) writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination, outGateway)
if h.config.Noises != nil { if h.config.Noises != nil {
errors.LogDebug(ctx, "NOISE", h.config.Noises) errors.LogDebug(ctx, "NOISE", h.config.Noises)
writer = &NoisePacketWriter{ writer = &NoisePacketWriter{
@@ -512,7 +507,7 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
} }
udpAddr := d.(*net.UDPAddr) udpAddr := d.(*net.UDPAddr)
sourceAddr := net.IPAddress(udpAddr.IP) sourceAddr := net.IPAddress(udpAddr.IP)
if r.Handler.applyFinalRules(net.Network_UDP, sourceAddr, net.Port(udpAddr.Port), r.DefaultRule) == RuleAction_Block { if rule := r.Handler.matchFinalRule(net.Network_UDP, sourceAddr, net.Port(udpAddr.Port), r.DefaultRule); rule != nil && rule.action == RuleAction_Block {
continue continue
} }
b.Resize(0, int32(n)) b.Resize(0, int32(n))
@@ -537,7 +532,7 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
} }
// DialDest means the dial target used in the dialer when creating conn // DialDest means the dial target used in the dialer when creating conn
func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination) buf.Writer { func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination, outGateway net.Address) buf.Writer {
iConn := conn iConn := conn
statConn, ok := iConn.(*stat.CounterConnection) statConn, ok := iConn.(*stat.CounterConnection)
if ok { if ok {
@@ -561,9 +556,8 @@ func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverr
DefaultRule: defaultRule, DefaultRule: defaultRule,
UDPOverride: UDPOverride, UDPOverride: UDPOverride,
ResolvedUDPAddr: resolvedUDPAddr, ResolvedUDPAddr: resolvedUDPAddr,
LocalAddr: net.DestinationFromAddr(conn.LocalAddr()).Address, OutGateway: outGateway,
} }
} }
return &buf.SequentialWriter{Writer: conn} return &buf.SequentialWriter{Writer: conn}
} }
@@ -580,7 +574,7 @@ type PacketWriter struct {
// Resulting in these packets being sent to many different IPs randomly // Resulting in these packets being sent to many different IPs randomly
// So, cache and keep the resolve result // So, cache and keep the resolve result
ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address] ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address]
LocalAddr net.Address OutGateway net.Address
} }
func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
@@ -603,21 +597,21 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
if ip, ok := w.ResolvedUDPAddr.Load(b.UDP.Address.Domain()); ok { if ip, ok := w.ResolvedUDPAddr.Load(b.UDP.Address.Domain()); ok {
b.UDP.Address = ip b.UDP.Address = ip
} else { } else {
ShouldUseSystemResolver := true shouldUseSystemResolver := true
if w.Handler.config.DomainStrategy.HasStrategy() { if strategy := w.Handler.resolveStrategy; strategy.HasStrategy() {
ips, err := internet.LookupForIP(b.UDP.Address.Domain(), w.Handler.config.DomainStrategy, w.LocalAddr) ips, err := internet.LookupForIP(b.UDP.Address.Domain(), strategy, w.OutGateway)
if err != nil { if err != nil {
// drop packet if resolve failed when forceIP // drop packet if resolve failed when forceIP
if w.Handler.config.DomainStrategy.ForceIP() { if strategy.ForceIP() {
b.Release() b.Release()
continue continue
} }
} else { } else {
ip = net.IPAddress(ips[dice.Roll(len(ips))]) ip = net.IPAddress(ips[dice.Roll(len(ips))])
ShouldUseSystemResolver = false shouldUseSystemResolver = false
} }
} }
if ShouldUseSystemResolver { if shouldUseSystemResolver {
udpAddr, err := net.ResolveUDPAddr("udp", b.UDP.NetAddr()) udpAddr, err := net.ResolveUDPAddr("udp", b.UDP.NetAddr())
if err != nil { if err != nil {
b.Release() b.Release()
@@ -631,7 +625,7 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
} }
} }
} }
if w.applyFinalRules(net.Network_UDP, b.UDP.Address, b.UDP.Port, w.DefaultRule) == RuleAction_Block { if rule := w.matchFinalRule(net.Network_UDP, b.UDP.Address, b.UDP.Port, w.DefaultRule); rule != nil && rule.action == RuleAction_Block {
b.Release() b.Release()
continue continue
} }
+6 -3
View File
@@ -173,15 +173,18 @@ func fillRequestHeader(ctx context.Context, header []*Header) ([]*Header, error)
outbounds := session.OutboundsFromContext(ctx) outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1] ob := outbounds[len(outbounds)-1]
if inbound == nil || ob == nil { var src net.Destination
return nil, errors.New("missing inbound or outbound metadata from context") if inbound != nil {
src = inbound.Source
} else {
src = net.TCPDestination(net.AnyIP, 0)
} }
data := struct { data := struct {
Source net.Destination Source net.Destination
Target net.Destination Target net.Destination
}{ }{
Source: inbound.Source, Source: src,
Target: ob.Target, Target: ob.Target,
} }
+1 -1
View File
@@ -332,7 +332,7 @@ func readResponseAndHandle100Continue(r *bufio.Reader, req *http.Request, writer
return nil, errors.New("failed to read http 1xx response").Base(err) return nil, errors.New("failed to read http 1xx response").Base(err)
} }
ResponseHeader1xx = append(ResponseHeader1xx, data...) ResponseHeader1xx = append(ResponseHeader1xx, data...)
if bytes.Equal(ResponseHeader1xx[len(ResponseHeader1xx)-4:], []byte{'\r', '\n', '\r', '\n'}) { if len(ResponseHeader1xx) >= 4 && bytes.Equal(ResponseHeader1xx[len(ResponseHeader1xx)-4:], []byte{'\r', '\n', '\r', '\n'}) {
break break
} }
if len(ResponseHeader1xx) > 1024 { if len(ResponseHeader1xx) > 1024 {
+49
View File
@@ -0,0 +1,49 @@
package http
import (
"bufio"
"bytes"
"io"
"net/http"
"strings"
"testing"
)
// A malformed upstream response containing a bare '\n' before the real
// status line used to crash readResponseAndHandle100Continue: the first
// ReadSlice('\n') returns fewer than 4 bytes, and slicing
// ResponseHeader1xx[len(ResponseHeader1xx)-4:] panicked with a negative
// index instead of returning an error.
func TestReadResponseAndHandle100ContinueDoesNotPanicOnEarlyNewline(t *testing.T) {
payload := "X\nHTTP/1.1 100 Continue\r\n\r\n" + strings.Repeat("A", 40)
r := bufio.NewReader(bytes.NewReader([]byte(payload)))
req, err := http.NewRequest("GET", "http://example.com/", nil)
if err != nil {
t.Fatal(err)
}
// Must not panic; a parse error for the garbage trailing bytes is fine.
_, _ = readResponseAndHandle100Continue(r, req, io.Discard)
}
func TestReadResponseAndHandle100ContinueForwardsAndParsesFinalResponse(t *testing.T) {
payload := "HTTP/1.1 100 Continue\r\n\r\n" +
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
r := bufio.NewReader(bytes.NewReader([]byte(payload)))
req, err := http.NewRequest("GET", "http://example.com/", nil)
if err != nil {
t.Fatal(err)
}
var forwarded bytes.Buffer
resp, err := readResponseAndHandle100Continue(r, req, &forwarded)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode != 200 {
t.Fatalf("expected status 200, got %d", resp.StatusCode)
}
if !strings.Contains(forwarded.String(), "100 Continue") {
t.Fatalf("expected 1xx response to be forwarded, got %q", forwarded.String())
}
}
+1
View File
@@ -82,6 +82,7 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
if err != nil { if err != nil {
return errors.New("failed to connect to server").Base(err) return errors.New("failed to connect to server").Base(err)
} }
defer connection.Close()
if session.TimeoutOnlyFromContext(ctx) { if session.TimeoutOnlyFromContext(ctx) {
ctx, _ = context.WithCancel(context.Background()) ctx, _ = context.WithCancel(context.Background())
+136 -5
View File
@@ -11,6 +11,8 @@ import (
"os" "os"
"strconv" "strconv"
"sync" "sync"
"sync/atomic"
"time"
"unsafe" "unsafe"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
@@ -38,21 +40,111 @@ const (
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
) )
//go:linkname procyield runtime.procyield
func procyield(cycles uint32)
type DarwinTun struct { type DarwinTun struct {
tunFile *os.File tunFile *os.File
options *Config options *Config
tunFd int tunFd int
ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system) ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system)
// Genuinely blocks Wait() until tunFd is readable, instead of the
// previous procyield-only busy-spin (dispatchLoop in
// stack_gvisor_endpoint.go calls ReadPacket() then Wait() in a tight
// loop with no other throttling whenever the queue is empty -- with
// only procyield(1), that pins a full CPU core for as long as the
// tunnel is up, observed causing severe device heating/thermal
// shutdown). nil if kqueue setup failed, in which case Wait() falls
// back to a bounded time.Sleep instead. See waitKqueue's own doc
// comment for why this is a dedicated type rather than a bare fd.
waitKq *waitKqueue
routeMonitor *os.File routeMonitor *os.File
routeMonitorOnce sync.Once routeMonitorOnce sync.Once
systemRoutes []netip.Prefix systemRoutes []netip.Prefix
gateway netip.Prefix gateway netip.Prefix
} }
// waitKqueue owns a kqueue fd used by DarwinTun.Wait() to block on
// read-readiness. Closing and waiting can race from different goroutines
// (Close() from the caller that tears down the tunnel, Wait() from
// dispatchLoop's own goroutine) -- reviewer feedback on XTLS/Xray-core#6580
// found that a bare `int` fd field let Close() race Wait()'s use of the
// same fd number, and on Darwin a closed fd number can be reused by an
// unrelated concurrent open() before Wait() gets to call Kevent on it,
// so Wait() could end up polling (or Close() could end up closing) a
// completely unrelated file descriptor. This type makes closing
// idempotent (sync.Once) and gates every Kevent call behind an atomic
// "closed" flag checked immediately before the syscall, so Wait() never
// issues a kevent syscall against a fd number that Close() has already
// (or is concurrently) invalidated -- there's still a narrow window where
// Wait() checks-then-uses the fd, but Close() only actually closes it
// after Wait() cannot start a new syscall on it (the flag is set first,
// synchronized with acquire/release semantics), which is sufficient since
// Wait()'s Kevent call itself is what's being raced, not a fd read/write.
type waitKqueue struct {
fd int
closed atomic.Bool
once sync.Once
}
// newWaitKqueue creates a kqueue registered for read-readiness on fd, for
// Wait() to block on. Returns nil if anything fails, so callers can fall
// back to a bounded sleep rather than error out of NewTun over what is
// purely a CPU-efficiency concern.
func newWaitKqueue(fd int) *waitKqueue {
kq, err := unix.Kqueue()
if err != nil {
return nil
}
_, err = unix.Kevent(kq, []unix.Kevent_t{{
Ident: uint64(fd),
Filter: unix.EVFILT_READ,
Flags: unix.EV_ADD | unix.EV_ENABLE,
}}, nil, nil)
if err != nil {
_ = unix.Close(kq)
return nil
}
return &waitKqueue{fd: kq}
}
// wait blocks until the registered fd is readable, timeout elapses, or a
// benign interrupt occurs -- all three are "this kqueue is still healthy,
// the caller should just try again" and return true; the caller
// (DarwinTun.Wait) doesn't need to distinguish them since it always calls
// ReadPacket() right after anyway, and that already handles "nothing was
// actually there" via ErrQueueEmpty. Returns false only when the kqueue
// itself is no longer usable -- already closed, or the kevent syscall
// failed for a reason other than EINTR -- see its own call site in
// DarwinTun.Wait for why a persistent failure must not be silently
// retried forever (reviewer feedback, XTLS/Xray-core#6580 P2).
func (w *waitKqueue) wait(timeout time.Duration) (ok bool) {
if w.closed.Load() {
return false
}
events := make([]unix.Kevent_t, 1)
ts := unix.NsecToTimespec(timeout.Nanoseconds())
_, err := unix.Kevent(w.fd, nil, events, &ts)
if err != nil {
return errors.Is(err, unix.EINTR)
}
return true
}
// close marks the kqueue as unusable (so any Wait() call that hasn't yet
// entered the kevent syscall bails out instead) and closes the underlying
// fd exactly once, regardless of how many times close is called or
// whether it races a Wait() already inside its kevent syscall (that call
// either completes against the still-open fd or returns an error safely
// -- either way, no other goroutine can be handed this fd number in
// between the atomic flag flip and the actual close, since nothing else
// in this type ever creates a new kqueue with the same field).
func (w *waitKqueue) close() {
w.once.Do(func() {
w.closed.Store(true)
_ = unix.Close(w.fd)
})
}
var ( var (
_ Tun = (*DarwinTun)(nil) _ Tun = (*DarwinTun)(nil)
_ GVisorDevice = (*DarwinTun)(nil) _ GVisorDevice = (*DarwinTun)(nil)
@@ -77,6 +169,7 @@ func NewTun(options *Config) (Tun, error) {
options: options, options: options,
tunFd: fd, tunFd: fd,
ownsFd: false, ownsFd: false,
waitKq: newWaitKqueue(fd),
}, nil }, nil
} }
@@ -103,6 +196,7 @@ func NewTun(options *Config) (Tun, error) {
options: options, options: options,
tunFd: int(tunFile.Fd()), tunFd: int(tunFile.Fd()),
ownsFd: true, ownsFd: true,
waitKq: newWaitKqueue(int(tunFile.Fd())),
gateway: gateway, gateway: gateway,
}, nil }, nil
} }
@@ -134,6 +228,9 @@ func (t *DarwinTun) Close() error {
_ = t.routeMonitor.Close() _ = t.routeMonitor.Close()
} }
}) })
if t.waitKq != nil {
t.waitKq.close()
}
routeErr := t.unsetSystemRoutes() routeErr := t.unsetSystemRoutes()
if t.ownsFd { if t.ownsFd {
return xerrors.Combine(routeErr, t.tunFile.Close()) return xerrors.Combine(routeErr, t.tunFile.Close())
@@ -242,9 +339,43 @@ func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
}), nil }), nil
} }
// Wait some cpu cycles // Wait blocks until tunFd is readable (or a short timeout elapses), rather
// than spinning the CPU -- see the waitKq field's own doc comment. A bounded
// timeout (not an indefinite wait) keeps this responsive to a Close() that
// happens to race a call already parked here.
//
// Reviewer feedback (XTLS/Xray-core#6580, P2): the original version
// discarded every error from the underlying kevent syscall. dispatchLoop
// (stack_gvisor_endpoint.go) calls ReadPacket() then Wait() in an
// unconditional tight loop -- if kevent started failing at runtime for a
// persistent reason (not just a benign EINTR), Wait() returning
// immediately every time reintroduces exactly the busy-spin this whole
// change exists to remove, just routed through a failing syscall instead
// of procyield. waitKq.wait's own bool return distinguishes "genuinely
// interrupted, try again" from "this kqueue is unusable now" -- Wait()
// permanently falls back to the sleep path once that happens, rather than
// retrying the same broken kqueue forever.
func (t *DarwinTun) Wait() { func (t *DarwinTun) Wait() {
procyield(1) if t.waitKq != nil && t.waitKq.wait(time.Second) {
return
}
if t.waitKq != nil {
// Persistent kevent failure (not a benign EINTR, and not just
// "the 1s timeout elapsed with nothing to read" -- wait() already
// returned true for both of those cases above). Stop trusting
// this kqueue for the rest of this DarwinTun's lifetime instead of
// re-attempting a syscall that's already shown it won't succeed.
t.waitKq.close()
t.waitKq = nil
}
// Reviewer feedback (XTLS/Xray-core#6580): procyield here is the same
// busy-spin this whole change exists to remove, just gated behind an
// edge case (kqueue setup failing, which practically never happens on
// real Darwin systems, or having just failed permanently above)
// instead of always -- a genuine bounded sleep actually yields the CPU
// instead of being a near-instant scheduler hint that lets the tight
// dispatchLoop caller spin just as hot as before.
time.Sleep(time.Millisecond)
} }
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) { func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
+184
View File
@@ -3,7 +3,11 @@
package tun package tun
import ( import (
"sync"
"testing" "testing"
"time"
"golang.org/x/sys/unix"
) )
func TestSelectDarwinGatewayDefault(t *testing.T) { func TestSelectDarwinGatewayDefault(t *testing.T) {
@@ -47,3 +51,183 @@ func TestSelectDarwinGatewayRequiresUsableLocalAddress(t *testing.T) {
t.Fatal("expected error") t.Fatal("expected error")
} }
} }
// newTestSocketpair returns a connected AF_UNIX/SOCK_DGRAM pair -- a real
// fd DarwinTun.Wait's kqueue can register EVFILT_READ against, without
// needing an actual utun interface (which requires root/network
// entitlements this test environment doesn't have). Datagram sockets
// (unlike pipes) support both "write makes readable" and "close makes
// readable" the same way a tun fd's read-readiness behaves.
func newTestSocketpair(t *testing.T) (a, b int) {
t.Helper()
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0)
if err != nil {
t.Fatalf("socketpair: %v", err)
}
t.Cleanup(func() {
_ = unix.Close(fds[0])
_ = unix.Close(fds[1])
})
return fds[0], fds[1]
}
// Reviewer feedback, XTLS/Xray-core#6580: "blocking with no data" case --
// wait() must not return before the timeout when nothing is written.
func TestWaitKqueueBlocksWithNoData(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
defer kq.close()
start := time.Now()
ok := kq.wait(150 * time.Millisecond)
elapsed := time.Since(start)
if !ok {
t.Fatal("wait() returned false on a healthy kqueue with a plain timeout")
}
if elapsed < 100*time.Millisecond {
t.Fatalf("wait() returned after only %v, expected it to block close to the 150ms timeout", elapsed)
}
}
// Reviewer feedback: "wake up with a readable fd" case.
func TestWaitKqueueWakesOnReadable(t *testing.T) {
a, b := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
defer kq.close()
done := make(chan bool, 1)
go func() {
done <- kq.wait(5 * time.Second)
}()
time.Sleep(20 * time.Millisecond) // let wait() actually enter the syscall first
if _, err := unix.Write(b, []byte{0x1}); err != nil {
t.Fatalf("write: %v", err)
}
select {
case ok := <-done:
if !ok {
t.Fatal("wait() returned false after the fd became readable")
}
case <-time.After(2 * time.Second):
t.Fatal("wait() did not wake up within 2s of the fd becoming readable")
}
}
// Reviewer feedback: "timeout" case, explicitly (distinct from the
// no-data test above, which also checks blocking duration -- this one
// only checks the return value).
func TestWaitKqueueTimesOut(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
defer kq.close()
if !kq.wait(50 * time.Millisecond) {
t.Fatal("wait() returned false on a plain timeout with no error condition")
}
}
// Reviewer feedback: "Close() wakes a blocked wait" case, and the
// no-double-close/no-fd-reuse concern (P1) -- close() while wait() is
// parked in its syscall must not panic, must not leave wait() hung, and a
// second close() call (from a caller that, say, calls Close() twice on
// the same DarwinTun) must be safe.
func TestWaitKqueueCloseDuringWaitIsSafe(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
started := make(chan struct{})
done := make(chan bool, 1)
go func() {
close(started)
done <- kq.wait(5 * time.Second)
}()
<-started
time.Sleep(20 * time.Millisecond) // let wait() actually enter the syscall first
kq.close()
kq.close() // double-close must be idempotent, not panic or double-free the fd
select {
case <-done:
// Either true (the close-of-the-underlying-fd unblocked kevent, a
// real kqueue behavior) or false (wait() observed the closed flag
// first) is acceptable -- what matters is that it returned at all,
// promptly, without hanging or crashing.
case <-time.After(2 * time.Second):
t.Fatal("wait() did not return within 2s of close() being called")
}
// A wait() call *after* close() must return false immediately (the
// closed-flag fast path), not attempt a syscall against the
// already-closed (and potentially since-reused, on a real system) fd
// number.
if kq.wait(time.Second) {
t.Fatal("wait() returned true after close() -- should short-circuit via the closed flag")
}
}
// Reviewer feedback: "multiple/concurrent close guard" case -- many
// goroutines calling close() concurrently must close the underlying fd
// exactly once.
func TestWaitKqueueConcurrentCloseIsSafe(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
var wg sync.WaitGroup
for range 20 {
wg.Add(1)
go func() {
defer wg.Done()
kq.close()
}()
}
wg.Wait()
if !kq.closed.Load() {
t.Fatal("closed flag not set after concurrent close() calls")
}
}
// Reviewer feedback: "kevent runtime failure without spinning" case (P2).
// Simulates a kqueue that has gone bad (closed out from under it, as if a
// concurrent/erroneous close happened) and confirms wait() reports it as
// unusable (false) rather than silently returning true forever, which is
// what DarwinTun.Wait relies on to permanently fall back to the sleep
// path instead of re-entering a failing syscall on every dispatchLoop
// iteration.
func TestWaitKqueueReportsPersistentFailure(t *testing.T) {
a, _ := newTestSocketpair(t)
kq := newWaitKqueue(a)
if kq == nil {
t.Fatal("newWaitKqueue returned nil")
}
// Close the underlying kqueue fd directly (bypassing kq.close(), which
// would also set the closed flag) to simulate the fd going bad for a
// reason other than this type's own close() -- e.g. some other code
// path in the process closing it, or the kernel invalidating it.
_ = unix.Close(kq.fd)
for i := 0; i < 5; i++ {
if kq.wait(50 * time.Millisecond) {
t.Fatalf("wait() call %d returned true against a closed underlying fd -- should report failure, not spin", i)
}
}
}
+729 -25
View File
@@ -3,28 +3,60 @@
package tun package tun
import ( import (
"context"
"errors" "errors"
"net" "net"
_ "unsafe" "net/netip"
"os"
"slices"
"sync"
"unsafe"
"golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/tun"
"gvisor.dev/gvisor/pkg/buffer" "gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/tcpip/stack"
"golang.org/x/net/route"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
xerrors "github.com/xtls/xray-core/common/errors"
) )
const tunHeaderSize = 4 const (
tunHeaderSize = 4
defaultFreeBSDGateway = "169.254.10.1/30"
// escapeFib is the routing table outbound sockets are switched to so
// their traffic bypasses the TUN routes installed in the default FIB
// (FreeBSD's substitute for the per-socket interface binding other
// platforms use). Requires the boot tunable net.fibs >= 2.
escapeFib = 1
)
//go:linkname procyield runtime.procyield //go:linkname procyield runtime.procyield
func procyield(cycles uint32) func procyield(cycles uint32)
type FreeBSDTun struct { type FreeBSDTun struct {
device tun.Device device tun.Device
mtu uint32 options *Config
tunIndex int
autoInterface bool
systemRoutes []netip.Prefix
escapeMu sync.Mutex
escapeRoutes []escapeRoute
routeMonitor *os.File
routeMonitorOnce sync.Once
}
// escapeRoute remembers one route written into the escape FIB, in the exact
// shape needed to delete it again. A zero gateway means an interface route.
type escapeRoute struct {
prefix netip.Prefix
ifIndex int
gateway netip.Addr
} }
var ( var (
@@ -34,20 +66,172 @@ var (
// NewTun builds new tun interface handler // NewTun builds new tun interface handler
func NewTun(options *Config) (Tun, error) { func NewTun(options *Config) (Tun, error) {
gateway, local, err := selectFreeBSDGateway(options.Gateway)
if err != nil {
return nil, err
}
// net.fibs is a boot-time constant, so validate the escape routing table
// before the shared handler registers a dialer controller that would
// otherwise steer every outbound socket into a table that was never set up.
if options.AutoOutboundsInterface != "" {
if err := checkEscapeFib(); err != nil {
return nil, err
}
}
tunDev, err := tun.CreateTUN(options.Name, int(options.MTU)) tunDev, err := tun.CreateTUN(options.Name, int(options.MTU))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &FreeBSDTun{device: tunDev, mtu: options.MTU}, nil name, err := tunDev.Name()
if err != nil {
_ = tunDev.Close()
return nil, err
}
// From here the interface exists in the kernel; the wireguard library does
// not remove it on Close, so every failure path must destroy it too or the
// next start fails with "interface already exists".
iface, err := net.InterfaceByName(name)
if err != nil {
_ = tunDev.Close()
destroyInterface(name)
return nil, err
}
if err := setIPAddress(name, gateway, local, iface.Index); err != nil {
_ = tunDev.Close()
destroyInterface(name)
return nil, err
}
return &FreeBSDTun{
device: tunDev,
options: options,
tunIndex: iface.Index,
autoInterface: options.AutoOutboundsInterface != "",
}, nil
}
// selectFreeBSDGateway picks the first IPv4 prefix from the configured gateway
// list and the local address derived from it (the darwin semantics: the
// gateway is the remote side of the point-to-point pair, the local address is
// the next one after it), falling back to the same link-local default.
func selectFreeBSDGateway(configured []string) (netip.Prefix, netip.Addr, error) {
gateway := netip.MustParsePrefix(defaultFreeBSDGateway)
if len(configured) > 0 {
found := false
for _, value := range configured {
prefix, err := netip.ParsePrefix(value)
if err != nil {
return netip.Prefix{}, netip.Addr{}, xerrors.New("invalid FreeBSD gateway ", value).Base(err)
}
if prefix.Addr().Is4() {
gateway, found = prefix, true
break
}
}
if !found {
return netip.Prefix{}, netip.Addr{}, xerrors.New("FreeBSD gateway requires at least one IPv4 prefix")
}
}
local, ok := nextLocalIPv4(gateway)
if !ok || !gateway.Contains(local) {
return netip.Prefix{}, netip.Addr{}, xerrors.New("FreeBSD gateway ", gateway.String(), " must contain at least one usable local IPv4 address after the gateway address")
}
return gateway, local, nil
}
func nextLocalIPv4(gateway netip.Prefix) (netip.Addr, bool) {
local4 := gateway.Addr().As4()
for i := len(local4) - 1; i >= 0; i-- {
local4[i]++
if local4[i] != 0 {
return netip.AddrFrom4(local4), true
}
}
return netip.Addr{}, false
} }
func (t *FreeBSDTun) Start() error { func (t *FreeBSDTun) Start() error {
if err := t.setSystemRoutes(); err != nil {
return err
}
// Gate on this instance's own option, not the package-global updater,
// which a previously-removed inbound may have left set. checkEscapeFib
// already ran in NewTun, before the dialer controller was registered.
if t.autoInterface {
if err := t.syncEscapeFib(); err != nil {
_ = t.unsetSystemRoutes()
return err
}
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
t.unsetEscapeFib()
_ = t.unsetSystemRoutes()
return err
}
t.routeMonitor = os.NewFile(uintptr(fd), "xray-route-monitor")
go t.monitorRouteChanges()
}
return nil return nil
} }
// monitorRouteChanges refreshes the outbound interface and the escape FIB
// mirror whenever the system routing table changes.
func (t *FreeBSDTun) monitorRouteChanges() {
buffer := make([]byte, 64*1024)
for {
if _, err := t.routeMonitor.Read(buffer); err != nil {
if !errors.Is(err, os.ErrClosed) {
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to monitor route changes")
}
return
}
if updater != nil {
updater.Update()
}
if err := t.syncEscapeFib(); err != nil {
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to refresh escape routes")
}
}
}
func (t *FreeBSDTun) Close() error { func (t *FreeBSDTun) Close() error {
return t.device.Close() t.routeMonitorOnce.Do(func() {
if t.routeMonitor != nil {
_ = t.routeMonitor.Close()
}
})
t.unsetEscapeFib()
routeErr := t.unsetSystemRoutes()
name, nameErr := t.Name()
closeErr := t.device.Close()
// The wireguard tun device does not tear the interface down on FreeBSD,
// so an unclean shutdown would leave utun<n> behind and the next start
// would fail with "interface already exists"; destroy it explicitly.
if nameErr == nil {
destroyInterface(name)
}
return xerrors.Combine(routeErr, closeErr)
}
func destroyInterface(name string) {
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
return
}
defer unix.Close(fd)
// struct ifreq: 16-byte name + a 16-byte union (SIOCIFDESTROY's encoded
// length is 32 bytes on amd64, and the kernel copies in all of it).
var req struct {
Name [unix.IFNAMSIZ]byte
_ [16]byte
}
copy(req.Name[:], name)
_ = ioctlPtr(fd, unix.SIOCIFDESTROY, unsafe.Pointer(&req))
} }
func (t *FreeBSDTun) Name() (string, error) { func (t *FreeBSDTun) Name() (string, error) {
@@ -55,21 +239,13 @@ func (t *FreeBSDTun) Name() (string, error) {
} }
func (t *FreeBSDTun) Index() (int, error) { func (t *FreeBSDTun) Index() (int, error) {
name, err := t.Name() return t.tunIndex, nil
if err != nil {
return 0, err
}
iface, err := net.InterfaceByName(name)
if err != nil {
return 0, err
}
return iface.Index, nil
} }
// WritePacket implements GVisorDevice method to write one packet to the tun device // WritePacket implements GVisorDevice method to write one packet to the tun device
func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error { func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
// request memory to write from reusable buffer pool // request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize) b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
defer b.Release() defer b.Release()
// prepare Unix specific packet header // prepare Unix specific packet header
@@ -104,7 +280,7 @@ func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
// which will make the stack call Wait which should implement desired push-back // which will make the stack call Wait which should implement desired push-back
func (t *FreeBSDTun) ReadPacket() (byte, *stack.PacketBuffer, error) { func (t *FreeBSDTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
// request memory to write from reusable buffer pool // request memory to write from reusable buffer pool
b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize) b := buf.NewWithSize(int32(t.options.MTU) + tunHeaderSize)
// read the bytes to the interface file // read the bytes to the interface file
n, err := b.ReadFrom(t.device.File()) n, err := b.ReadFrom(t.device.File())
@@ -141,23 +317,551 @@ func (t *FreeBSDTun) Wait() {
} }
func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) { func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) {
return &LinkEndpoint{deviceMTU: t.mtu, device: t}, nil return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
} }
func setinterface(network, address string, fd uintptr, iface *net.Interface) error { const (
IN6_IFF_NODAD = 0x0020 // netinet6/in6_var.h
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
)
// ifAliasReq4 is struct in_aliasreq from netinet/in_var.h in the 64-byte
// layout unix.SIOCAIFADDR encodes (name + addr/dstaddr/mask sockaddrs).
type ifAliasReq4 struct {
Name [unix.IFNAMSIZ]byte
Addr unix.RawSockaddrInet4
Dstaddr unix.RawSockaddrInet4
Mask unix.RawSockaddrInet4
}
// ifAliasReq6 is struct in6_aliasreq from netinet6/in6_var.h. The trailing
// Vhid field matters: unix.SIOCAIFADDR_IN6 is not in x/sys/unix, so
// siocaifaddrIn6 is derived from this struct's size, and the kernel only
// accepts the ioctl whose encoded length matches the real struct.
type ifAliasReq6 struct {
Name [unix.IFNAMSIZ]byte
Addr unix.RawSockaddrInet6
Dstaddr unix.RawSockaddrInet6
Prefixmask unix.RawSockaddrInet6
Flags int32
Lifetime addrLifetime6
Vhid int32
}
// addrLifetime6 is struct in6_addrlifetime (time_t is int64 on freebsd/amd64).
type addrLifetime6 struct {
Expire int64
Preferred int64
Vltime uint32
Pltime uint32
}
// SIOCAIFADDR_IN6 = _IOW('i', 27, struct in6_aliasreq); x/sys/unix does not
// carry the netinet6 ioctls, so encode it from the struct size like the
// header macro does.
const siocaifaddrIn6 = 0x80000000 | (uintptr(unsafe.Sizeof(ifAliasReq6{})) << 16) | ('i' << 8) | 27
// setIPAddress assigns the local/remote point-to-point IPv4 pair and a
// link-local IPv6 address to the interface, required for the routing to work
// (same scheme as the darwin implementation: local address is the one right
// after the gateway address).
func setIPAddress(name string, gateway netip.Prefix, local netip.Addr, ifIndex int) error {
socket4, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
return err
}
defer unix.Close(socket4)
local4 := local.As4()
ifReq4 := ifAliasReq4{
Addr: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: local4,
},
Dstaddr: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: gateway.Addr().As4(),
},
Mask: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: prefixMask4(gateway.Bits()),
},
}
copy(ifReq4.Name[:], name)
if err = ioctlPtr(socket4, unix.SIOCAIFADDR, unsafe.Pointer(&ifReq4)); err != nil {
return os.NewSyscallError("SIOCAIFADDR", err)
}
socket6, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, 0)
if err != nil {
return err
}
defer unix.Close(socket6)
// link-local ipv6 address with suffix from ipv4, enough for v6 interface
// routes to be attachable (darwin parity); a link-local address needs its
// scope, which for FreeBSD ioctls is the interface index
local6 := netip.AddrFrom16([16]byte{0: 0xfe, 1: 0x80, 12: local4[0], 13: local4[1], 14: local4[2], 15: local4[3]})
ifReq6 := ifAliasReq6{
Addr: unix.RawSockaddrInet6{
Len: unix.SizeofSockaddrInet6,
Family: unix.AF_INET6,
Addr: local6.As16(),
Scope_id: uint32(ifIndex),
},
Prefixmask: unix.RawSockaddrInet6{
Len: unix.SizeofSockaddrInet6,
Family: unix.AF_INET6,
Addr: prefixMask6(64),
},
Flags: IN6_IFF_NODAD,
Lifetime: addrLifetime6{
Vltime: ND6_INFINITE_LIFETIME,
Pltime: ND6_INFINITE_LIFETIME,
},
}
copy(ifReq6.Name[:], name)
if err = ioctlPtr(socket6, uint(siocaifaddrIn6), unsafe.Pointer(&ifReq6)); err != nil {
// non-fatal: FreeBSD auto-configures a link-local address on UP
// interfaces, which is all the v6 interface routes need
xerrors.LogInfoInner(context.Background(), os.NewSyscallError("SIOCAIFADDR_IN6", err), "[tun] failed to assign the IPv6 link-local address")
}
return nil return nil
} }
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
if fixedName == "" { _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
return nil, errors.New("automatic outbound interface selection is not supported on this platform") if errno != 0 {
return errno
} }
iface, err := net.InterfaceByName(fixedName) return nil
}
func prefixMask4(bits int) [4]byte {
var mask [4]byte
copy(mask[:], net.CIDRMask(bits, 32))
return mask
}
func prefixMask6(bits int) [16]byte {
var mask [16]byte
copy(mask[:], net.CIDRMask(bits, 128))
return mask
}
// setinterface is the per-socket half of autoOutboundsInterface. FreeBSD has
// no SO_BINDTODEVICE/IP_BOUND_IF equivalent, so the socket is pointed at the
// escape FIB instead, where Start() mirrors the physical default route; the
// iface argument is resolved by the shared updater but unused here (the escape
// is table-based, not a per-socket interface bind). checkEscapeFib in NewTun
// guarantees the FIB exists before this can run.
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
return unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SETFIB, escapeFib)
}
func (t *FreeBSDTun) setSystemRoutes() error {
routes, err := buildSystemRoutes(t.options.AutoSystemRoutingTable)
if err != nil {
return err
}
// Route through the interface, not a gateway: the tun(4) device is a
// broadcast interface here, so its point-to-point peer address doubles as
// the subnet broadcast and the kernel refuses to route to it (EACCES).
// Interface routes sidestep the gateway entirely (what wg-quick does on
// FreeBSD).
for _, destination := range routes {
if err := execRoute(-1, unix.RTM_ADD, t.tunIndex, destination, netip.Addr{}); err != nil {
_ = t.unsetSystemRoutes()
return xerrors.New("failed to add system route ", destination).Base(err)
}
t.systemRoutes = append(t.systemRoutes, destination)
}
return nil
}
func (t *FreeBSDTun) unsetSystemRoutes() error {
var errs []error
for i := len(t.systemRoutes) - 1; i >= 0; i-- {
destination := t.systemRoutes[i]
if err := execRoute(-1, unix.RTM_DELETE, t.tunIndex, destination, netip.Addr{}); err != nil && !errors.Is(err, unix.ESRCH) {
errs = append(errs, xerrors.New("failed to delete system route ", destination).Base(err))
}
}
t.systemRoutes = nil
return xerrors.Combine(errs...)
}
func buildSystemRoutes(configured []string) ([]netip.Prefix, error) {
routes := make([]netip.Prefix, 0, len(configured))
seen := make(map[netip.Prefix]struct{})
appendRoute := func(prefix netip.Prefix) {
prefix = prefix.Masked()
if _, found := seen[prefix]; found {
return
}
seen[prefix] = struct{}{}
routes = append(routes, prefix)
}
for _, value := range configured {
prefix, err := netip.ParsePrefix(value)
if err != nil {
return nil, xerrors.New("invalid system route ", value).Base(err)
}
if prefix.Bits() == 0 {
for _, protected := range protectedDefaultRoutes(prefix.Addr().Is4()) {
appendRoute(protected)
}
continue
}
appendRoute(prefix)
}
return routes, nil
}
// protectedDefaultRoutes splits a full default route into eight more-specific
// prefixes covering everything but the zero /8, so the system's real default
// route stays in place for outbound interface discovery (darwin parity).
func protectedDefaultRoutes(ipv4 bool) []netip.Prefix {
routes := make([]netip.Prefix, 0, 8)
for i := 0; i < 8; i++ {
if ipv4 {
var address [4]byte
address[0] = 1 << i
routes = append(routes, netip.PrefixFrom(netip.AddrFrom4(address), 8-i))
} else {
var address [16]byte
address[0] = 1 << i
routes = append(routes, netip.PrefixFrom(netip.AddrFrom16(address), 8-i))
}
}
return routes
}
// execRoute writes one RTM message to a routing socket. fib >= 0 targets that
// routing table via SO_SETFIB on the routing socket (what route(8) -fib
// does); fib -1 leaves the process default table. An invalid gateway produces
// an interface route pinned to interfaceIndex instead of a gateway route.
func execRoute(fib int, messageType int, interfaceIndex int, destination netip.Prefix, gateway netip.Addr) error {
message := route.RouteMessage{
Type: messageType,
Version: unix.RTM_VERSION,
Flags: unix.RTF_STATIC | unix.RTF_GATEWAY,
Seq: 1,
}
if messageType == unix.RTM_ADD {
message.Flags |= unix.RTF_UP
}
var gatewayAddr route.Addr
switch {
case !gateway.IsValid():
message.Flags &^= unix.RTF_GATEWAY
message.Index = interfaceIndex
gatewayAddr = &route.LinkAddr{Index: interfaceIndex}
case gateway.Is4():
gatewayAddr = &route.Inet4Addr{IP: gateway.As4()}
default:
gatewayAddr = &route.Inet6Addr{IP: gateway.As16()}
}
if destination.Addr().Is4() {
message.Addrs = []route.Addr{
unix.RTAX_DST: &route.Inet4Addr{IP: destination.Addr().As4()},
unix.RTAX_NETMASK: &route.Inet4Addr{IP: prefixMask4(destination.Bits())},
unix.RTAX_GATEWAY: gatewayAddr,
}
} else {
message.Addrs = []route.Addr{
unix.RTAX_DST: &route.Inet6Addr{IP: destination.Addr().As16()},
unix.RTAX_NETMASK: &route.Inet6Addr{IP: prefixMask6(destination.Bits())},
unix.RTAX_GATEWAY: gatewayAddr,
}
}
request, err := message.Marshal()
if err != nil {
return err
}
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
return err
}
defer unix.Close(fd)
if fib >= 0 {
if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_SETFIB, fib); err != nil {
return err
}
}
_, err = unix.Write(fd, request)
return err
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
if fixedName != "" {
iface, err := net.InterfaceByName(fixedName)
if err != nil {
return nil, err
}
if iface.Index == tunIndex {
return nil, errors.New("outbound interface cannot be the TUN interface")
}
return iface, nil
}
physical, err := physicalDefaultRoutes(tunIndex, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if iface.Index == tunIndex { for _, family := range []int{unix.AF_INET, unix.AF_INET6} {
return nil, errors.New("outbound interface cannot be the TUN interface") for _, route := range physical {
if route.family == family {
return route.iface, nil
}
}
}
return nil, errors.New("default route not found")
}
// physicalRoute describes one physical default route: the interface it
// leaves through, its gateway, and the connected prefix that makes the
// gateway resolvable.
type physicalRoute struct {
family int
iface *net.Interface
gateway netip.Addr
connected netip.Prefix
}
// physicalDefaultRoutes scans the default routing table for default routes
// that do not go through the TUN interface, at most one per address family
// (the first usable one wins, matching the darwin implementation's
// preference order). A non-zero onlyIndex restricts the scan to that
// interface, for the fixed-name mode of autoOutboundsInterface.
func physicalDefaultRoutes(tunIndex int, onlyIndex int) ([]physicalRoute, error) {
rib, err := route.FetchRIB(unix.AF_UNSPEC, route.RIBTypeRoute, 0)
if err != nil {
return nil, err
}
messages, err := route.ParseRIB(route.RIBTypeRoute, rib)
if err != nil {
return nil, err
}
found := make([]physicalRoute, 0, 2)
seen := make(map[int]bool)
for _, message := range messages {
routeMessage, ok := message.(*route.RouteMessage)
if !ok || routeMessage.Index == tunIndex {
continue
}
if onlyIndex != 0 && routeMessage.Index != onlyIndex {
continue
}
if routeMessage.Flags&unix.RTF_UP == 0 || routeMessage.Flags&unix.RTF_GATEWAY == 0 {
continue
}
family, ok := defaultRouteFamily(routeMessage)
if !ok || seen[family] {
continue
}
iface, err := usableInterface(routeMessage.Index)
if err != nil {
continue
}
gatewayAddr, ok := routeAddrToNetip(routeMessage.Addrs[unix.RTAX_GATEWAY])
if !ok {
continue
}
connected, err := connectedPrefix(iface, gatewayAddr)
if err != nil {
continue
}
seen[family] = true
found = append(found, physicalRoute{
family: family,
iface: iface,
gateway: gatewayAddr,
connected: connected,
})
}
if len(found) == 0 {
return nil, errors.New("default route not found")
}
return found, nil
}
// defaultRouteFamily reports the address family of a RIB message that
// represents a true default route (unspecified destination, zero mask).
func defaultRouteFamily(message *route.RouteMessage) (int, bool) {
if len(message.Addrs) <= unix.RTAX_NETMASK {
return 0, false
}
switch destination := message.Addrs[unix.RTAX_DST].(type) {
case *route.Inet4Addr:
mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet4Addr)
if !ok || destination.IP != netip.IPv4Unspecified().As4() {
return 0, false
}
ones, bits := net.IPMask(mask.IP[:]).Size()
return unix.AF_INET, ones == 0 && bits == 32
case *route.Inet6Addr:
mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet6Addr)
if !ok || destination.IP != netip.IPv6Unspecified().As16() {
return 0, false
}
ones, bits := net.IPMask(mask.IP[:]).Size()
return unix.AF_INET6, ones == 0 && bits == 128
default:
return 0, false
}
}
func usableInterface(index int) (*net.Interface, error) {
iface, err := net.InterfaceByIndex(index)
if err != nil {
return nil, err
}
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
return nil, errors.New("default route interface is not usable")
} }
return iface, nil return iface, nil
} }
func routeAddrToNetip(addr route.Addr) (netip.Addr, bool) {
switch typed := addr.(type) {
case *route.Inet4Addr:
return netip.AddrFrom4(typed.IP), true
case *route.Inet6Addr:
return netip.AddrFrom16(typed.IP), true
default:
return netip.Addr{}, false
}
}
// checkEscapeFib verifies the system can host the escape routing table at
// all: FIBs are a boot-time resource on FreeBSD.
func checkEscapeFib() error {
fibs, err := unix.SysctlUint32("net.fibs")
if err != nil {
return xerrors.New("failed to read net.fibs").Base(err)
}
if fibs < 2 {
return errors.New("automatic outbound interface on FreeBSD needs a second routing table: add net.fibs=2 to /boot/loader.conf and reboot")
}
current, err := unix.SysctlUint32("net.my_fibnum")
if err != nil {
return xerrors.New("failed to read net.my_fibnum").Base(err)
}
if current == escapeFib {
return errors.New("xray runs inside routing table 1, which is reserved as the escape table; start it in another FIB")
}
return nil
}
// syncEscapeFib mirrors the physical default routes (and the connected
// prefixes their gateways resolve through) into the escape FIB, replacing
// whatever mirror a previous call installed. On discovery failure the old
// mirror is kept, since a stale escape route beats none during a transient
// route flap.
func (t *FreeBSDTun) syncEscapeFib() error {
var onlyIndex int
if t.options.AutoOutboundsInterface != "" && updater != nil {
if iface := updater.Get(); iface != nil {
onlyIndex = iface.Index
}
}
physical, err := physicalDefaultRoutes(t.tunIndex, onlyIndex)
if err != nil {
return err
}
desired := make([]escapeRoute, 0, 2*len(physical))
for _, p := range physical {
desired = append(desired,
escapeRoute{prefix: p.connected, ifIndex: p.iface.Index},
escapeRoute{prefix: defaultPrefix(p.family), ifIndex: p.iface.Index, gateway: p.gateway},
)
}
t.escapeMu.Lock()
defer t.escapeMu.Unlock()
// The route monitor hears our own escape FIB writes too; rewriting an
// unchanged mirror on every wake-up would ping-pong forever.
if slices.Equal(t.escapeRoutes, desired) {
return nil
}
t.unsetEscapeFibLocked()
for _, entry := range desired {
err := execRoute(escapeFib, unix.RTM_ADD, entry.ifIndex, entry.prefix, entry.gateway)
if err != nil && !errors.Is(err, unix.EEXIST) {
return xerrors.New("failed to add escape route ", entry.prefix).Base(err)
}
t.escapeRoutes = append(t.escapeRoutes, entry)
}
return nil
}
func (t *FreeBSDTun) unsetEscapeFib() {
t.escapeMu.Lock()
defer t.escapeMu.Unlock()
t.unsetEscapeFibLocked()
}
func (t *FreeBSDTun) unsetEscapeFibLocked() {
for i := len(t.escapeRoutes) - 1; i >= 0; i-- {
entry := t.escapeRoutes[i]
err := execRoute(escapeFib, unix.RTM_DELETE, entry.ifIndex, entry.prefix, entry.gateway)
if err != nil && !errors.Is(err, unix.ESRCH) {
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to delete escape route ", entry.prefix)
}
}
t.escapeRoutes = nil
}
func defaultPrefix(family int) netip.Prefix {
if family == unix.AF_INET {
return netip.PrefixFrom(netip.IPv4Unspecified(), 0)
}
return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
}
// connectedPrefix finds the interface's address prefix containing the
// gateway, which the escape FIB needs as an interface route so the mirrored
// default route's gateway is resolvable there.
func connectedPrefix(iface *net.Interface, gateway netip.Addr) (netip.Prefix, error) {
addrs, err := iface.Addrs()
if err != nil {
return netip.Prefix{}, err
}
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok {
continue
}
ip, ok := netip.AddrFromSlice(ipNet.IP)
if !ok {
continue
}
ip = ip.Unmap()
ones, _ := ipNet.Mask.Size()
prefix := netip.PrefixFrom(ip, ones).Masked()
if prefix.Contains(gateway.WithZone("").Unmap()) {
return prefix, nil
}
}
return netip.Prefix{}, errors.New("no connected prefix contains the gateway")
}
+150
View File
@@ -0,0 +1,150 @@
//go:build freebsd
package tun
import (
"net/netip"
"testing"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
)
func TestSelectFreeBSDGatewayDefault(t *testing.T) {
gateway, local, err := selectFreeBSDGateway(nil)
if err != nil {
t.Fatal(err)
}
if gateway != netip.MustParsePrefix(defaultFreeBSDGateway) {
t.Fatal("expected default gateway, got ", gateway)
}
if local != netip.MustParseAddr("169.254.10.2") {
t.Fatal("wrong local address: ", local)
}
}
func TestSelectFreeBSDGatewayPicksFirstIPv4(t *testing.T) {
gateway, local, err := selectFreeBSDGateway([]string{"fd00::1/64", "10.0.0.1/30", "10.9.9.9/24"})
if err != nil {
t.Fatal(err)
}
if gateway != netip.MustParsePrefix("10.0.0.1/30") {
t.Fatal("wrong gateway: ", gateway)
}
if local != netip.MustParseAddr("10.0.0.2") {
t.Fatal("wrong local address: ", local)
}
}
func TestSelectFreeBSDGatewayRequiresIPv4(t *testing.T) {
if _, _, err := selectFreeBSDGateway([]string{"fd00::1/64"}); err == nil {
t.Fatal("expected error when no IPv4 gateway is configured")
}
}
func TestSelectFreeBSDGatewayRejectsGarbage(t *testing.T) {
if _, _, err := selectFreeBSDGateway([]string{"not-a-prefix"}); err == nil {
t.Fatal("expected error for invalid gateway")
}
}
func TestSelectFreeBSDGatewayRejectsFullPrefix(t *testing.T) {
// 10.0.0.255/30: the "next" local address falls outside the prefix
if _, _, err := selectFreeBSDGateway([]string{"10.0.0.255/30"}); err == nil {
t.Fatal("expected error when no usable local address follows the gateway")
}
}
func TestNextLocalIPv4(t *testing.T) {
local, ok := nextLocalIPv4(netip.MustParsePrefix("169.254.10.1/30"))
if !ok || local != netip.MustParseAddr("169.254.10.2") {
t.Fatal("wrong local address: ", local)
}
}
func TestBuildSystemRoutesSplitsDefault(t *testing.T) {
routes, err := buildSystemRoutes([]string{"0.0.0.0/0"})
if err != nil {
t.Fatal(err)
}
expected := []string{
"1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5",
"16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1",
}
if len(routes) != len(expected) {
t.Fatal("expected ", len(expected), " routes, got ", routes)
}
for i, want := range expected {
if routes[i] != netip.MustParsePrefix(want) {
t.Fatal("route ", i, ": expected ", want, ", got ", routes[i])
}
}
}
func TestBuildSystemRoutesSplitsDefaultIPv6(t *testing.T) {
routes, err := buildSystemRoutes([]string{"::/0"})
if err != nil {
t.Fatal(err)
}
if len(routes) != 8 || routes[7] != netip.MustParsePrefix("8000::/1") {
t.Fatal("unexpected v6 split: ", routes)
}
}
func TestBuildSystemRoutesDeduplicates(t *testing.T) {
routes, err := buildSystemRoutes([]string{"10.0.0.0/8", "10.1.2.3/8", "0.0.0.0/0", "0.0.0.0/0"})
if err != nil {
t.Fatal(err)
}
if len(routes) != 9 { // 10.0.0.0/8 once + 8 splits once
t.Fatal("expected 9 routes, got ", routes)
}
}
func TestBuildSystemRoutesRejectsGarbage(t *testing.T) {
if _, err := buildSystemRoutes([]string{"10.0.0.0/33"}); err == nil {
t.Fatal("expected error for invalid route")
}
}
func routeMessage(dst, mask route.Addr) *route.RouteMessage {
addrs := make([]route.Addr, unix.RTAX_NETMASK+1)
addrs[unix.RTAX_DST] = dst
addrs[unix.RTAX_NETMASK] = mask
return &route.RouteMessage{Addrs: addrs}
}
func TestDefaultRouteFamilyMatchesIPv4Default(t *testing.T) {
family, ok := defaultRouteFamily(routeMessage(&route.Inet4Addr{}, &route.Inet4Addr{}))
if !ok || family != unix.AF_INET {
t.Fatal("expected IPv4 default route match")
}
}
func TestDefaultRouteFamilyMatchesIPv6Default(t *testing.T) {
family, ok := defaultRouteFamily(routeMessage(&route.Inet6Addr{}, &route.Inet6Addr{}))
if !ok || family != unix.AF_INET6 {
t.Fatal("expected IPv6 default route match")
}
}
func TestDefaultRouteFamilyRejectsNonDefault(t *testing.T) {
if _, ok := defaultRouteFamily(routeMessage(
&route.Inet4Addr{IP: [4]byte{10, 0, 0, 0}},
&route.Inet4Addr{IP: [4]byte{255, 0, 0, 0}},
)); ok {
t.Fatal("non-default destination must not match")
}
if _, ok := defaultRouteFamily(routeMessage(
&route.Inet4Addr{},
&route.Inet4Addr{IP: [4]byte{255, 0, 0, 0}},
)); ok {
t.Fatal("non-zero mask must not match")
}
}
func TestDefaultRouteFamilyRejectsShortAddrs(t *testing.T) {
if _, ok := defaultRouteFamily(&route.RouteMessage{}); ok {
t.Fatal("message without addresses must not match")
}
}
+145 -137
View File
@@ -3,14 +3,14 @@
package tun package tun
import ( import (
"context"
"crypto/md5" "crypto/md5"
"encoding/binary" "encoding/binary"
go_errors "errors" go_errors "errors"
"net" "net"
"net/netip" "net/netip"
"sort"
"strings"
"sync" "sync"
"time"
"unsafe" "unsafe"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
@@ -31,13 +31,14 @@ func procyield(cycles uint32)
type WindowsTun struct { type WindowsTun struct {
sync.RWMutex sync.RWMutex
options *Config options *Config
adapter *wintun.Adapter adapter *wintun.Adapter
session wintun.Session session wintun.Session
readWait windows.Handle readWait windows.Handle
luid winipcfg.LUID luid winipcfg.LUID
changeCallback winipcfg.ChangeCallback cbr winipcfg.ChangeCallback
closed bool cbi winipcfg.ChangeCallback
closed bool
} }
// WindowsTun implements Tun // WindowsTun implements Tun
@@ -85,23 +86,37 @@ func open(name, desc string) (*wintun.Adapter, error) {
return nil, err return nil, err
} }
func (t *WindowsTun) Start() error { func (t *WindowsTun) Start() (err error) {
var has4, has6 bool var address4, address6 bool
allowedIPs := make([]netip.Prefix, 0, len(t.options.AutoSystemRoutingTable)) addresses := make([]netip.Prefix, 0, len(t.options.Gateway))
for _, route := range t.options.AutoSystemRoutingTable { for _, cidr := range t.options.Gateway {
allowedIPs = append(allowedIPs, netip.MustParsePrefix(route)) prefix := netip.MustParsePrefix(cidr)
if prefix.Addr().Is4() {
address4 = true
} else {
address6 = true
}
addresses = append(addresses, prefix)
} }
dns := make([]netip.Addr, 0, len(t.options.DNS))
for _, ip := range t.options.DNS {
dns = append(dns, netip.MustParseAddr(ip))
}
var route4, route6 bool
routesMap := make(map[winipcfg.RouteData]struct{}) routesMap := make(map[winipcfg.RouteData]struct{})
for _, ip := range allowedIPs { for _, cidr := range t.options.AutoSystemRoutingTable {
prefix := netip.MustParsePrefix(cidr)
route := winipcfg.RouteData{ route := winipcfg.RouteData{
Destination: ip.Masked(), Destination: prefix.Masked(),
Metric: 0, Metric: 0,
} }
if ip.Addr().Is4() { if prefix.Addr().Is4() {
has4 = true route4 = true
route.NextHop = netip.IPv4Unspecified() route.NextHop = netip.IPv4Unspecified()
} else { } else {
has6 = true route6 = true
route.NextHop = netip.IPv6Unspecified() route.NextHop = netip.IPv6Unspecified()
} }
routesMap[route] = struct{}{} routesMap[route] = struct{}{}
@@ -111,24 +126,40 @@ func (t *WindowsTun) Start() error {
r := route r := route
routesData = append(routesData, &r) routesData = append(routesData, &r)
} }
err := t.luid.SetRoutes(routesData)
if err != nil {
return errors.New("unable to set routes").Base(err)
}
if len(t.options.Gateway) > 0 { var retryTimes int
addresses := make([]netip.Prefix, 0, len(t.options.Gateway)) var firstErr error
for _, address := range t.options.Gateway { startOver:
addresses = append(addresses, netip.MustParsePrefix(address)) if retryTimes > 0 {
} if retryTimes > 15 {
err := t.luid.SetIPAddresses(addresses) return windows.ERROR_NOT_FOUND
if err != nil {
return errors.New("unable to set ips").Base(err)
} }
errors.LogErrorInner(context.Background(), firstErr, "Interface configuration failed, retrying attempt ", retryTimes, "/15")
time.Sleep(time.Second)
} }
retryTimes++
if has4 { for _, family := range []winipcfg.AddressFamily{windows.AF_INET, windows.AF_INET6} {
ipif, err := t.luid.IPInterface(windows.AF_INET) if family == windows.AF_INET && route4 || family == windows.AF_INET6 && route6 {
err = t.luid.SetRoutesForFamily(family, routesData)
if err != nil {
firstErr = errors.New("unable to set routes").Base(err)
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
return firstErr
}
}
if family == windows.AF_INET && address4 || family == windows.AF_INET6 && address6 {
err = t.luid.SetIPAddressesForFamily(family, addresses)
if err != nil {
firstErr = errors.New("unable to set ips").Base(err)
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
return firstErr
}
}
ipif, err := t.luid.IPInterface(family)
if err != nil { if err != nil {
return err return err
} }
@@ -136,56 +167,45 @@ func (t *WindowsTun) Start() error {
ipif.DadTransmits = 0 ipif.DadTransmits = 0
ipif.ManagedAddressConfigurationSupported = false ipif.ManagedAddressConfigurationSupported = false
ipif.OtherStatefulConfigurationSupported = false ipif.OtherStatefulConfigurationSupported = false
ipif.NLMTU = t.options.MTU if family == windows.AF_INET && (address4 || route4) || family == windows.AF_INET6 && (address6 || route6) {
ipif.UseAutomaticMetric = false ipif.NLMTU = t.options.MTU
ipif.Metric = 0 }
if family == windows.AF_INET && route4 || family == windows.AF_INET6 && route6 {
ipif.UseAutomaticMetric = false
ipif.Metric = 0
}
err = ipif.Set() err = ipif.Set()
if err != nil { if err != nil {
return err firstErr = errors.New("unable to set metric and MTU").Base(err)
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
return firstErr
} }
} err = t.luid.SetDNS(family, dns, nil)
if has6 {
ipif, err := t.luid.IPInterface(windows.AF_INET6)
if err != nil { if err != nil {
return err firstErr = errors.New("unable to set DNS").Base(err)
} if err == windows.ERROR_NOT_FOUND {
ipif.RouterDiscoveryBehavior = winipcfg.RouterDiscoveryDisabled goto startOver
ipif.DadTransmits = 0 }
ipif.ManagedAddressConfigurationSupported = false return firstErr
ipif.OtherStatefulConfigurationSupported = false
ipif.NLMTU = t.options.MTU
ipif.UseAutomaticMetric = false
ipif.Metric = 0
err = ipif.Set()
if err != nil {
return err
}
}
if len(t.options.DNS) > 0 {
dns := make([]netip.Addr, 0, len(t.options.DNS))
for _, ip := range t.options.DNS {
dns = append(dns, netip.MustParseAddr(ip))
}
err := t.luid.SetDNS(windows.AF_INET, dns, nil)
if err != nil {
return err
}
err = t.luid.SetDNS(windows.AF_INET6, dns, nil)
if err != nil {
return err
} }
} }
if updater != nil { if updater != nil {
t.changeCallback, err = winipcfg.RegisterInterfaceChangeCallback(func(notificationType winipcfg.MibNotificationType, iface *winipcfg.MibIPInterfaceRow) { t.cbr, err = winipcfg.RegisterRouteChangeCallback(func(notificationType winipcfg.MibNotificationType, route *winipcfg.MibIPforwardRow2) {
updater.Update()
})
if err != nil {
return err
}
t.cbi, err = winipcfg.RegisterInterfaceChangeCallback(func(notificationType winipcfg.MibNotificationType, iface *winipcfg.MibIPInterfaceRow) {
updater.Update() updater.Update()
}) })
if err != nil { if err != nil {
return err return err
} }
} }
return nil return nil
} }
@@ -197,12 +217,26 @@ func (t *WindowsTun) Close() error {
} }
t.closed = true t.closed = true
if t.changeCallback != nil { if t.cbr != nil {
t.changeCallback.Unregister() t.cbr.Unregister()
}
if t.cbi != nil {
t.cbi.Unregister()
}
if t.luid != 0 {
t.luid.FlushRoutes(windows.AF_INET)
t.luid.FlushIPAddresses(windows.AF_INET)
t.luid.FlushDNS(windows.AF_INET)
t.luid.FlushRoutes(windows.AF_INET6)
t.luid.FlushIPAddresses(windows.AF_INET6)
t.luid.FlushDNS(windows.AF_INET6)
}
if t.session != (wintun.Session{}) {
t.session.End()
}
if t.adapter != nil {
t.adapter.Close()
} }
t.session.End()
_ = t.adapter.Close()
return nil return nil
} }
@@ -311,75 +345,49 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
} }
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
interfaces, err := net.Interfaces() if fixedName != "" {
return net.InterfaceByName(fixedName)
}
r, err := winipcfg.GetIPForwardTable2(windows.AF_UNSPEC)
if err != nil { if err != nil {
return nil, err return nil, err
} }
lowestMetric := ^uint32(0)
index := uint32(0)
lowestMetricWifi := ^uint32(0)
indexWifi := uint32(0)
for i := range r {
if r[i].DestinationPrefix.PrefixLength != 0 || r[i].InterfaceIndex == uint32(tunIndex) {
continue
}
ifrow, err := r[i].InterfaceLUID.Interface()
if err != nil || ifrow.OperStatus != winipcfg.IfOperStatusUp {
continue
}
if fixedName != "" { iface, err := r[i].InterfaceLUID.IPInterface(windows.AF_INET)
for _, iface := range interfaces { if err != nil {
if iface.Index != tunIndex && iface.Name == fixedName { iface, err = r[i].InterfaceLUID.IPInterface(windows.AF_INET6)
return &iface, nil if err != nil {
continue
} }
} }
return nil, nil
}
var candidates []struct { if ifrow.Type == windows.IF_TYPE_IEEE80211 {
index int if r[i].Metric+iface.Metric < lowestMetricWifi {
score int lowestMetricWifi = r[i].Metric + iface.Metric
indexWifi = r[i].InterfaceIndex
}
continue
}
if r[i].Metric+iface.Metric < lowestMetric {
lowestMetric = r[i].Metric + iface.Metric
index = r[i].InterfaceIndex
}
} }
for i, iface := range interfaces { if indexWifi != 0 {
if iface.Index == tunIndex { index = indexWifi
continue
}
if strings.Contains(iface.Name, "vEthernet") {
continue
}
if iface.Flags&net.FlagUp == 0 {
continue
}
if iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil || len(addrs) == 0 {
continue
}
candidates = append(candidates, struct {
index int
score int
}{i, scoreWindowsInterface(&iface, addrs)})
} }
return net.InterfaceByIndex(int(index))
sort.Slice(candidates, func(i, j int) bool {
if candidates[i].score != candidates[j].score {
return candidates[i].score > candidates[j].score
}
return interfaces[candidates[i].index].Name < interfaces[candidates[j].index].Name
})
if len(candidates) == 0 {
return nil, nil
}
iface := interfaces[candidates[0].index]
return &iface, nil
}
func scoreWindowsInterface(iface *net.Interface, addrs []net.Addr) int {
score := 0
name := strings.ToLower(iface.Name)
if strings.Contains(name, "wlan") || strings.Contains(name, "wi-fi") {
score += 2
}
for _, addr := range addrs {
if strings.HasPrefix(addr.String(), "192.168.") {
score++
break
}
}
return score
} }
-2
View File
@@ -202,8 +202,6 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
} }
defer conn.Close() defer conn.Close()
ob.Conn = conn // for Vision's pre-connect
iConn := stat.TryUnwrapStatsConn(conn) iConn := stat.TryUnwrapStatsConn(conn)
target := ob.Target target := ob.Target
errors.LogInfo(ctx, "tunneling request to ", target, " via ", rec.Destination.NetAddr()) errors.LogInfo(ctx, "tunneling request to ", target, " via ", rec.Destination.NetAddr())
+7 -2
View File
@@ -82,9 +82,14 @@ func TestResolveIP(t *testing.T) {
}, },
{ {
Tag: "direct", Tag: "direct",
ProxySettings: serial.ToTypedMessage(&freedom.Config{ SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
DomainStrategy: internet.DomainStrategy_USE_IP, StreamSettings: &internet.StreamConfig{
SocketSettings: &internet.SocketConfig{
DomainStrategy: internet.DomainStrategy_USE_IP,
},
},
}), }),
ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
}, },
}, },
} }
+9 -7
View File
@@ -92,7 +92,7 @@ func TestPassiveConnection(t *testing.T) {
} }
} }
func TestProxy(t *testing.T) { func TestDialerProxy(t *testing.T) {
tcpServer := tcp.Server{ tcpServer := tcp.Server{
MsgProcessor: xor, MsgProcessor: xor,
} }
@@ -187,8 +187,10 @@ func TestProxy(t *testing.T) {
}, },
}), }),
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
ProxySettings: &internet.ProxyConfig{ StreamSettings: &internet.StreamConfig{
Tag: "proxy", SocketSettings: &internet.SocketConfig{
DialerProxy: "proxy",
},
}, },
}), }),
}, },
@@ -218,7 +220,7 @@ func TestProxy(t *testing.T) {
} }
} }
func TestProxyOverKCP(t *testing.T) { func TestDialerProxyOverKCP(t *testing.T) {
tcpServer := tcp.Server{ tcpServer := tcp.Server{
MsgProcessor: xor, MsgProcessor: xor,
} }
@@ -321,11 +323,11 @@ func TestProxyOverKCP(t *testing.T) {
}, },
}), }),
SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
ProxySettings: &internet.ProxyConfig{
Tag: "proxy",
},
StreamSettings: &internet.StreamConfig{ StreamSettings: &internet.StreamConfig{
ProtocolName: "mkcp", ProtocolName: "mkcp",
SocketSettings: &internet.SocketConfig{
DialerProxy: "proxy",
},
}, },
}), }),
}, },
+2 -2
View File
@@ -509,7 +509,7 @@ func TestVlessXtlsVisionReality(t *testing.T) {
// This testing test all known utls fingerprint in tls.PresetFingerprints that support reality (expect unsafe and random*) // This testing test all known utls fingerprint in tls.PresetFingerprints that support reality (expect unsafe and random*)
// Beacuse figerprint support may be broken after utls/reality update // Beacuse figerprint support may be broken after utls/reality update
// Known broken fingerprint: android, 360 // Known working fingerprint: chrome, firefox, safari
func TestVlessRealityFingerprints(t *testing.T) { func TestVlessRealityFingerprints(t *testing.T) {
TestFingerprint := func(fingerprint string) error { TestFingerprint := func(fingerprint string) error {
tcpServer := tcp.Server{ tcpServer := tcp.Server{
@@ -641,7 +641,7 @@ func TestVlessRealityFingerprints(t *testing.T) {
} }
return nil return nil
} }
fingerPrints := []string{"chrome", "firefox", "safari", "ios", "edge", "qq"} fingerPrints := []string{"chrome", "firefox", "safari"}
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
wg.Add(len(fingerPrints)) wg.Add(len(fingerPrints))
for _, fp := range fingerPrints { for _, fp := range fingerPrints {
+1 -7
View File
@@ -10,7 +10,7 @@ type ConfigCreator func() interface{}
var globalTransportConfigCreatorCache = make(map[string]ConfigCreator) var globalTransportConfigCreatorCache = make(map[string]ConfigCreator)
var strategy = [][]byte{ var strategy = [11][3]byte{
// name strategy, prefer, fallback // name strategy, prefer, fallback
{0, 0, 0}, // AsIs none, /, / {0, 0, 0}, // AsIs none, /, /
{1, 0, 0}, // UseIP use, both, none {1, 0, 0}, // UseIP use, both, none
@@ -25,8 +25,6 @@ var strategy = [][]byte{
{2, 6, 4}, // ForceIPv6v4 force, 6, 4 {2, 6, 4}, // ForceIPv6v4 force, 6, 4
} }
const unknownProtocol = "unknown"
func RegisterProtocolConfigCreator(name string, creator ConfigCreator) error { func RegisterProtocolConfigCreator(name string, creator ConfigCreator) error {
if _, found := globalTransportConfigCreatorCache[name]; found { if _, found := globalTransportConfigCreatorCache[name]; found {
return errors.New("protocol ", name, " is already registered").AtError() return errors.New("protocol ", name, " is already registered").AtError()
@@ -91,10 +89,6 @@ func (c *StreamConfig) HasSecuritySettings() bool {
return len(c.SecuritySettings) > 0 return len(c.SecuritySettings) > 0
} }
func (c *ProxyConfig) HasTag() bool {
return c != nil && len(c.Tag) > 0
}
func (m SocketConfig_TProxyMode) IsEnabled() bool { func (m SocketConfig_TProxyMode) IsEnabled() bool {
return m != SocketConfig_Off return m != SocketConfig_Off
} }
+86 -104
View File
@@ -206,7 +206,7 @@ func (x SocketConfig_TProxyMode) Number() protoreflect.EnumNumber {
// Deprecated: Use SocketConfig_TProxyMode.Descriptor instead. // Deprecated: Use SocketConfig_TProxyMode.Descriptor instead.
func (SocketConfig_TProxyMode) EnumDescriptor() ([]byte, []int) { func (SocketConfig_TProxyMode) EnumDescriptor() ([]byte, []int) {
return file_transport_internet_config_proto_rawDescGZIP(), []int{6, 0} return file_transport_internet_config_proto_rawDescGZIP(), []int{5, 0}
} }
type TransportConfig struct { type TransportConfig struct {
@@ -443,22 +443,26 @@ func (x *UdpHop) GetIntervalMax() int64 {
} }
type QuicParams struct { type QuicParams struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Congestion string `protobuf:"bytes,1,opt,name=congestion,proto3" json:"congestion,omitempty"` 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"` 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"` 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"` 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"` BrutalDisableLossCompensation bool `protobuf:"varint,5,opt,name=brutal_disable_loss_compensation,json=brutalDisableLossCompensation,proto3" json:"brutal_disable_loss_compensation,omitempty"`
InitStreamReceiveWindow uint64 `protobuf:"varint,6,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"` UdpHop *UdpHop `protobuf:"bytes,6,opt,name=udp_hop,json=udpHop,proto3" json:"udp_hop,omitempty"`
MaxStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"` InitStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"`
InitConnReceiveWindow uint64 `protobuf:"varint,8,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"` MaxStreamReceiveWindow uint64 `protobuf:"varint,8,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"`
MaxConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"` InitConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"`
MaxIdleTimeout int64 `protobuf:"varint,10,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"` MaxConnReceiveWindow uint64 `protobuf:"varint,10,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"`
KeepAlivePeriod int64 `protobuf:"varint,11,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"` MaxIdleTimeout int64 `protobuf:"varint,11,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"`
DisablePathMtuDiscovery bool `protobuf:"varint,12,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"` KeepAlivePeriod int64 `protobuf:"varint,12,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"`
MaxIncomingStreams int64 `protobuf:"varint,13,opt,name=max_incoming_streams,json=maxIncomingStreams,proto3" json:"max_incoming_streams,omitempty"` DisablePathMtuDiscovery bool `protobuf:"varint,13,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"`
unknownFields protoimpl.UnknownFields DisableChromeParrot bool `protobuf:"varint,14,opt,name=disable_chrome_parrot,json=disableChromeParrot,proto3" json:"disable_chrome_parrot,omitempty"`
sizeCache protoimpl.SizeCache 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() { func (x *QuicParams) Reset() {
@@ -519,6 +523,13 @@ func (x *QuicParams) GetBrutalDown() uint64 {
return 0 return 0
} }
func (x *QuicParams) GetBrutalDisableLossCompensation() bool {
if x != nil {
return x.BrutalDisableLossCompensation
}
return false
}
func (x *QuicParams) GetUdpHop() *UdpHop { func (x *QuicParams) GetUdpHop() *UdpHop {
if x != nil { if x != nil {
return x.UdpHop return x.UdpHop
@@ -575,6 +586,20 @@ func (x *QuicParams) GetDisablePathMtuDiscovery() bool {
return false 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 { func (x *QuicParams) GetMaxIncomingStreams() int64 {
if x != nil { if x != nil {
return x.MaxIncomingStreams return x.MaxIncomingStreams
@@ -582,54 +607,9 @@ func (x *QuicParams) GetMaxIncomingStreams() int64 {
return 0 return 0
} }
type ProxyConfig struct { func (x *QuicParams) GetDisableStatelessReset() bool {
state protoimpl.MessageState `protogen:"open.v1"`
Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
TransportLayerProxy bool `protobuf:"varint,2,opt,name=transportLayerProxy,proto3" json:"transportLayerProxy,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProxyConfig) Reset() {
*x = ProxyConfig{}
mi := &file_transport_internet_config_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProxyConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProxyConfig) ProtoMessage() {}
func (x *ProxyConfig) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_config_proto_msgTypes[4]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) return x.DisableStatelessReset
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProxyConfig.ProtoReflect.Descriptor instead.
func (*ProxyConfig) Descriptor() ([]byte, []int) {
return file_transport_internet_config_proto_rawDescGZIP(), []int{4}
}
func (x *ProxyConfig) GetTag() string {
if x != nil {
return x.Tag
}
return ""
}
func (x *ProxyConfig) GetTransportLayerProxy() bool {
if x != nil {
return x.TransportLayerProxy
} }
return false return false
} }
@@ -648,7 +628,7 @@ type CustomSockopt struct {
func (x *CustomSockopt) Reset() { func (x *CustomSockopt) Reset() {
*x = CustomSockopt{} *x = CustomSockopt{}
mi := &file_transport_internet_config_proto_msgTypes[5] mi := &file_transport_internet_config_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -660,7 +640,7 @@ func (x *CustomSockopt) String() string {
func (*CustomSockopt) ProtoMessage() {} func (*CustomSockopt) ProtoMessage() {}
func (x *CustomSockopt) ProtoReflect() protoreflect.Message { func (x *CustomSockopt) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_config_proto_msgTypes[5] mi := &file_transport_internet_config_proto_msgTypes[4]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -673,7 +653,7 @@ func (x *CustomSockopt) ProtoReflect() protoreflect.Message {
// Deprecated: Use CustomSockopt.ProtoReflect.Descriptor instead. // Deprecated: Use CustomSockopt.ProtoReflect.Descriptor instead.
func (*CustomSockopt) Descriptor() ([]byte, []int) { func (*CustomSockopt) Descriptor() ([]byte, []int) {
return file_transport_internet_config_proto_rawDescGZIP(), []int{5} return file_transport_internet_config_proto_rawDescGZIP(), []int{4}
} }
func (x *CustomSockopt) GetSystem() string { func (x *CustomSockopt) GetSystem() string {
@@ -753,7 +733,7 @@ type SocketConfig struct {
func (x *SocketConfig) Reset() { func (x *SocketConfig) Reset() {
*x = SocketConfig{} *x = SocketConfig{}
mi := &file_transport_internet_config_proto_msgTypes[6] mi := &file_transport_internet_config_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -765,7 +745,7 @@ func (x *SocketConfig) String() string {
func (*SocketConfig) ProtoMessage() {} func (*SocketConfig) ProtoMessage() {}
func (x *SocketConfig) ProtoReflect() protoreflect.Message { func (x *SocketConfig) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_config_proto_msgTypes[6] mi := &file_transport_internet_config_proto_msgTypes[5]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -778,7 +758,7 @@ func (x *SocketConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use SocketConfig.ProtoReflect.Descriptor instead. // Deprecated: Use SocketConfig.ProtoReflect.Descriptor instead.
func (*SocketConfig) Descriptor() ([]byte, []int) { func (*SocketConfig) Descriptor() ([]byte, []int) {
return file_transport_internet_config_proto_rawDescGZIP(), []int{6} return file_transport_internet_config_proto_rawDescGZIP(), []int{5}
} }
func (x *SocketConfig) GetMark() int32 { func (x *SocketConfig) GetMark() int32 {
@@ -940,7 +920,7 @@ type HappyEyeballsConfig struct {
func (x *HappyEyeballsConfig) Reset() { func (x *HappyEyeballsConfig) Reset() {
*x = HappyEyeballsConfig{} *x = HappyEyeballsConfig{}
mi := &file_transport_internet_config_proto_msgTypes[7] mi := &file_transport_internet_config_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -952,7 +932,7 @@ func (x *HappyEyeballsConfig) String() string {
func (*HappyEyeballsConfig) ProtoMessage() {} func (*HappyEyeballsConfig) ProtoMessage() {}
func (x *HappyEyeballsConfig) ProtoReflect() protoreflect.Message { func (x *HappyEyeballsConfig) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_config_proto_msgTypes[7] mi := &file_transport_internet_config_proto_msgTypes[6]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -965,7 +945,7 @@ func (x *HappyEyeballsConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use HappyEyeballsConfig.ProtoReflect.Descriptor instead. // Deprecated: Use HappyEyeballsConfig.ProtoReflect.Descriptor instead.
func (*HappyEyeballsConfig) Descriptor() ([]byte, []int) { func (*HappyEyeballsConfig) Descriptor() ([]byte, []int) {
return file_transport_internet_config_proto_rawDescGZIP(), []int{7} return file_transport_internet_config_proto_rawDescGZIP(), []int{6}
} }
func (x *HappyEyeballsConfig) GetPrioritizeIpv6() bool { func (x *HappyEyeballsConfig) GetPrioritizeIpv6() bool {
@@ -1020,7 +1000,7 @@ const file_transport_internet_config_proto_rawDesc = "" +
"\x06UdpHop\x12\x14\n" + "\x06UdpHop\x12\x14\n" +
"\x05ports\x18\x01 \x03(\rR\x05ports\x12!\n" + "\x05ports\x18\x01 \x03(\rR\x05ports\x12!\n" +
"\finterval_min\x18\x02 \x01(\x03R\vintervalMin\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" + "\n" +
"QuicParams\x12\x1e\n" + "QuicParams\x12\x1e\n" +
"\n" + "\n" +
@@ -1030,20 +1010,23 @@ const file_transport_internet_config_proto_rawDesc = "" +
"bbrProfile\x12\x1b\n" + "bbrProfile\x12\x1b\n" +
"\tbrutal_up\x18\x03 \x01(\x04R\bbrutalUp\x12\x1f\n" + "\tbrutal_up\x18\x03 \x01(\x04R\bbrutalUp\x12\x1f\n" +
"\vbrutal_down\x18\x04 \x01(\x04R\n" + "\vbrutal_down\x18\x04 \x01(\x04R\n" +
"brutalDown\x128\n" + "brutalDown\x12G\n" +
"\audp_hop\x18\x05 \x01(\v2\x1f.xray.transport.internet.UdpHopR\x06udpHop\x12;\n" + " brutal_disable_loss_compensation\x18\x05 \x01(\bR\x1dbrutalDisableLossCompensation\x128\n" +
"\x1ainit_stream_receive_window\x18\x06 \x01(\x04R\x17initStreamReceiveWindow\x129\n" + "\audp_hop\x18\x06 \x01(\v2\x1f.xray.transport.internet.UdpHopR\x06udpHop\x12;\n" +
"\x19max_stream_receive_window\x18\a \x01(\x04R\x16maxStreamReceiveWindow\x127\n" + "\x1ainit_stream_receive_window\x18\a \x01(\x04R\x17initStreamReceiveWindow\x129\n" +
"\x18init_conn_receive_window\x18\b \x01(\x04R\x15initConnReceiveWindow\x125\n" + "\x19max_stream_receive_window\x18\b \x01(\x04R\x16maxStreamReceiveWindow\x127\n" +
"\x17max_conn_receive_window\x18\t \x01(\x04R\x14maxConnReceiveWindow\x12(\n" + "\x18init_conn_receive_window\x18\t \x01(\x04R\x15initConnReceiveWindow\x125\n" +
"\x10max_idle_timeout\x18\n" + "\x17max_conn_receive_window\x18\n" +
" \x01(\x03R\x0emaxIdleTimeout\x12*\n" + " \x01(\x04R\x14maxConnReceiveWindow\x12(\n" +
"\x11keep_alive_period\x18\v \x01(\x03R\x0fkeepAlivePeriod\x12;\n" + "\x10max_idle_timeout\x18\v \x01(\x03R\x0emaxIdleTimeout\x12*\n" +
"\x1adisable_path_mtu_discovery\x18\f \x01(\bR\x17disablePathMtuDiscovery\x120\n" + "\x11keep_alive_period\x18\f \x01(\x03R\x0fkeepAlivePeriod\x12;\n" +
"\x14max_incoming_streams\x18\r \x01(\x03R\x12maxIncomingStreams\"Q\n" + "\x1adisable_path_mtu_discovery\x18\r \x01(\bR\x17disablePathMtuDiscovery\x122\n" +
"\vProxyConfig\x12\x10\n" + "\x15disable_chrome_parrot\x18\x0e \x01(\bR\x13disableChromeParrot\x12\x1e\n" +
"\x03tag\x18\x01 \x01(\tR\x03tag\x120\n" + "\n" +
"\x13transportLayerProxy\x18\x02 \x01(\bR\x13transportLayerProxy\"\x93\x01\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\"\x93\x01\n" +
"\rCustomSockopt\x12\x16\n" + "\rCustomSockopt\x12\x16\n" +
"\x06system\x18\x01 \x01(\tR\x06system\x12\x18\n" + "\x06system\x18\x01 \x01(\tR\x06system\x12\x18\n" +
"\anetwork\x18\x02 \x01(\tR\anetwork\x12\x14\n" + "\anetwork\x18\x02 \x01(\tR\anetwork\x12\x14\n" +
@@ -1127,7 +1110,7 @@ func file_transport_internet_config_proto_rawDescGZIP() []byte {
} }
var file_transport_internet_config_proto_enumTypes = make([]protoimpl.EnumInfo, 3) var file_transport_internet_config_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
var file_transport_internet_config_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_transport_internet_config_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_transport_internet_config_proto_goTypes = []any{ var file_transport_internet_config_proto_goTypes = []any{
(DomainStrategy)(0), // 0: xray.transport.internet.DomainStrategy (DomainStrategy)(0), // 0: xray.transport.internet.DomainStrategy
(AddressPortStrategy)(0), // 1: xray.transport.internet.AddressPortStrategy (AddressPortStrategy)(0), // 1: xray.transport.internet.AddressPortStrategy
@@ -1136,28 +1119,27 @@ var file_transport_internet_config_proto_goTypes = []any{
(*StreamConfig)(nil), // 4: xray.transport.internet.StreamConfig (*StreamConfig)(nil), // 4: xray.transport.internet.StreamConfig
(*UdpHop)(nil), // 5: xray.transport.internet.UdpHop (*UdpHop)(nil), // 5: xray.transport.internet.UdpHop
(*QuicParams)(nil), // 6: xray.transport.internet.QuicParams (*QuicParams)(nil), // 6: xray.transport.internet.QuicParams
(*ProxyConfig)(nil), // 7: xray.transport.internet.ProxyConfig (*CustomSockopt)(nil), // 7: xray.transport.internet.CustomSockopt
(*CustomSockopt)(nil), // 8: xray.transport.internet.CustomSockopt (*SocketConfig)(nil), // 8: xray.transport.internet.SocketConfig
(*SocketConfig)(nil), // 9: xray.transport.internet.SocketConfig (*HappyEyeballsConfig)(nil), // 9: xray.transport.internet.HappyEyeballsConfig
(*HappyEyeballsConfig)(nil), // 10: xray.transport.internet.HappyEyeballsConfig (*serial.TypedMessage)(nil), // 10: xray.common.serial.TypedMessage
(*serial.TypedMessage)(nil), // 11: xray.common.serial.TypedMessage (*net.IPOrDomain)(nil), // 11: xray.common.net.IPOrDomain
(*net.IPOrDomain)(nil), // 12: xray.common.net.IPOrDomain
} }
var file_transport_internet_config_proto_depIdxs = []int32{ var file_transport_internet_config_proto_depIdxs = []int32{
11, // 0: xray.transport.internet.TransportConfig.settings:type_name -> xray.common.serial.TypedMessage 10, // 0: xray.transport.internet.TransportConfig.settings:type_name -> xray.common.serial.TypedMessage
12, // 1: xray.transport.internet.StreamConfig.address:type_name -> xray.common.net.IPOrDomain 11, // 1: xray.transport.internet.StreamConfig.address:type_name -> xray.common.net.IPOrDomain
3, // 2: xray.transport.internet.StreamConfig.transport_settings:type_name -> xray.transport.internet.TransportConfig 3, // 2: xray.transport.internet.StreamConfig.transport_settings:type_name -> xray.transport.internet.TransportConfig
11, // 3: xray.transport.internet.StreamConfig.security_settings:type_name -> xray.common.serial.TypedMessage 10, // 3: xray.transport.internet.StreamConfig.security_settings:type_name -> xray.common.serial.TypedMessage
11, // 4: xray.transport.internet.StreamConfig.udpmasks:type_name -> xray.common.serial.TypedMessage 10, // 4: xray.transport.internet.StreamConfig.udpmasks:type_name -> xray.common.serial.TypedMessage
11, // 5: xray.transport.internet.StreamConfig.tcpmasks:type_name -> xray.common.serial.TypedMessage 10, // 5: xray.transport.internet.StreamConfig.tcpmasks:type_name -> xray.common.serial.TypedMessage
6, // 6: xray.transport.internet.StreamConfig.quic_params:type_name -> xray.transport.internet.QuicParams 6, // 6: xray.transport.internet.StreamConfig.quic_params:type_name -> xray.transport.internet.QuicParams
9, // 7: xray.transport.internet.StreamConfig.socket_settings:type_name -> xray.transport.internet.SocketConfig 8, // 7: xray.transport.internet.StreamConfig.socket_settings:type_name -> xray.transport.internet.SocketConfig
5, // 8: xray.transport.internet.QuicParams.udp_hop:type_name -> xray.transport.internet.UdpHop 5, // 8: xray.transport.internet.QuicParams.udp_hop:type_name -> xray.transport.internet.UdpHop
2, // 9: xray.transport.internet.SocketConfig.tproxy:type_name -> xray.transport.internet.SocketConfig.TProxyMode 2, // 9: xray.transport.internet.SocketConfig.tproxy:type_name -> xray.transport.internet.SocketConfig.TProxyMode
0, // 10: xray.transport.internet.SocketConfig.domain_strategy:type_name -> xray.transport.internet.DomainStrategy 0, // 10: xray.transport.internet.SocketConfig.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
8, // 11: xray.transport.internet.SocketConfig.customSockopt:type_name -> xray.transport.internet.CustomSockopt 7, // 11: xray.transport.internet.SocketConfig.customSockopt:type_name -> xray.transport.internet.CustomSockopt
1, // 12: xray.transport.internet.SocketConfig.address_port_strategy:type_name -> xray.transport.internet.AddressPortStrategy 1, // 12: xray.transport.internet.SocketConfig.address_port_strategy:type_name -> xray.transport.internet.AddressPortStrategy
10, // 13: xray.transport.internet.SocketConfig.happy_eyeballs:type_name -> xray.transport.internet.HappyEyeballsConfig 9, // 13: xray.transport.internet.SocketConfig.happy_eyeballs:type_name -> xray.transport.internet.HappyEyeballsConfig
14, // [14:14] is the sub-list for method output_type 14, // [14:14] is the sub-list for method output_type
14, // [14:14] is the sub-list for method input_type 14, // [14:14] is the sub-list for method input_type
14, // [14:14] is the sub-list for extension type_name 14, // [14:14] is the sub-list for extension type_name
@@ -1176,7 +1158,7 @@ func file_transport_internet_config_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_config_proto_rawDesc), len(file_transport_internet_config_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_config_proto_rawDesc), len(file_transport_internet_config_proto_rawDesc)),
NumEnums: 3, NumEnums: 3,
NumMessages: 8, NumMessages: 7,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
+13 -14
View File
@@ -75,20 +75,19 @@ message QuicParams {
string bbr_profile = 2; string bbr_profile = 2;
uint64 brutal_up = 3; uint64 brutal_up = 3;
uint64 brutal_down = 4; uint64 brutal_down = 4;
UdpHop udp_hop = 5; bool brutal_disable_loss_compensation = 5;
uint64 init_stream_receive_window = 6; UdpHop udp_hop = 6;
uint64 max_stream_receive_window = 7; uint64 init_stream_receive_window = 7;
uint64 init_conn_receive_window = 8; uint64 max_stream_receive_window = 8;
uint64 max_conn_receive_window = 9; uint64 init_conn_receive_window = 9;
int64 max_idle_timeout = 10; uint64 max_conn_receive_window = 10;
int64 keep_alive_period = 11; int64 max_idle_timeout = 11;
bool disable_path_mtu_discovery = 12; int64 keep_alive_period = 12;
int64 max_incoming_streams = 13; bool disable_path_mtu_discovery = 13;
} bool disable_chrome_parrot = 14;
bool disableGSO = 15;
message ProxyConfig { int64 max_incoming_streams = 16;
string tag = 1; bool disable_stateless_reset = 17;
bool transportLayerProxy = 2;
} }
message CustomSockopt { message CustomSockopt {
+89 -4
View File
@@ -7,6 +7,7 @@ import (
"net/netip" "net/netip"
"slices" "slices"
"strings" "strings"
"sync"
"time" "time"
"github.com/pion/stun/v3" "github.com/pion/stun/v3"
@@ -15,35 +16,67 @@ import (
) )
type realmConnClient struct { type realmConnClient struct {
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
net.PacketConn net.PacketConn
peer *net.UDPAddr peer *net.UDPAddr
realmClient *Client realmClient *Client
realmID string realmID string
stunServers []string stunServers []string
family Family
mapper *PortMapper
stunTimeout time.Duration stunTimeout time.Duration
punchTimeout time.Duration punchTimeout time.Duration
punchInterval time.Duration punchInterval time.Duration
} }
func NewConnClient(config *Config, raw net.PacketConn) (net.PacketConn, error) { 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{ conn := &realmConnClient{
ctx: ctx,
cancel: cancel,
PacketConn: raw, PacketConn: raw,
realmClient: NewClient(config.Scheme, config.Host, config.Port, config.Token, config.TlsConfig), realmClient: NewClient(config.Scheme, config.Host, config.Port, config.Token, config.TlsConfig),
realmID: config.ID, realmID: config.ID,
stunServers: config.StunServers, stunServers: config.StunServers,
family: family,
mapper: mapper,
stunTimeout: defaultSTUNTimeout, stunTimeout: defaultSTUNTimeout,
punchTimeout: defaultPunchTimeout, punchTimeout: defaultPunchTimeout,
punchInterval: defaultPunchInterval, punchInterval: defaultPunchInterval,
} }
return conn.getpeer() return conn.getpeer()
} }
func (c *realmConnClient) getpeer() (net.PacketConn, error) { func (c *realmConnClient) getpeer() (net.PacketConn, error) {
start := time.Now() 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)) errors.LogDebug(context.Background(), "[realm] update stun servers ", servers, " with ", time.Since(start))
if len(servers) == 0 { if len(servers) == 0 {
return nil, errors.New("empty locals") return nil, errors.New("empty locals")
@@ -70,7 +103,7 @@ func (c *realmConnClient) getpeer() (net.PacketConn, error) {
peers, _ := parseAddrPorts(resp.Addresses) peers, _ := parseAddrPorts(resp.Addresses)
errors.LogDebug(context.Background(), "[realm] update peers ", peers) 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) errors.LogDebug(context.Background(), "[realm] filtered peers ", filteredPeers)
expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen) expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen)
errors.LogDebug(context.Background(), "[realm] expanded peers ", expandedPeers) 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)) 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 c.peer = peer
return c, nil return c, nil
} }
@@ -116,10 +154,12 @@ func (c *realmConnClient) discover(servers []*net.UDPAddr) []netip.AddrPort {
} }
} }
c.PacketConn.SetReadDeadline(time.Time{}) c.PacketConn.SetReadDeadline(time.Time{})
if c.mapper != nil {
results = insertAddr(results, c.mapper.ExternalAddr())
}
slices.SortFunc(results, func(a, b netip.AddrPort) int { slices.SortFunc(results, func(a, b netip.AddrPort) int {
return strings.Compare(a.String(), b.String()) return strings.Compare(a.String(), b.String())
}) })
return results 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) { func (c *realmConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return c.PacketConn.WriteTo(p, c.peer) 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
}
}
}
}
+156 -16
View File
@@ -22,6 +22,115 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) _ = 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 { type Config struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Scheme string `protobuf:"bytes,1,opt,name=scheme,proto3" json:"scheme,omitempty"` 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"` 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"` 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"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *Config) Reset() { func (x *Config) Reset() {
*x = Config{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -49,7 +160,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {} func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -62,7 +173,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead. // Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) { 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 { func (x *Config) GetScheme() string {
@@ -114,11 +225,29 @@ func (x *Config) GetTlsConfig() *tls.Config {
return nil 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 var File_transport_internet_finalmask_realm_config_proto protoreflect.FileDescriptor
const file_transport_internet_finalmask_realm_config_proto_rawDesc = "" + const file_transport_internet_finalmask_realm_config_proto_rawDesc = "" +
"\n" + "\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" + "\x06Config\x12\x16\n" +
"\x06scheme\x18\x01 \x01(\tR\x06scheme\x12\x12\n" + "\x06scheme\x18\x01 \x01(\tR\x06scheme\x12\x12\n" +
"\x04host\x18\x02 \x01(\tR\x04host\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" + "\x02ID\x18\x05 \x01(\tR\x02ID\x12!\n" +
"\fstun_servers\x18\x06 \x03(\tR\vstunServers\x12B\n" + "\fstun_servers\x18\x06 \x03(\tR\vstunServers\x12B\n" +
"\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<github.com/xtls/xray-core/transport/internet/finalmask/realm\xaa\x02'Xray.Transport.Internet.Finalmask.Realmb\x06proto3" "+com.xray.transport.internet.finalmask.realmP\x01Z<github.com/xtls/xray-core/transport/internet/finalmask/realm\xaa\x02'Xray.Transport.Internet.Finalmask.Realmb\x06proto3"
var ( var (
@@ -142,18 +277,22 @@ func file_transport_internet_finalmask_realm_config_proto_rawDescGZIP() []byte {
return file_transport_internet_finalmask_realm_config_proto_rawDescData return file_transport_internet_finalmask_realm_config_proto_rawDescData
} }
var file_transport_internet_finalmask_realm_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_transport_internet_finalmask_realm_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_transport_internet_finalmask_realm_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_transport_internet_finalmask_realm_config_proto_goTypes = []any{ var file_transport_internet_finalmask_realm_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.transport.internet.finalmask.realm.Config (Family)(0), // 0: xray.transport.internet.finalmask.realm.Family
(*tls.Config)(nil), // 1: xray.transport.internet.tls.Config (*PortMapping)(nil), // 1: xray.transport.internet.finalmask.realm.PortMapping
(*Config)(nil), // 2: xray.transport.internet.finalmask.realm.Config
(*tls.Config)(nil), // 3: xray.transport.internet.tls.Config
} }
var file_transport_internet_finalmask_realm_config_proto_depIdxs = []int32{ var file_transport_internet_finalmask_realm_config_proto_depIdxs = []int32{
1, // 0: xray.transport.internet.finalmask.realm.Config.tls_config:type_name -> xray.transport.internet.tls.Config 3, // 0: xray.transport.internet.finalmask.realm.Config.tls_config:type_name -> xray.transport.internet.tls.Config
1, // [1:1] is the sub-list for method output_type 1, // 1: xray.transport.internet.finalmask.realm.Config.port_mapping:type_name -> xray.transport.internet.finalmask.realm.PortMapping
1, // [1:1] is the sub-list for method input_type 2, // [2:2] is the sub-list for method output_type
1, // [1:1] is the sub-list for extension type_name 2, // [2:2] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension extendee 2, // [2:2] is the sub-list for extension type_name
0, // [0:1] is the sub-list for field 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() } 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{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 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)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_realm_config_proto_rawDesc), len(file_transport_internet_finalmask_realm_config_proto_rawDesc)),
NumEnums: 0, NumEnums: 1,
NumMessages: 1, NumMessages: 2,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
GoTypes: file_transport_internet_finalmask_realm_config_proto_goTypes, GoTypes: file_transport_internet_finalmask_realm_config_proto_goTypes,
DependencyIndexes: file_transport_internet_finalmask_realm_config_proto_depIdxs, 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, MessageInfos: file_transport_internet_finalmask_realm_config_proto_msgTypes,
}.Build() }.Build()
File_transport_internet_finalmask_realm_config_proto = out.File File_transport_internet_finalmask_realm_config_proto = out.File
@@ -8,6 +8,18 @@ option java_multiple_files = true;
import "transport/internet/tls/config.proto"; 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 { message Config {
string scheme = 1; string scheme = 1;
string host = 2; string host = 2;
@@ -16,4 +28,6 @@ message Config {
string ID = 5; string ID = 5;
repeated string stun_servers = 6; repeated string stun_servers = 6;
xray.transport.internet.tls.Config tls_config = 7; xray.transport.internet.tls.Config tls_config = 7;
string IPMode = 8;
PortMapping port_mapping = 9;
} }
@@ -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)
}
+45 -9
View File
@@ -33,14 +33,16 @@ type STUNPacketEvent struct {
} }
type realmConnServer struct { type realmConnServer struct {
cleaned chan struct{} wg sync.WaitGroup
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
net.PacketConn net.PacketConn
realmClient *Client realmClient *Client
realmID string realmID string
stunServers []string stunServers []string
family Family
mapper *PortMapper
stunTimeout time.Duration stunTimeout time.Duration
punchTimeout time.Duration punchTimeout time.Duration
punchInterval time.Duration punchInterval time.Duration
@@ -57,8 +59,29 @@ type realmConnServer struct {
func NewConnServer(config *Config, raw net.PacketConn) (net.PacketConn, error) { func NewConnServer(config *Config, raw net.PacketConn) (net.PacketConn, error) {
ctx, cancel := context.WithCancel(context.Background()) 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{ conn := &realmConnServer{
cleaned: make(chan struct{}),
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
PacketConn: raw, 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), realmClient: NewClient(config.Scheme, config.Host, config.Port, config.Token, config.TlsConfig),
realmID: config.ID, realmID: config.ID,
stunServers: config.StunServers, stunServers: config.StunServers,
family: family,
mapper: mapper,
stunTimeout: defaultSTUNTimeout, stunTimeout: defaultSTUNTimeout,
punchTimeout: defaultPunchTimeout, punchTimeout: defaultPunchTimeout,
punchInterval: defaultPunchInterval, punchInterval: defaultPunchInterval,
@@ -74,6 +99,12 @@ func NewConnServer(config *Config, raw net.PacketConn) (net.PacketConn, error) {
stun: make(chan STUNPacketEvent, defaultEventBuffer), 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() go conn.run()
return conn, nil return conn, nil
@@ -137,6 +168,8 @@ func (c *realmConnServer) discover(servers []*net.UDPAddr) []netip.AddrPort {
results := make([]netip.AddrPort, 0, len(servers)) results := make([]netip.AddrPort, 0, len(servers))
for len(transactionIDs) > 0 { for len(transactionIDs) > 0 {
select { select {
case <-c.ctx.Done():
goto end
case <-deadline.C: case <-deadline.C:
goto end goto end
case ev := <-c.stun: case ev := <-c.stun:
@@ -148,6 +181,9 @@ func (c *realmConnServer) discover(servers []*net.UDPAddr) []netip.AddrPort {
} }
end: end:
deadline.Stop() deadline.Stop()
if c.mapper != nil {
results = insertAddr(results, c.mapper.ExternalAddr())
}
slices.SortFunc(results, func(a, b netip.AddrPort) int { slices.SortFunc(results, func(a, b netip.AddrPort) int {
return strings.Compare(a.String(), b.String()) return strings.Compare(a.String(), b.String())
}) })
@@ -159,7 +195,7 @@ func (c *realmConnServer) getlocals(force bool) []netip.AddrPort {
c.localsMu.Lock() c.localsMu.Lock()
if force || time.Since(c.localsLast) > defaultStunCacheTTL { if force || time.Since(c.localsLast) > defaultStunCacheTTL {
start := time.Now() 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)) errors.LogDebug(context.Background(), "[realm] update stun servers ", servers, " with ", time.Since(start))
if len(servers) > 0 { if len(servers) > 0 {
start = time.Now() start = time.Now()
@@ -232,7 +268,7 @@ retry:
if err != nil { if err != nil {
errors.LogErrorInner(context.Background(), err, "[realm] ", c.realmID, " register session err retry in ", backoff) errors.LogErrorInner(context.Background(), err, "[realm] ", c.realmID, " register session err retry in ", backoff)
if c.waitctx(c.ctx, backoff) { if c.waitctx(c.ctx, backoff) {
close(c.cleaned) c.wg.Done()
return return
} }
backoff *= 2 backoff *= 2
@@ -259,7 +295,7 @@ retry:
case <-c.ctx.Done(): case <-c.ctx.Done():
_ = c.realmClient.Deregister(context.Background(), c.realmID, resp.SessionID) _ = c.realmClient.Deregister(context.Background(), c.realmID, resp.SessionID)
errors.LogDebug(context.Background(), "[realm] ", c.realmID, " ", resp.SessionID, " deregistered") errors.LogDebug(context.Background(), "[realm] ", c.realmID, " ", resp.SessionID, " deregistered")
close(c.cleaned) c.wg.Done()
return return
default: default:
goto retry goto retry
@@ -360,7 +396,7 @@ func (c *realmConnServer) punchEvent(ctx context.Context, sid string, ev *PunchE
peers, _ := parseAddrPorts(ev.Addresses) peers, _ := parseAddrPorts(ev.Addresses)
errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " update peers ", peers) 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) errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " filtered peers ", filteredPeers)
expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen) expandedPeers := expandSymmetricNATCandidates(filteredPeers, seen)
errors.LogDebug(context.Background(), "[realm] ", ev.Nonce, " expanded peers ", expandedPeers) 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 { func (c *realmConnServer) Close() error {
c.cancel() c.cancel()
<-c.cleaned c.wg.Wait()
return c.PacketConn.Close() return c.PacketConn.Close()
} }
+36 -7
View File
@@ -23,14 +23,23 @@ const (
symmetricNATMaxPortsPerHost = 32 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 var network string
if local.IsUnspecified() { if family == Family_Dual {
network = "ip" if local.IsUnspecified() {
} else { network = "ip"
if local.To4() != nil {
network = "ip4"
} else { } else {
if local.To4() != nil {
network = "ip4"
} else {
network = "ip6"
}
}
} else {
if family == Family_V4 {
network = "ip4"
}
if family == Family_V6 {
network = "ip6" 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 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 var allow4, allow6 bool
for _, local := range locals { for _, local := range locals {
if local.Addr().Is4() { if local.Addr().Is4() {
@@ -116,6 +125,12 @@ func candidatePunchAddrs(locals, peers []netip.AddrPort) ([]netip.AddrPort, map[
break break
} }
} }
if family == Family_V4 {
allow6 = false
}
if family == Family_V6 {
allow4 = false
}
seen := make(map[netip.AddrPort]struct{}, len(peers)) seen := make(map[netip.AddrPort]struct{}, len(peers))
candidates := make([]netip.AddrPort, 0, len(peers)) candidates := make([]netip.AddrPort, 0, len(peers))
for _, peer := range peers { for _, peer := range peers {
@@ -218,3 +233,17 @@ func parseAddrPorts(addrs []string) ([]netip.AddrPort, error) {
} }
return out, nil 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)
}
+20 -11
View File
@@ -29,10 +29,11 @@ type Config struct {
MasqFile string `protobuf:"bytes,5,opt,name=masq_file,json=masqFile,proto3" json:"masq_file,omitempty"` 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"` 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"` 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"` MasqUrlXForwarded bool `protobuf:"varint,8,opt,name=masq_url_x_forwarded,json=masqUrlXForwarded,proto3" json:"masq_url_x_forwarded,omitempty"`
MasqString string `protobuf:"bytes,9,opt,name=masq_string,json=masqString,proto3" json:"masq_string,omitempty"` MasqUrlInsecure bool `protobuf:"varint,9,opt,name=masq_url_insecure,json=masqUrlInsecure,proto3" json:"masq_url_insecure,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"` MasqString string `protobuf:"bytes,10,opt,name=masq_string,json=masqString,proto3" json:"masq_string,omitempty"`
MasqStringStatusCode int32 `protobuf:"varint,11,opt,name=masq_string_status_code,json=masqStringStatusCode,proto3" json:"masq_string_status_code,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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -109,6 +110,13 @@ func (x *Config) GetMasqUrlRewriteHost() bool {
return false return false
} }
func (x *Config) GetMasqUrlXForwarded() bool {
if x != nil {
return x.MasqUrlXForwarded
}
return false
}
func (x *Config) GetMasqUrlInsecure() bool { func (x *Config) GetMasqUrlInsecure() bool {
if x != nil { if x != nil {
return x.MasqUrlInsecure return x.MasqUrlInsecure
@@ -141,20 +149,21 @@ var File_transport_internet_hysteria_config_proto protoreflect.FileDescriptor
const file_transport_internet_hysteria_config_proto_rawDesc = "" + const file_transport_internet_hysteria_config_proto_rawDesc = "" +
"\n" + "\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" + "\x06Config\x12\x12\n" +
"\x04auth\x18\x02 \x01(\tR\x04auth\x12(\n" + "\x04auth\x18\x02 \x01(\tR\x04auth\x12(\n" +
"\x10udp_idle_timeout\x18\x03 \x01(\x03R\x0eudpIdleTimeout\x12\x1b\n" + "\x10udp_idle_timeout\x18\x03 \x01(\x03R\x0eudpIdleTimeout\x12\x1b\n" +
"\tmasq_type\x18\x04 \x01(\tR\bmasqType\x12\x1b\n" + "\tmasq_type\x18\x04 \x01(\tR\bmasqType\x12\x1b\n" +
"\tmasq_file\x18\x05 \x01(\tR\bmasqFile\x12\x19\n" + "\tmasq_file\x18\x05 \x01(\tR\bmasqFile\x12\x19\n" +
"\bmasq_url\x18\x06 \x01(\tR\amasqUrl\x121\n" + "\bmasq_url\x18\x06 \x01(\tR\amasqUrl\x121\n" +
"\x15masq_url_rewrite_host\x18\a \x01(\bR\x12masqUrlRewriteHost\x12*\n" + "\x15masq_url_rewrite_host\x18\a \x01(\bR\x12masqUrlRewriteHost\x12/\n" +
"\x11masq_url_insecure\x18\b \x01(\bR\x0fmasqUrlInsecure\x12\x1f\n" + "\x14masq_url_x_forwarded\x18\b \x01(\bR\x11masqUrlXForwarded\x12*\n" +
"\vmasq_string\x18\t \x01(\tR\n" + "\x11masq_url_insecure\x18\t \x01(\bR\x0fmasqUrlInsecure\x12\x1f\n" +
"\vmasq_string\x18\n" +
" \x01(\tR\n" +
"masqString\x12o\n" + "masqString\x12o\n" +
"\x13masq_string_headers\x18\n" + "\x13masq_string_headers\x18\v \x03(\v2?.xray.transport.internet.hysteria.Config.MasqStringHeadersEntryR\x11masqStringHeaders\x125\n" +
" \x03(\v2?.xray.transport.internet.hysteria.Config.MasqStringHeadersEntryR\x11masqStringHeaders\x125\n" + "\x17masq_string_status_code\x18\f \x01(\x05R\x14masqStringStatusCode\x1aD\n" +
"\x17masq_string_status_code\x18\v \x01(\x05R\x14masqStringStatusCode\x1aD\n" +
"\x16MasqStringHeadersEntry\x12\x10\n" + "\x16MasqStringHeadersEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\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" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02B\x82\x01\n" +
+9 -4
View File
@@ -11,12 +11,17 @@ message Config {
string auth = 2; string auth = 2;
int64 udp_idle_timeout = 3; int64 udp_idle_timeout = 3;
string masq_type = 4; string masq_type = 4;
string masq_file = 5; string masq_file = 5;
string masq_url = 6; string masq_url = 6;
bool masq_url_rewrite_host = 7; bool masq_url_rewrite_host = 7;
bool masq_url_insecure = 8; bool masq_url_x_forwarded = 8;
string masq_string = 9; bool masq_url_insecure = 9;
map<string, string> masq_string_headers = 10;
int32 masq_string_status_code = 11; string masq_string = 10;
map<string, string> masq_string_headers = 11;
int32 masq_string_status_code = 12;
} }
@@ -33,6 +33,8 @@ type BrutalSender struct {
pktInfoSlots [pktInfoSlotCount]pktInfo pktInfoSlots [pktInfoSlotCount]pktInfo
ackRate float64 ackRate float64
disableLossCompensation bool
debug bool debug bool
lastAckPrintTimestamp int64 lastAckPrintTimestamp int64
} }
@@ -43,13 +45,14 @@ type pktInfo struct {
LossCount uint64 LossCount uint64
} }
func NewBrutalSender(bps uint64) *BrutalSender { func NewBrutalSender(bps uint64, disableLossCompensation bool) *BrutalSender {
debug, _ := strconv.ParseBool(os.Getenv(debugEnv)) debug, _ := strconv.ParseBool(os.Getenv(debugEnv))
bs := &BrutalSender{ bs := &BrutalSender{
bps: congestion.ByteCount(bps), bps: congestion.ByteCount(bps),
maxDatagramSize: congestion.InitialPacketSize, maxDatagramSize: congestion.InitialPacketSize,
ackRate: 1, ackRate: 1,
debug: debug, disableLossCompensation: disableLossCompensation,
debug: debug,
} }
bs.pacer = common.NewPacer(func() congestion.ByteCount { bs.pacer = common.NewPacer(func() congestion.ByteCount {
return congestion.ByteCount(float64(bs.bps) / bs.ackRate) 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) { func (b *BrutalSender) updateAckRate(currentTimestamp int64) {
if b.disableLossCompensation {
b.ackRate = 1
return
}
minTimestamp := currentTimestamp - pktInfoSlotCount minTimestamp := currentTimestamp - pktInfoSlotCount
var ackCount, lossCount uint64 var ackCount, lossCount uint64
for _, info := range b.pktInfoSlots { for _, info := range b.pktInfoSlots {
@@ -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)
}
})
}
}
@@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/apernet/quic-go" "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/bbr"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/brutal" "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) { func UseBBR(conn *quic.Conn, profile bbr.Profile) {
conn.SetCongestionControl(bbr.NewBbrSender( conn.SetCongestionControl(bbr.NewBbrSender(
bbr.DefaultClock{}, bbr.DefaultClock{},
bbr.GetInitialPacketSize(conn.RemoteAddr()), seedPacketSize(conn.InitialPacketSize(), bbr.GetInitialPacketSize(conn.RemoteAddr())),
profile, profile,
)) ))
} }
func UseBrutal(conn *quic.Conn, tx uint64) { // seedPacketSize picks the datagram size to seed a replacement congestion
conn.SetCongestionControl(brutal.NewBrutalSender(tx)) // 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) { func UseConfigured(conn *quic.Conn, congestionType, bbrProfile string) {
+10 -3
View File
@@ -90,6 +90,7 @@ func (c *client) dial(ctx context.Context) error {
MaxIdleTimeout: time.Duration(quicParams.MaxIdleTimeout) * time.Second, MaxIdleTimeout: time.Duration(quicParams.MaxIdleTimeout) * time.Second,
KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second, KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second,
DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"), DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"),
ChromeParrot: !quicParams.DisableChromeParrot,
EnableDatagrams: true, EnableDatagrams: true,
MaxDatagramFrameSize: MaxDatagramFrameSize, MaxDatagramFrameSize: MaxDatagramFrameSize,
OmitMaxDatagramFrameSize: time.Now().After(time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)), 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 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 var conn *quic.Conn
rt := &http3.Transport{ rt := &http3.Transport{
@@ -219,6 +225,7 @@ func (c *client) dial(ctx context.Context) error {
// udp, _ := strconv.ParseBool(resp.Header.Get(ResponseHeaderUDPEnabled)) // udp, _ := strconv.ParseBool(resp.Header.Get(ResponseHeaderUDPEnabled))
down, _ := strconv.ParseUint(resp.Header.Get(CommonHeaderCCRX), 10, 64) down, _ := strconv.ParseUint(resp.Header.Get(CommonHeaderCCRX), 10, 64)
errors.LogDebug(context.Background(), "ECHAccepted ", conn.ConnectionState().TLS.ECHAccepted)
switch quicParams.Congestion { switch quicParams.Congestion {
case "reno": case "reno":
@@ -228,10 +235,10 @@ func (c *client) dial(ctx context.Context) error {
if quicParams.BrutalUp == 0 || down == 0 { if quicParams.BrutalUp == 0 || down == 0 {
congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile)) congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile))
} else { } else {
congestion.UseBrutal(conn, min(quicParams.BrutalUp, down)) congestion.UseBrutal(conn, min(quicParams.BrutalUp, down), quicParams.BrutalDisableLossCompensation)
} }
case "force-brutal": case "force-brutal":
congestion.UseBrutal(conn, quicParams.BrutalUp) congestion.UseBrutal(conn, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation)
default: default:
panic(quicParams.Congestion) panic(quicParams.Congestion)
} }
+73 -12
View File
@@ -2,6 +2,7 @@ package hysteria
import ( import (
"context" "context"
"crypto/rand"
gotls "crypto/tls" gotls "crypto/tls"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@@ -78,10 +79,10 @@ func (h *httpHandler) AuthHTTP(w http.ResponseWriter, r *http.Request) bool {
if quicParams.BrutalUp == 0 || down == 0 { if quicParams.BrutalUp == 0 || down == 0 {
congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile)) congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile))
} else { } else {
congestion.UseBrutal(conn, min(quicParams.BrutalUp, down)) congestion.UseBrutal(conn, min(quicParams.BrutalUp, down), quicParams.BrutalDisableLossCompensation)
} }
case "force-brutal": case "force-brutal":
congestion.UseBrutal(conn, quicParams.BrutalUp) congestion.UseBrutal(conn, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation)
default: default:
panic(quicParams.Congestion) panic(quicParams.Congestion)
} }
@@ -165,8 +166,9 @@ func (l *Listener) handleClient(conn *quic.Conn) {
Handler: handler, Handler: handler,
StreamDispatcher: handler.StreamDispatcher, StreamDispatcher: handler.StreamDispatcher,
} }
_ = h3s.ServeQUICConn(conn) err := h3s.ServeQUICConn(conn)
_ = conn.CloseWithError(closeErrCodeOK, "") _ = conn.CloseWithError(closeErrCodeOK, "")
errors.LogDebug(context.Background(), conn.RemoteAddr(), " ServeQUICConn exited with ", err)
} }
func (l *Listener) keepAccepting() { func (l *Listener) keepAccepting() {
@@ -219,21 +221,43 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti
return nil, err return nil, err
} }
transport := http.DefaultTransport.(*http.Transport) transport := http.DefaultTransport.(*http.Transport)
if config.MasqUrlInsecure { switch u.Scheme {
transport = transport.Clone() case "http", "https":
transport.TLSClientConfig = &gotls.Config{ if config.MasqUrlInsecure {
InsecureSkipVerify: true, transport = transport.Clone()
if transport.TLSClientConfig == nil {
transport.TLSClientConfig = &gotls.Config{}
}
transport.TLSClientConfig.InsecureSkipVerify = true
} }
case "", "unix":
path := u.Path
u = &url.URL{Scheme: "http", Host: "localhost"}
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{ masqHandler = &httputil.ReverseProxy{
Rewrite: func(pr *httputil.ProxyRequest) { Rewrite: func(r *httputil.ProxyRequest) {
pr.SetURL(u) r.SetURL(u)
if !config.MasqUrlRewriteHost { 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) { ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
errors.LogErrorInner(context.Background(), err, "HTTP reverse proxy error")
w.WriteHeader(http.StatusBadGateway) w.WriteHeader(http.StatusBadGateway)
}, },
} }
@@ -307,7 +331,13 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti
pktConn = newConn 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) listener, err := tr.Listen(tlsConfig.GetTLSConfig(tls.WithNextProto("h3")), quicConfig)
if err != nil { if err != nil {
@@ -336,3 +366,34 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti
func init() { func init() {
common.Must(internet.RegisterTransportListener(protocolName, Listen)) 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])
}
+34 -33
View File
@@ -169,41 +169,42 @@ func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig)
return errors.New("failed to set TCP_MAXSEG", err) return errors.New("failed to set TCP_MAXSEG", err)
} }
} }
if len(config.CustomSockopt) > 0 { }
for _, custom := range config.CustomSockopt {
if custom.System != "" && custom.System != runtime.GOOS { if len(config.CustomSockopt) > 0 {
errors.LogDebug(context.Background(), "CustomSockopt system not match: ", "want ", custom.System, " got ", runtime.GOOS) for _, custom := range config.CustomSockopt {
continue if custom.System != "" && custom.System != runtime.GOOS {
errors.LogDebug(context.Background(), "CustomSockopt system not match: ", "want ", custom.System, " got ", runtime.GOOS)
continue
}
// Skip unwanted network type
// network might be tcp4 or tcp6
// use HasPrefix so that "tcp" can match tcp4/6 with "tcp" if user want to control all tcp (udp is also the same)
// if it is empty, strings.HasPrefix will always return true to make it apply for all networks
if !strings.HasPrefix(network, custom.Network) {
continue
}
level := 0x6 // default TCP
var opt int
if len(custom.Opt) == 0 {
return errors.New("No opt!")
} else {
opt, _ = strconv.Atoi(custom.Opt)
}
if custom.Level != "" {
level, _ = strconv.Atoi(custom.Level)
}
if custom.Type == "int" {
value, _ := strconv.Atoi(custom.Value)
if err := syscall.SetsockoptInt(int(fd), level, opt, value); err != nil {
return errors.New("failed to set CustomSockoptInt", opt, value, err)
} }
// Skip unwanted network type } else if custom.Type == "str" {
// network might be tcp4 or tcp6 if err := syscall.SetsockoptString(int(fd), level, opt, custom.Value); err != nil {
// use HasPrefix so that "tcp" can match tcp4/6 with "tcp" if user want to control all tcp (udp is also the same) return errors.New("failed to set CustomSockoptString", opt, custom.Value, err)
// if it is empty, strings.HasPrefix will always return true to make it apply for all networks
if !strings.HasPrefix(network, custom.Network) {
continue
}
level := 0x6 // default TCP
var opt int
if len(custom.Opt) == 0 {
return errors.New("No opt!")
} else {
opt, _ = strconv.Atoi(custom.Opt)
}
if custom.Level != "" {
level, _ = strconv.Atoi(custom.Level)
}
if custom.Type == "int" {
value, _ := strconv.Atoi(custom.Value)
if err := syscall.SetsockoptInt(int(fd), level, opt, value); err != nil {
return errors.New("failed to set CustomSockoptInt", opt, value, err)
}
} else if custom.Type == "str" {
if err := syscall.SetsockoptString(int(fd), level, opt, custom.Value); err != nil {
return errors.New("failed to set CustomSockoptString", opt, custom.Value, err)
}
} else {
return errors.New("unknown CustomSockopt type:", custom.Type)
} }
} else {
return errors.New("unknown CustomSockopt type:", custom.Type)
} }
} }
} }
+11 -4
View File
@@ -14,11 +14,18 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error { func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error {
if config.ReceiveOriginalDestAddress && isUDPSocket(network) { if config.ReceiveOriginalDestAddress && isUDPSocket(network) {
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVDSTADDR, 1); err != nil { // Only the options matching the socket's address family are accepted,
return errors.New("failed to set IP_RECVDSTADDR").Base(err) // so one of the two pairs succeeding is enough.
err6 := unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_RECVPKTINFO, 1)
if err6 == nil {
err6 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_RECVDSTPORT, 1)
} }
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVDSTPORT, 1); err != nil { err4 := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVDSTADDR, 1)
return errors.New("failed to set IP_RECVDSTPORT").Base(err) if err4 == nil {
err4 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVDSTPORT, 1)
}
if err4 != nil && err6 != nil {
return errors.New("failed to enable receiving the original destination").Base(err4)
} }
} }
return nil return nil
+23 -25
View File
@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"net/http/httptrace" "net/http/httptrace"
"sync" "sync"
"sync/atomic"
"github.com/apernet/quic-go/http3" "github.com/apernet/quic-go/http3"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
@@ -32,7 +33,7 @@ type DialerClient interface {
type DefaultDialerClient struct { type DefaultDialerClient struct {
transportConfig *Config transportConfig *Config
client *http.Client client *http.Client
closed bool closed atomic.Bool
httpVersion string httpVersion string
// pool of net.Conn, created using dialUploadConn // pool of net.Conn, created using dialUploadConn
uploadRawPool *sync.Pool uploadRawPool *sync.Pool
@@ -40,7 +41,7 @@ type DefaultDialerClient struct {
} }
func (c *DefaultDialerClient) IsClosed() bool { func (c *DefaultDialerClient) IsClosed() bool {
return c.closed return c.closed.Load()
} }
func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessionId string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) { func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessionId string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) {
@@ -67,12 +68,12 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessio
} }
c.transportConfig.FillStreamRequest(req, sessionId, "") c.transportConfig.FillStreamRequest(req, sessionId, "")
wrc = &WaitReadCloser{Wait: make(chan struct{})} wrc = &WaitReadCloser{wait: done.New()}
go func() { go func() {
resp, err := c.client.Do(req) resp, err := c.client.Do(req)
if err != nil { if err != nil {
if !uploadOnly { // stream-down is enough if !uploadOnly { // stream-down is enough
c.closed = true c.closed.Store(true)
errors.LogInfoInner(ctx, err, "failed to "+method+" "+url) errors.LogInfoInner(ctx, err, "failed to "+method+" "+url)
} }
gotConn.Close() gotConn.Close()
@@ -108,7 +109,7 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessio
if c.httpVersion != "1.1" { if c.httpVersion != "1.1" {
resp, err := c.client.Do(req) resp, err := c.client.Do(req)
if err != nil { if err != nil {
c.closed = true c.closed.Store(true)
return err return err
} }
@@ -148,7 +149,7 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessio
if h1UploadConn.UnreadedResponsesCount > 0 { if h1UploadConn.UnreadedResponsesCount > 0 {
resp, err := http.ReadResponse(h1UploadConn.RespBufReader, req) resp, err := http.ReadResponse(h1UploadConn.RespBufReader, req)
if err != nil { if err != nil {
c.closed = true c.closed.Store(true)
return fmt.Errorf("error while reading response: %s", err.Error()) return fmt.Errorf("error while reading response: %s", err.Error())
} }
io.Copy(io.Discard, resp.Body) io.Copy(io.Discard, resp.Body)
@@ -187,38 +188,35 @@ func (c *DefaultDialerClient) Close() error {
} }
type WaitReadCloser struct { type WaitReadCloser struct {
Wait chan struct{} wait *done.Instance
io.ReadCloser reader atomic.Pointer[io.ReadCloser]
} }
func (w *WaitReadCloser) Set(rc io.ReadCloser) { func (w *WaitReadCloser) Set(rc io.ReadCloser) {
w.ReadCloser = rc w.reader.Store(&rc)
defer func() { if w.wait.Done() {
if recover() != nil { if p := w.reader.Swap(nil); p != nil {
rc.Close() (*p).Close()
} }
}() }
close(w.Wait) w.wait.Close()
} }
func (w *WaitReadCloser) Read(b []byte) (int, error) { func (w *WaitReadCloser) Read(b []byte) (int, error) {
if w.ReadCloser == nil { rc := w.reader.Load()
if <-w.Wait; w.ReadCloser == nil { if rc == nil {
<-w.wait.Wait()
if rc = w.reader.Load(); rc == nil {
return 0, io.ErrClosedPipe return 0, io.ErrClosedPipe
} }
} }
return w.ReadCloser.Read(b) return (*rc).Read(b)
} }
func (w *WaitReadCloser) Close() error { func (w *WaitReadCloser) Close() error {
if w.ReadCloser != nil { w.wait.Close()
return w.ReadCloser.Close() if p := w.reader.Swap(nil); p != nil {
return (*p).Close()
} }
defer func() {
if recover() != nil && w.ReadCloser != nil {
w.ReadCloser.Close()
}
}()
close(w.Wait)
return nil return nil
} }
+10 -5
View File
@@ -1,6 +1,7 @@
package splithttp package splithttp
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io" "io"
@@ -330,14 +331,18 @@ func (c *Config) FillStreamRequest(request *http.Request, sessionId string, seqS
func (c *Config) FillPacketRequest(request *http.Request, sessionId string, seqStr string, payload buf.MultiBuffer) error { func (c *Config) FillPacketRequest(request *http.Request, sessionId string, seqStr string, payload buf.MultiBuffer) error {
dataPlacement := c.GetNormalizedUplinkDataPlacement() dataPlacement := c.GetNormalizedUplinkDataPlacement()
data := make([]byte, payload.Len())
payload.Copy(data)
buf.ReleaseMulti(payload)
if dataPlacement == PlacementBody || dataPlacement == PlacementAuto { if dataPlacement == PlacementBody || dataPlacement == PlacementAuto {
request.Header = c.GetRequestHeader() request.Header = c.GetRequestHeader()
request.Body = io.NopCloser(&buf.MultiBufferContainer{MultiBuffer: payload}) request.Body = io.NopCloser(bytes.NewReader(data))
request.ContentLength = int64(payload.Len()) request.ContentLength = int64(len(data))
request.GetBody = func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(data)), nil
}
} else { } else {
data := make([]byte, payload.Len())
payload.Copy(data)
buf.ReleaseMulti(payload)
switch dataPlacement { switch dataPlacement {
case PlacementHeader: case PlacementHeader:
request.Header = c.GetRequestHeaderWithPayload(data) request.Header = c.GetRequestHeaderWithPayload(data)
@@ -1,9 +1,13 @@
package splithttp_test package splithttp_test
import ( import (
"io"
"net/http"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
. "github.com/xtls/xray-core/transport/internet/splithttp" . "github.com/xtls/xray-core/transport/internet/splithttp"
) )
@@ -77,3 +81,35 @@ func Test_GetNormalizedPath(t *testing.T) {
}) })
} }
} }
func Test_FillPacketRequest_GetBody(t *testing.T) {
data := []byte("hello xray")
payload := buf.MergeBytes(nil, data)
req, err := http.NewRequest("POST", "https://example.com/", nil)
common.Must(err)
config := &Config{}
config.FillPacketRequest(req, "sess", "0", payload)
if req.GetBody == nil {
t.Fatalf("Expected GetBody to be set")
}
first, err := io.ReadAll(req.Body)
common.Must(err)
if string(data) != string(first) {
t.Fatalf("Body mismatch. Format %q and %q are not equal", data, first)
}
body2, err := req.GetBody()
common.Must(err)
second, err := io.ReadAll(body2)
common.Must(err)
if string(data) != string(second) {
t.Fatalf("Replayed body mismatch. Format %q and %q are not equal", data, second)
}
}
+13 -4
View File
@@ -175,6 +175,7 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second, KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second,
MaxIncomingStreams: quicParams.MaxIncomingStreams, MaxIncomingStreams: quicParams.MaxIncomingStreams,
DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"), DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"),
ChromeParrot: !quicParams.DisableChromeParrot,
} }
if quicParams.MaxIdleTimeout == 0 { if quicParams.MaxIdleTimeout == 0 {
quicConfig.MaxIdleTimeout = net.ConnIdleTimeout quicConfig.MaxIdleTimeout = net.ConnIdleTimeout
@@ -255,18 +256,25 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
pktConn = newConn 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 { if err != nil {
return nil, err return nil, err
} }
context.AfterFunc(conn.Context(), func() { pktConn.Close() }) context.AfterFunc(conn.Context(), func() { tr.Close(); pktConn.Close() })
switch quicParams.Congestion { switch quicParams.Congestion {
case "reno": case "reno":
case "", "bbr": case "", "bbr":
congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile)) congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile))
case "force-brutal": case "force-brutal":
congestion.UseBrutal(conn, quicParams.BrutalUp) congestion.UseBrutal(conn, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation)
default: default:
panic(quicParams.Congestion) panic(quicParams.Congestion)
} }
@@ -595,11 +603,12 @@ func (w uploadWriter) Write(b []byte) (int, error) {
var writed int var writed int
for _, buff := range buffer.MultiBuffer { for _, buff := range buffer.MultiBuffer {
n := int(buff.Len())
err := w.WriteMultiBuffer(buf.MultiBuffer{buff}) err := w.WriteMultiBuffer(buf.MultiBuffer{buff})
if err != nil { if err != nil {
return writed, err return writed, err
} }
writed += int(buff.Len()) writed += n
} }
return writed, nil return writed, nil
} }
+13 -2
View File
@@ -3,6 +3,7 @@ package splithttp
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/rand"
gotls "crypto/tls" gotls "crypto/tls"
"encoding/base64" "encoding/base64"
"fmt" "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"), 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 { if err != nil {
return nil, errors.New("failed to listen QUIC for XHTTP/3 on ", address, ":", port).Base(err) 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 { if err := l.h3server.ServeListener(l.h3listener); err != nil {
errors.LogErrorInner(ctx, err, "failed to serve HTTP/3 for XHTTP/3") errors.LogErrorInner(ctx, err, "failed to serve HTTP/3 for XHTTP/3")
} }
_ = tr.Close()
_ = Conn.Close()
}() }()
} else { // tcp } else { // tcp
l.listener, err = internet.ListenSystem(ctx, &net.TCPAddr{ l.listener, err = internet.ListenSystem(ctx, &net.TCPAddr{
@@ -622,7 +633,7 @@ func (l *QListener) Accept(ctx context.Context) (*quic.Conn, error) {
case "", "bbr": case "", "bbr":
congestion.UseBBR(conn, bbr.Profile(l.quicParams.BbrProfile)) congestion.UseBBR(conn, bbr.Profile(l.quicParams.BbrProfile))
case "force-brutal": case "force-brutal":
congestion.UseBrutal(conn, l.quicParams.BrutalUp) congestion.UseBrutal(conn, l.quicParams.BrutalUp, l.quicParams.BrutalDisableLossCompensation)
default: default:
panic(l.quicParams.Congestion) panic(l.quicParams.Congestion)
} }
+14 -8
View File
@@ -50,18 +50,24 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
errors.LogDebug(ctx, "dialing to "+dest.String()) errors.LogDebug(ctx, "dialing to "+dest.String())
if dest.Network == net.Network_UDP { if dest.Network == net.Network_UDP {
srcAddr := resolveSrcAddr(net.Network_UDP, src)
if srcAddr == nil {
srcAddr = &net.UDPAddr{
IP: []byte{0, 0, 0, 0},
Port: 0,
}
}
var lc net.ListenConfig
destAddr, err := net.ResolveUDPAddr("udp", dest.NetAddr()) destAddr, err := net.ResolveUDPAddr("udp", dest.NetAddr())
if err != nil { if err != nil {
return nil, err return nil, err
} }
srcAddr := resolveSrcAddr(net.Network_UDP, src)
if srcAddr == nil {
// some OS don't support mapped IPv4 dual stack
// and need to select 0.0.0.0 or [::] manually based on the destination
wildcard := net.AnyIP.IP()
if destAddr.IP.To4() == nil {
wildcard = net.AnyIPv6.IP()
}
srcAddr = &net.UDPAddr{
IP: wildcard,
Port: 0,
}
}
var lc net.ListenConfig
lc.Control = func(network, address string, c syscall.RawConn) error { lc.Control = func(network, address string, c syscall.RawConn) error {
for _, ctl := range Controllers { for _, ctl := range Controllers {
if err := ctl(network, address, c); err != nil { if err := ctl(network, address, c); err != nil {
-3
View File
@@ -42,9 +42,6 @@ func ListenTCP(ctx context.Context, address net.Address, port net.Port, streamSe
var listener net.Listener var listener net.Listener
var err error var err error
if port == net.Port(0) { // unix if port == net.Port(0) { // unix
if !address.Family().IsDomain() {
return nil, errors.New("invalid unix listen: ", address).AtError()
}
listener, err = internet.ListenSystem(ctx, &net.UnixAddr{ listener, err = internet.ListenSystem(ctx, &net.UnixAddr{
Name: address.Domain(), Name: address.Domain(),
Net: "unix", Net: "unix",
+3
View File
@@ -65,6 +65,9 @@ func ListenTCP(ctx context.Context, address net.Address, port net.Port, settings
if address.Family().IsDomain() { if address.Family().IsDomain() {
return nil, errors.New("domain address is not allowed for listening: ", address.Domain()) return nil, errors.New("domain address is not allowed for listening: ", address.Domain())
} }
if port == 0 {
return nil, errors.New("port 0 is not allowed for listening on TCP")
}
protocol := settings.ProtocolName protocol := settings.ProtocolName
listenFunc := transportListenerCache[protocol] listenFunc := transportListenerCache[protocol]
+31 -15
View File
@@ -17,23 +17,39 @@ func retrieveOriginalDestFromControlMessages(msgs []unix.SocketControlMessage) n
var havePort bool var havePort bool
for _, msg := range msgs { for _, msg := range msgs {
if msg.Header.Level != unix.IPPROTO_IP { switch msg.Header.Level {
continue case unix.IPPROTO_IP:
} switch msg.Header.Type {
case unix.IP_RECVDSTADDR:
switch msg.Header.Type { if len(msg.Data) < 4 {
case unix.IP_RECVDSTADDR: continue
if len(msg.Data) < 4 { }
continue ip = append(ip[:0], msg.Data[:4]...)
haveAddress = true
case unix.IP_RECVDSTPORT:
if len(msg.Data) < 2 {
continue
}
port = binary.BigEndian.Uint16(msg.Data[:2])
havePort = true
} }
ip = append(ip[:0], msg.Data[:4]...) case unix.IPPROTO_IPV6:
haveAddress = true switch msg.Header.Type {
case unix.IP_RECVDSTPORT: case unix.IPV6_PKTINFO:
if len(msg.Data) < 2 { // struct in6_pktinfo: the destination address is followed by
continue // the interface index.
if len(msg.Data) < 16 {
continue
}
ip = append(ip[:0], msg.Data[:16]...)
haveAddress = true
case unix.IPV6_RECVDSTPORT:
if len(msg.Data) < 2 {
continue
}
port = binary.BigEndian.Uint16(msg.Data[:2])
havePort = true
} }
port = binary.BigEndian.Uint16(msg.Data[:2])
havePort = true
} }
} }
@@ -33,6 +33,36 @@ func TestRetrieveOriginalDestFromControlMessages(t *testing.T) {
} }
} }
func TestRetrieveOriginalDestFromControlMessagesIPv6(t *testing.T) {
// 2001:db8::7 followed by the interface index, as in struct in6_pktinfo.
pktinfo := []byte{
0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0x07,
0x02, 0, 0, 0,
}
msgs := []unix.SocketControlMessage{
{
Header: unix.Cmsghdr{Level: unix.IPPROTO_IPV6, Type: unix.IPV6_RECVDSTPORT},
Data: []byte{0x30, 0x39},
},
{
Header: unix.Cmsghdr{Level: unix.IPPROTO_IPV6, Type: unix.IPV6_PKTINFO},
Data: pktinfo,
},
}
dest := retrieveOriginalDestFromControlMessages(msgs)
if !dest.IsValid() {
t.Fatal("destination is invalid")
}
if got, want := dest.Address.IP().String(), "2001:db8::7"; got != want {
t.Fatalf("address = %q, want %q", got, want)
}
if got, want := dest.Port.Value(), uint16(12345); got != want {
t.Fatalf("port = %d, want %d", got, want)
}
}
func TestRetrieveOriginalDestRequiresAddressAndPort(t *testing.T) { func TestRetrieveOriginalDestRequiresAddressAndPort(t *testing.T) {
tests := [][]unix.SocketControlMessage{ tests := [][]unix.SocketControlMessage{
{ {
@@ -47,6 +77,18 @@ func TestRetrieveOriginalDestRequiresAddressAndPort(t *testing.T) {
Data: []byte{0x30, 0x39}, Data: []byte{0x30, 0x39},
}, },
}, },
{
{
Header: unix.Cmsghdr{Level: unix.IPPROTO_IPV6, Type: unix.IPV6_PKTINFO},
Data: make([]byte, 20),
},
},
{
{
Header: unix.Cmsghdr{Level: unix.IPPROTO_IPV6, Type: unix.IPV6_RECVDSTPORT},
Data: []byte{0x30, 0x39},
},
},
} }
for _, msgs := range tests { for _, msgs := range tests {
+19
View File
@@ -175,6 +175,25 @@ type delayDialConn struct {
streamSettings *internet.MemoryStreamConfig streamSettings *internet.MemoryStreamConfig
} }
// LocalAddr returns nil until the deferred WebSocket dial has completed.
// Without this method, Go promotes LocalAddr from the embedded net.Conn; the
// embedded interface is nil before the first Write, so the promoted call panics.
func (d *delayDialConn) LocalAddr() net.Addr {
if d.Conn == nil {
return nil
}
return d.Conn.LocalAddr()
}
// RemoteAddr returns nil until the deferred WebSocket dial has completed.
// See LocalAddr for why an explicit method is required here.
func (d *delayDialConn) RemoteAddr() net.Addr {
if d.Conn == nil {
return nil
}
return d.Conn.RemoteAddr()
}
func (d *delayDialConn) Write(b []byte) (int, error) { func (d *delayDialConn) Write(b []byte) (int, error) {
if d.closed { if d.closed {
return 0, io.ErrClosedPipe return 0, io.ErrClosedPipe