From 4be238a5f32a6f9709eb17bf68d5afb878759708 Mon Sep 17 00:00:00 2001 From: patterniha Date: Tue, 29 Apr 2025 14:39:02 +0330 Subject: [PATCH] New feature: Happy Eyeballs (RFC 8305) --- infra/conf/transport_internet.go | 61 +++++-- infra/conf/transport_test.go | 31 ++-- transport/internet/config.pb.go | 241 +++++++++++++++++++-------- transport/internet/config.proto | 10 ++ transport/internet/dialer.go | 4 +- transport/internet/happy_eyeballs.go | 165 ++++++++++++++++++ 6 files changed, 413 insertions(+), 99 deletions(-) create mode 100644 transport/internet/happy_eyeballs.go diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index b4ae0801..fe6ceab4 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -699,25 +699,34 @@ type CustomSockoptConfig struct { Type string `json:"type"` } +type HappyEyeballsConfig struct { + Enabled bool `json:"enabled"` + PrioritizeIPv6 bool `json:"prioritizeIPv6"` + TryDelayMs uint64 `json:"tryDelayMs"` + Interleave uint32 `json:"interleave"` + MaxConcurrentTry uint32 `json:"maxConcurrentTry"` +} + type SocketConfig struct { - Mark int32 `json:"mark"` - TFO interface{} `json:"tcpFastOpen"` - TProxy string `json:"tproxy"` - AcceptProxyProtocol bool `json:"acceptProxyProtocol"` - DomainStrategy string `json:"domainStrategy"` - DialerProxy string `json:"dialerProxy"` - TCPKeepAliveInterval int32 `json:"tcpKeepAliveInterval"` - TCPKeepAliveIdle int32 `json:"tcpKeepAliveIdle"` - TCPCongestion string `json:"tcpCongestion"` - TCPWindowClamp int32 `json:"tcpWindowClamp"` - TCPMaxSeg int32 `json:"tcpMaxSeg"` - Penetrate bool `json:"penetrate"` - TCPUserTimeout int32 `json:"tcpUserTimeout"` - V6only bool `json:"v6only"` - Interface string `json:"interface"` - TcpMptcp bool `json:"tcpMptcp"` - CustomSockopt []*CustomSockoptConfig `json:"customSockopt"` - AddressPortStrategy string `json:"addressPortStrategy"` + Mark int32 `json:"mark"` + TFO interface{} `json:"tcpFastOpen"` + TProxy string `json:"tproxy"` + AcceptProxyProtocol bool `json:"acceptProxyProtocol"` + DomainStrategy string `json:"domainStrategy"` + DialerProxy string `json:"dialerProxy"` + TCPKeepAliveInterval int32 `json:"tcpKeepAliveInterval"` + TCPKeepAliveIdle int32 `json:"tcpKeepAliveIdle"` + TCPCongestion string `json:"tcpCongestion"` + TCPWindowClamp int32 `json:"tcpWindowClamp"` + TCPMaxSeg int32 `json:"tcpMaxSeg"` + Penetrate bool `json:"penetrate"` + TCPUserTimeout int32 `json:"tcpUserTimeout"` + V6only bool `json:"v6only"` + Interface string `json:"interface"` + TcpMptcp bool `json:"tcpMptcp"` + CustomSockopt []*CustomSockoptConfig `json:"customSockopt"` + AddressPortStrategy string `json:"addressPortStrategy"` + HappyEyeballsSettings *HappyEyeballsConfig `json:"happyEyeballs"` } // Build implements Buildable. @@ -809,6 +818,21 @@ func (c *SocketConfig) Build() (*internet.SocketConfig, error) { return nil, errors.New("unsupported address and port strategy: ", c.AddressPortStrategy) } + var happyEyeballs = &internet.HappyEyeballsConfig{Enabled: false, Interleave: 1, PrioritizeIpv6: false, TryDelayMs: 250, MaxConcurrentTry: 4} + if c.HappyEyeballsSettings != nil { + happyEyeballs.Enabled = c.HappyEyeballsSettings.Enabled + happyEyeballs.PrioritizeIpv6 = c.HappyEyeballsSettings.PrioritizeIPv6 + if c.HappyEyeballsSettings.Interleave > 0 { + happyEyeballs.Interleave = c.HappyEyeballsSettings.Interleave + } + if c.HappyEyeballsSettings.TryDelayMs > 0 { + happyEyeballs.TryDelayMs = c.HappyEyeballsSettings.TryDelayMs + } + if c.HappyEyeballsSettings.MaxConcurrentTry > 0 { + happyEyeballs.MaxConcurrentTry = c.HappyEyeballsSettings.MaxConcurrentTry + } + } + return &internet.SocketConfig{ Mark: c.Mark, Tfo: tfo, @@ -828,6 +852,7 @@ func (c *SocketConfig) Build() (*internet.SocketConfig, error) { TcpMptcp: c.TcpMptcp, CustomSockopt: customSockopts, AddressPortStrategy: addressPortStrategy, + HappyEyeballs: happyEyeballs, }, nil } diff --git a/infra/conf/transport_test.go b/infra/conf/transport_test.go index e7470082..56c20b12 100644 --- a/infra/conf/transport_test.go +++ b/infra/conf/transport_test.go @@ -26,6 +26,7 @@ func TestSocketConfig(t *testing.T) { Tfo: 256, DomainStrategy: internet.DomainStrategy_USE_IP, DialerProxy: "tag", + HappyEyeballs: &internet.HappyEyeballsConfig{Enabled: false, Interleave: 1, TryDelayMs: 250, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { @@ -45,8 +46,9 @@ func TestSocketConfig(t *testing.T) { // test "tcpFastOpen": false, disabled TFO is expected expectedOutput = &internet.SocketConfig{ - Mark: 0, - Tfo: -1, + Mark: 0, + Tfo: -1, + HappyEyeballs: &internet.HappyEyeballsConfig{Enabled: false, Interleave: 1, TryDelayMs: 250, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { @@ -63,8 +65,9 @@ func TestSocketConfig(t *testing.T) { // test "tcpFastOpen": 65535, queue length 65535 is expected expectedOutput = &internet.SocketConfig{ - Mark: 0, - Tfo: 65535, + Mark: 0, + Tfo: 65535, + HappyEyeballs: &internet.HappyEyeballsConfig{Enabled: false, Interleave: 1, TryDelayMs: 250, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { @@ -81,8 +84,9 @@ func TestSocketConfig(t *testing.T) { // test "tcpFastOpen": -65535, disable TFO is expected expectedOutput = &internet.SocketConfig{ - Mark: 0, - Tfo: -65535, + Mark: 0, + Tfo: -65535, + HappyEyeballs: &internet.HappyEyeballsConfig{Enabled: false, Interleave: 1, TryDelayMs: 250, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { @@ -99,8 +103,9 @@ func TestSocketConfig(t *testing.T) { // test "tcpFastOpen": 0, no operation is expected expectedOutput = &internet.SocketConfig{ - Mark: 0, - Tfo: 0, + Mark: 0, + Tfo: 0, + HappyEyeballs: &internet.HappyEyeballsConfig{Enabled: false, Interleave: 1, TryDelayMs: 250, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { @@ -117,8 +122,9 @@ func TestSocketConfig(t *testing.T) { // test omit "tcpFastOpen", no operation is expected expectedOutput = &internet.SocketConfig{ - Mark: 0, - Tfo: 0, + Mark: 0, + Tfo: 0, + HappyEyeballs: &internet.HappyEyeballsConfig{Enabled: false, Interleave: 1, TryDelayMs: 250, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { @@ -133,8 +139,9 @@ func TestSocketConfig(t *testing.T) { // test "tcpFastOpen": null, no operation is expected expectedOutput = &internet.SocketConfig{ - Mark: 0, - Tfo: 0, + Mark: 0, + Tfo: 0, + HappyEyeballs: &internet.HappyEyeballsConfig{Enabled: false, Interleave: 1, TryDelayMs: 250, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { diff --git a/transport/internet/config.pb.go b/transport/internet/config.pb.go index 6aa11b3e..31fccde1 100644 --- a/transport/internet/config.pb.go +++ b/transport/internet/config.pb.go @@ -511,24 +511,25 @@ type SocketConfig struct { Tproxy SocketConfig_TProxyMode `protobuf:"varint,3,opt,name=tproxy,proto3,enum=xray.transport.internet.SocketConfig_TProxyMode" json:"tproxy,omitempty"` // ReceiveOriginalDestAddress is for enabling IP_RECVORIGDSTADDR socket // option. This option is for UDP only. - ReceiveOriginalDestAddress bool `protobuf:"varint,4,opt,name=receive_original_dest_address,json=receiveOriginalDestAddress,proto3" json:"receive_original_dest_address,omitempty"` - BindAddress []byte `protobuf:"bytes,5,opt,name=bind_address,json=bindAddress,proto3" json:"bind_address,omitempty"` - BindPort uint32 `protobuf:"varint,6,opt,name=bind_port,json=bindPort,proto3" json:"bind_port,omitempty"` - AcceptProxyProtocol bool `protobuf:"varint,7,opt,name=accept_proxy_protocol,json=acceptProxyProtocol,proto3" json:"accept_proxy_protocol,omitempty"` - DomainStrategy DomainStrategy `protobuf:"varint,8,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"domain_strategy,omitempty"` - DialerProxy string `protobuf:"bytes,9,opt,name=dialer_proxy,json=dialerProxy,proto3" json:"dialer_proxy,omitempty"` - TcpKeepAliveInterval int32 `protobuf:"varint,10,opt,name=tcp_keep_alive_interval,json=tcpKeepAliveInterval,proto3" json:"tcp_keep_alive_interval,omitempty"` - TcpKeepAliveIdle int32 `protobuf:"varint,11,opt,name=tcp_keep_alive_idle,json=tcpKeepAliveIdle,proto3" json:"tcp_keep_alive_idle,omitempty"` - TcpCongestion string `protobuf:"bytes,12,opt,name=tcp_congestion,json=tcpCongestion,proto3" json:"tcp_congestion,omitempty"` - Interface string `protobuf:"bytes,13,opt,name=interface,proto3" json:"interface,omitempty"` - V6Only bool `protobuf:"varint,14,opt,name=v6only,proto3" json:"v6only,omitempty"` - TcpWindowClamp int32 `protobuf:"varint,15,opt,name=tcp_window_clamp,json=tcpWindowClamp,proto3" json:"tcp_window_clamp,omitempty"` - TcpUserTimeout int32 `protobuf:"varint,16,opt,name=tcp_user_timeout,json=tcpUserTimeout,proto3" json:"tcp_user_timeout,omitempty"` - TcpMaxSeg int32 `protobuf:"varint,17,opt,name=tcp_max_seg,json=tcpMaxSeg,proto3" json:"tcp_max_seg,omitempty"` - Penetrate bool `protobuf:"varint,18,opt,name=penetrate,proto3" json:"penetrate,omitempty"` - TcpMptcp bool `protobuf:"varint,19,opt,name=tcp_mptcp,json=tcpMptcp,proto3" json:"tcp_mptcp,omitempty"` - CustomSockopt []*CustomSockopt `protobuf:"bytes,20,rep,name=customSockopt,proto3" json:"customSockopt,omitempty"` - AddressPortStrategy AddressPortStrategy `protobuf:"varint,21,opt,name=address_port_strategy,json=addressPortStrategy,proto3,enum=xray.transport.internet.AddressPortStrategy" json:"address_port_strategy,omitempty"` + ReceiveOriginalDestAddress bool `protobuf:"varint,4,opt,name=receive_original_dest_address,json=receiveOriginalDestAddress,proto3" json:"receive_original_dest_address,omitempty"` + BindAddress []byte `protobuf:"bytes,5,opt,name=bind_address,json=bindAddress,proto3" json:"bind_address,omitempty"` + BindPort uint32 `protobuf:"varint,6,opt,name=bind_port,json=bindPort,proto3" json:"bind_port,omitempty"` + AcceptProxyProtocol bool `protobuf:"varint,7,opt,name=accept_proxy_protocol,json=acceptProxyProtocol,proto3" json:"accept_proxy_protocol,omitempty"` + DomainStrategy DomainStrategy `protobuf:"varint,8,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"domain_strategy,omitempty"` + DialerProxy string `protobuf:"bytes,9,opt,name=dialer_proxy,json=dialerProxy,proto3" json:"dialer_proxy,omitempty"` + TcpKeepAliveInterval int32 `protobuf:"varint,10,opt,name=tcp_keep_alive_interval,json=tcpKeepAliveInterval,proto3" json:"tcp_keep_alive_interval,omitempty"` + TcpKeepAliveIdle int32 `protobuf:"varint,11,opt,name=tcp_keep_alive_idle,json=tcpKeepAliveIdle,proto3" json:"tcp_keep_alive_idle,omitempty"` + TcpCongestion string `protobuf:"bytes,12,opt,name=tcp_congestion,json=tcpCongestion,proto3" json:"tcp_congestion,omitempty"` + Interface string `protobuf:"bytes,13,opt,name=interface,proto3" json:"interface,omitempty"` + V6Only bool `protobuf:"varint,14,opt,name=v6only,proto3" json:"v6only,omitempty"` + TcpWindowClamp int32 `protobuf:"varint,15,opt,name=tcp_window_clamp,json=tcpWindowClamp,proto3" json:"tcp_window_clamp,omitempty"` + TcpUserTimeout int32 `protobuf:"varint,16,opt,name=tcp_user_timeout,json=tcpUserTimeout,proto3" json:"tcp_user_timeout,omitempty"` + TcpMaxSeg int32 `protobuf:"varint,17,opt,name=tcp_max_seg,json=tcpMaxSeg,proto3" json:"tcp_max_seg,omitempty"` + Penetrate bool `protobuf:"varint,18,opt,name=penetrate,proto3" json:"penetrate,omitempty"` + TcpMptcp bool `protobuf:"varint,19,opt,name=tcp_mptcp,json=tcpMptcp,proto3" json:"tcp_mptcp,omitempty"` + CustomSockopt []*CustomSockopt `protobuf:"bytes,20,rep,name=customSockopt,proto3" json:"customSockopt,omitempty"` + AddressPortStrategy AddressPortStrategy `protobuf:"varint,21,opt,name=address_port_strategy,json=addressPortStrategy,proto3,enum=xray.transport.internet.AddressPortStrategy" json:"address_port_strategy,omitempty"` + HappyEyeballs *HappyEyeballsConfig `protobuf:"bytes,22,opt,name=happy_eyeballs,json=happyEyeballs,proto3" json:"happy_eyeballs,omitempty"` } func (x *SocketConfig) Reset() { @@ -708,6 +709,90 @@ func (x *SocketConfig) GetAddressPortStrategy() AddressPortStrategy { return AddressPortStrategy_None } +func (x *SocketConfig) GetHappyEyeballs() *HappyEyeballsConfig { + if x != nil { + return x.HappyEyeballs + } + return nil +} + +type HappyEyeballsConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + PrioritizeIpv6 bool `protobuf:"varint,2,opt,name=prioritize_ipv6,json=prioritizeIpv6,proto3" json:"prioritize_ipv6,omitempty"` + Interleave uint32 `protobuf:"varint,3,opt,name=interleave,proto3" json:"interleave,omitempty"` + TryDelayMs uint64 `protobuf:"varint,4,opt,name=try_delayMs,json=tryDelayMs,proto3" json:"try_delayMs,omitempty"` + MaxConcurrentTry uint32 `protobuf:"varint,5,opt,name=max_concurrent_try,json=maxConcurrentTry,proto3" json:"max_concurrent_try,omitempty"` +} + +func (x *HappyEyeballsConfig) Reset() { + *x = HappyEyeballsConfig{} + mi := &file_transport_internet_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HappyEyeballsConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HappyEyeballsConfig) ProtoMessage() {} + +func (x *HappyEyeballsConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_config_proto_msgTypes[5] + 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 HappyEyeballsConfig.ProtoReflect.Descriptor instead. +func (*HappyEyeballsConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{5} +} + +func (x *HappyEyeballsConfig) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *HappyEyeballsConfig) GetPrioritizeIpv6() bool { + if x != nil { + return x.PrioritizeIpv6 + } + return false +} + +func (x *HappyEyeballsConfig) GetInterleave() uint32 { + if x != nil { + return x.Interleave + } + return 0 +} + +func (x *HappyEyeballsConfig) GetTryDelayMs() uint64 { + if x != nil { + return x.TryDelayMs + } + return 0 +} + +func (x *HappyEyeballsConfig) GetMaxConcurrentTry() uint32 { + if x != nil { + return x.MaxConcurrentTry + } + return 0 +} + var File_transport_internet_config_proto protoreflect.FileDescriptor var file_transport_internet_config_proto_rawDesc = []byte{ @@ -766,7 +851,7 @@ var file_transport_internet_config_proto_rawDesc = []byte{ 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6f, 0x70, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x22, 0xfd, 0x07, 0x0a, 0x0c, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, + 0x74, 0x79, 0x70, 0x65, 0x22, 0xd2, 0x08, 0x0a, 0x0c, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x61, 0x72, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x6d, 0x61, 0x72, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x74, 0x66, 0x6f, 0x12, 0x48, 0x0a, 0x06, 0x74, @@ -827,37 +912,55 @@ var file_transport_internet_config_proto_rawDesc = []byte{ 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x50, 0x6f, 0x72, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x13, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x50, 0x6f, 0x72, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, - 0x65, 0x67, 0x79, 0x22, 0x2f, 0x0a, 0x0a, 0x54, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, - 0x65, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x66, 0x66, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x54, 0x50, - 0x72, 0x6f, 0x78, 0x79, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, - 0x63, 0x74, 0x10, 0x02, 0x2a, 0xa9, 0x01, 0x0a, 0x0e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, - 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x53, 0x5f, 0x49, 0x53, - 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x10, 0x01, 0x12, 0x0b, - 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x34, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x55, - 0x53, 0x45, 0x5f, 0x49, 0x50, 0x36, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x55, 0x53, 0x45, 0x5f, - 0x49, 0x50, 0x34, 0x36, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, - 0x36, 0x34, 0x10, 0x05, 0x12, 0x0c, 0x0a, 0x08, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x5f, 0x49, 0x50, - 0x10, 0x06, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x5f, 0x49, 0x50, 0x34, 0x10, - 0x07, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x5f, 0x49, 0x50, 0x36, 0x10, 0x08, - 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x5f, 0x49, 0x50, 0x34, 0x36, 0x10, 0x09, - 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x5f, 0x49, 0x50, 0x36, 0x34, 0x10, 0x0a, - 0x2a, 0x97, 0x01, 0x0a, 0x13, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x50, 0x6f, 0x72, 0x74, - 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x6f, 0x6e, 0x65, - 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x72, 0x76, 0x50, 0x6f, 0x72, 0x74, 0x4f, 0x6e, 0x6c, - 0x79, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x72, 0x76, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x4f, 0x6e, 0x6c, 0x79, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x72, 0x76, 0x50, 0x6f, - 0x72, 0x74, 0x41, 0x6e, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x10, 0x03, 0x12, 0x0f, - 0x0a, 0x0b, 0x54, 0x78, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x4f, 0x6e, 0x6c, 0x79, 0x10, 0x04, 0x12, - 0x12, 0x0a, 0x0e, 0x54, 0x78, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x4f, 0x6e, 0x6c, - 0x79, 0x10, 0x05, 0x12, 0x15, 0x0a, 0x11, 0x54, 0x78, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x41, 0x6e, - 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x10, 0x06, 0x42, 0x67, 0x0a, 0x1b, 0x63, 0x6f, - 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x50, 0x01, 0x5a, 0x2c, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, - 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, - 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0xaa, 0x02, 0x17, 0x58, 0x72, 0x61, 0x79, - 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x65, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x67, 0x79, 0x12, 0x53, 0x0a, 0x0e, 0x68, 0x61, 0x70, 0x70, 0x79, 0x5f, 0x65, 0x79, 0x65, + 0x62, 0x61, 0x6c, 0x6c, 0x73, 0x18, 0x16, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x48, 0x61, 0x70, 0x70, 0x79, 0x45, 0x79, 0x65, 0x62, 0x61, + 0x6c, 0x6c, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x68, 0x61, 0x70, 0x70, 0x79, + 0x45, 0x79, 0x65, 0x62, 0x61, 0x6c, 0x6c, 0x73, 0x22, 0x2f, 0x0a, 0x0a, 0x54, 0x50, 0x72, 0x6f, + 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x66, 0x66, 0x10, 0x00, 0x12, + 0x0a, 0x0a, 0x06, 0x54, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x52, + 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x10, 0x02, 0x22, 0xc7, 0x01, 0x0a, 0x13, 0x48, 0x61, + 0x70, 0x70, 0x79, 0x45, 0x79, 0x65, 0x62, 0x61, 0x6c, 0x6c, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x70, + 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x7a, 0x65, + 0x49, 0x70, 0x76, 0x36, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6c, 0x65, 0x61, + 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6c, + 0x65, 0x61, 0x76, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x72, 0x79, 0x5f, 0x64, 0x65, 0x6c, 0x61, + 0x79, 0x4d, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x74, 0x72, 0x79, 0x44, 0x65, + 0x6c, 0x61, 0x79, 0x4d, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x63, 0x6f, 0x6e, + 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x72, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, + 0x54, 0x72, 0x79, 0x2a, 0xa9, 0x01, 0x0a, 0x0e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, + 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x53, 0x5f, 0x49, 0x53, 0x10, + 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, + 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x34, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, + 0x45, 0x5f, 0x49, 0x50, 0x36, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x55, 0x53, 0x45, 0x5f, 0x49, + 0x50, 0x34, 0x36, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x36, + 0x34, 0x10, 0x05, 0x12, 0x0c, 0x0a, 0x08, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x5f, 0x49, 0x50, 0x10, + 0x06, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x5f, 0x49, 0x50, 0x34, 0x10, 0x07, + 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x5f, 0x49, 0x50, 0x36, 0x10, 0x08, 0x12, + 0x0e, 0x0a, 0x0a, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x5f, 0x49, 0x50, 0x34, 0x36, 0x10, 0x09, 0x12, + 0x0e, 0x0a, 0x0a, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x5f, 0x49, 0x50, 0x36, 0x34, 0x10, 0x0a, 0x2a, + 0x97, 0x01, 0x0a, 0x13, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x50, 0x6f, 0x72, 0x74, 0x53, + 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x6f, 0x6e, 0x65, 0x10, + 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x72, 0x76, 0x50, 0x6f, 0x72, 0x74, 0x4f, 0x6e, 0x6c, 0x79, + 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x72, 0x76, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x4f, 0x6e, 0x6c, 0x79, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x72, 0x76, 0x50, 0x6f, 0x72, + 0x74, 0x41, 0x6e, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x10, 0x03, 0x12, 0x0f, 0x0a, + 0x0b, 0x54, 0x78, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x4f, 0x6e, 0x6c, 0x79, 0x10, 0x04, 0x12, 0x12, + 0x0a, 0x0e, 0x54, 0x78, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x4f, 0x6e, 0x6c, 0x79, + 0x10, 0x05, 0x12, 0x15, 0x0a, 0x11, 0x54, 0x78, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x41, 0x6e, 0x64, + 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x10, 0x06, 0x42, 0x67, 0x0a, 0x1b, 0x63, 0x6f, 0x6d, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x50, 0x01, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, + 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0xaa, 0x02, 0x17, 0x58, 0x72, 0x61, 0x79, 0x2e, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -873,7 +976,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_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_transport_internet_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_transport_internet_config_proto_goTypes = []any{ (DomainStrategy)(0), // 0: xray.transport.internet.DomainStrategy (AddressPortStrategy)(0), // 1: xray.transport.internet.AddressPortStrategy @@ -883,24 +986,26 @@ var file_transport_internet_config_proto_goTypes = []any{ (*ProxyConfig)(nil), // 5: xray.transport.internet.ProxyConfig (*CustomSockopt)(nil), // 6: xray.transport.internet.CustomSockopt (*SocketConfig)(nil), // 7: xray.transport.internet.SocketConfig - (*serial.TypedMessage)(nil), // 8: xray.common.serial.TypedMessage - (*net.IPOrDomain)(nil), // 9: xray.common.net.IPOrDomain + (*HappyEyeballsConfig)(nil), // 8: xray.transport.internet.HappyEyeballsConfig + (*serial.TypedMessage)(nil), // 9: xray.common.serial.TypedMessage + (*net.IPOrDomain)(nil), // 10: xray.common.net.IPOrDomain } var file_transport_internet_config_proto_depIdxs = []int32{ - 8, // 0: xray.transport.internet.TransportConfig.settings:type_name -> xray.common.serial.TypedMessage - 9, // 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 - 8, // 3: xray.transport.internet.StreamConfig.security_settings:type_name -> xray.common.serial.TypedMessage - 7, // 4: xray.transport.internet.StreamConfig.socket_settings:type_name -> xray.transport.internet.SocketConfig - 2, // 5: xray.transport.internet.SocketConfig.tproxy:type_name -> xray.transport.internet.SocketConfig.TProxyMode - 0, // 6: xray.transport.internet.SocketConfig.domain_strategy:type_name -> xray.transport.internet.DomainStrategy - 6, // 7: xray.transport.internet.SocketConfig.customSockopt:type_name -> xray.transport.internet.CustomSockopt - 1, // 8: xray.transport.internet.SocketConfig.address_port_strategy:type_name -> xray.transport.internet.AddressPortStrategy - 9, // [9:9] is the sub-list for method output_type - 9, // [9:9] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 9, // 0: xray.transport.internet.TransportConfig.settings:type_name -> xray.common.serial.TypedMessage + 10, // 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 + 9, // 3: xray.transport.internet.StreamConfig.security_settings:type_name -> xray.common.serial.TypedMessage + 7, // 4: xray.transport.internet.StreamConfig.socket_settings:type_name -> xray.transport.internet.SocketConfig + 2, // 5: xray.transport.internet.SocketConfig.tproxy:type_name -> xray.transport.internet.SocketConfig.TProxyMode + 0, // 6: xray.transport.internet.SocketConfig.domain_strategy:type_name -> xray.transport.internet.DomainStrategy + 6, // 7: xray.transport.internet.SocketConfig.customSockopt:type_name -> xray.transport.internet.CustomSockopt + 1, // 8: xray.transport.internet.SocketConfig.address_port_strategy:type_name -> xray.transport.internet.AddressPortStrategy + 8, // 9: xray.transport.internet.SocketConfig.happy_eyeballs:type_name -> xray.transport.internet.HappyEyeballsConfig + 10, // [10:10] is the sub-list for method output_type + 10, // [10:10] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_transport_internet_config_proto_init() } @@ -914,7 +1019,7 @@ func file_transport_internet_config_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_transport_internet_config_proto_rawDesc, NumEnums: 3, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/transport/internet/config.proto b/transport/internet/config.proto index cb892c30..d6a981a5 100644 --- a/transport/internet/config.proto +++ b/transport/internet/config.proto @@ -130,4 +130,14 @@ message SocketConfig { repeated CustomSockopt customSockopt = 20; AddressPortStrategy address_port_strategy = 21; + + HappyEyeballsConfig happy_eyeballs = 22; +} + +message HappyEyeballsConfig { + bool enabled = 1; + bool prioritize_ipv6 = 2; + uint32 interleave = 3; + uint64 try_delayMs = 4; + uint32 max_concurrent_try = 5; } diff --git a/transport/internet/dialer.go b/transport/internet/dialer.go index 74e5f0b6..0bc377c8 100644 --- a/transport/internet/dialer.go +++ b/transport/internet/dialer.go @@ -255,9 +255,11 @@ func DialSystem(ctx context.Context, dest net.Destination, sockopt *SocketConfig if sockopt.DomainStrategy.forceIP() { return nil, err } - } else { + } else if !sockopt.HappyEyeballs.Enabled || len(ips) < 2 || len(sockopt.DialerProxy) > 0 || dest.Network != net.Network_TCP { dest.Address = net.IPAddress(ips[dice.Roll(len(ips))]) errors.LogInfo(ctx, "replace destination with "+dest.String()) + } else { + return TcpRaceDial(ctx, src, ips, dest.Port, sockopt) } } diff --git a/transport/internet/happy_eyeballs.go b/transport/internet/happy_eyeballs.go new file mode 100644 index 00000000..00d77ab9 --- /dev/null +++ b/transport/internet/happy_eyeballs.go @@ -0,0 +1,165 @@ +package internet + +import ( + "context" + "github.com/xtls/xray-core/common/net" + "time" +) + +type result struct { + err error + conn net.Conn + index int +} + +func TcpRaceDial(ctx context.Context, src net.Address, ips []net.IP, port net.Port, sockopt *SocketConfig) (net.Conn, error) { + if len(ips) < 2 { + panic("at least 2 ips is required to race dial") + } + ips = sortIPs(ips, sockopt.HappyEyeballs.PrioritizeIpv6, sockopt.HappyEyeballs.Interleave) + newCtx, cancel := context.WithCancel(ctx) + defer cancel() + var resultCh = make(chan *result, len(ips)) + nextTryIndex := 0 + activeNum := uint32(0) + waitTime := time.Duration(0) + var winConn net.Conn + for { + select { + case r := <-resultCh: + activeNum-- + select { + case <-ctx.Done(): + cancel() + waitTime = 100 * time.Hour + if winConn != nil { + winConn.Close() + } + if r.conn != nil { + r.conn.Close() + } + if activeNum == 0 { + return nil, ctx.Err() + } + continue + default: + if r.conn != nil { + cancel() + waitTime = 100 * time.Hour + if winConn == nil { + winConn = r.conn + } else { + r.conn.Close() + } + } + if winConn != nil && activeNum == 0 { + return winConn, nil + } + if winConn != nil { + continue + } + if nextTryIndex < len(ips) { + waitTime = time.Duration(0) + continue + } + if activeNum == 0 { + return nil, r.err + } + waitTime = 100 * time.Hour + continue + } + + case <-time.After(waitTime): + if nextTryIndex == len(ips) || activeNum == sockopt.HappyEyeballs.MaxConcurrentTry { + panic("impossible situation") + } + go tcpTryDial(newCtx, src, sockopt, ips[nextTryIndex], port, nextTryIndex, resultCh) + activeNum++ + nextTryIndex++ + if nextTryIndex == len(ips) || activeNum == sockopt.HappyEyeballs.MaxConcurrentTry { + waitTime = 100 * time.Hour + } else { + waitTime = time.Duration(sockopt.HappyEyeballs.TryDelayMs) * time.Millisecond + } + continue + } + } +} + +// sortIPs sort IPs according to rfc 8305. +func sortIPs(ips []net.IP, priorIPv6 bool, interleave uint32) []net.IP { + if len(ips) == 0 { + return ips + } + var ip4 = make([]net.IP, 0, len(ips)) + var ip6 = make([]net.IP, 0, len(ips)) + for _, ip := range ips { + parsedIp := net.IPAddress(ip).IP() + if len(parsedIp) == net.IPv4len { + ip4 = append(ip4, parsedIp) + } else { + ip6 = append(ip6, parsedIp) + } + } + + if len(ip4) == 0 || len(ip6) == 0 { + return ips + } + + var newIPs = make([]net.IP, 0, len(ips)) + consumeIP4 := 0 + consumeIP6 := 0 + consumeTurn := uint32(0) + ip4turn := true + if priorIPv6 { + ip4turn = false + } + for { + if ip4turn { + newIPs = append(newIPs, ip4[consumeIP4]) + consumeIP4++ + if consumeIP4 == len(ip4) { + newIPs = append(newIPs, ip6[consumeIP6:]...) + break + } + consumeTurn++ + if consumeTurn == interleave { + ip4turn = false + consumeTurn = uint32(0) + } + } else { + newIPs = append(newIPs, ip6[consumeIP6]) + consumeIP6++ + if consumeIP6 == len(ip6) { + newIPs = append(newIPs, ip4[consumeIP4:]...) + break + } + consumeTurn++ + if consumeTurn == interleave { + ip4turn = true + consumeTurn = uint32(0) + } + } + } + + return newIPs +} + +func tcpTryDial(ctx context.Context, src net.Address, sockopt *SocketConfig, ip net.IP, port net.Port, index int, resultCh chan<- *result) { + conn, err := effectiveSystemDialer.Dial(ctx, src, net.Destination{Address: net.IPAddress(ip), Network: net.Network_TCP, Port: port}, sockopt) + select { + case <-ctx.Done(): + if conn != nil { + conn.Close() + } + resultCh <- &result{err: ctx.Err(), index: index} + return + default: + if err != nil { + resultCh <- &result{err: err, index: index} + return + } + resultCh <- &result{conn: conn, index: index} + return + } +}