From fe57507fd98773319f7163f67e132f6846893b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=90=B2=93=F0=90=B3=9B=F0=90=B3=AA=F0=90=B3=82?= =?UTF-8?q?=F0=90=B3=90=20=F0=90=B2=80=F0=90=B3=A2=F0=90=B3=A6=F0=90=B3=AB?= =?UTF-8?q?=F0=90=B3=A2=20=F0=90=B2=A5=F0=90=B3=94=F0=90=B3=9B=F0=90=B3=AA?= =?UTF-8?q?=F0=90=B3=8C=F0=90=B3=91=F0=90=B3=96=F0=90=B3=87?= <26771058+KobeArthurScofield@users.noreply.github.com> Date: Mon, 15 Sep 2025 21:31:27 +0800 Subject: [PATCH 001/136] Outbound: One endpoint and at most one user only (#5144) https://github.com/XTLS/Xray-core/pull/5124#issuecomment-3281091009 Fixes https://github.com/XTLS/Xray-core/pull/5124#pullrequestreview-3218097421 --- app/proxyman/config.go | 22 -- app/proxyman/config.pb.go | 543 +++++++------------------- app/proxyman/config.proto | 36 +- app/proxyman/inbound/always.go | 15 +- app/proxyman/inbound/dynamic.go | 222 ----------- app/proxyman/inbound/inbound.go | 10 +- common/protocol/headers.go | 9 - common/protocol/server_picker.go | 89 ----- common/protocol/server_picker_test.go | 71 ---- common/protocol/server_spec.go | 112 +----- common/protocol/server_spec.pb.go | 6 +- common/protocol/server_spec.proto | 2 +- common/protocol/server_spec_test.go | 79 ---- core/xray_test.go | 18 +- features/inbound/inbound.go | 4 - infra/conf/http.go | 19 +- infra/conf/shadowsocks.go | 42 +- infra/conf/socks.go | 19 +- infra/conf/socks_test.go | 56 ++- infra/conf/trojan.go | 42 +- infra/conf/vless.go | 16 +- infra/conf/vless_test.go | 60 ++- infra/conf/vmess.go | 36 +- infra/conf/vmess_test.go | 73 ++-- infra/conf/xray.go | 60 --- infra/conf/xray_test.go | 10 - proxy/http/client.go | 26 +- proxy/http/config.pb.go | 8 +- proxy/http/config.proto | 2 +- proxy/shadowsocks/client.go | 29 +- proxy/shadowsocks/config.pb.go | 6 +- proxy/shadowsocks/config.proto | 2 +- proxy/socks/client.go | 27 +- proxy/socks/config.pb.go | 6 +- proxy/socks/config.proto | 2 +- proxy/trojan/client.go | 27 +- proxy/trojan/config.pb.go | 6 +- proxy/trojan/config.proto | 2 +- proxy/vless/outbound/config.pb.go | 6 +- proxy/vless/outbound/config.proto | 2 +- proxy/vless/outbound/outbound.go | 31 +- proxy/vmess/encoding/commands.go | 72 ---- proxy/vmess/encoding/commands_test.go | 55 --- proxy/vmess/inbound/config.pb.go | 54 +-- proxy/vmess/inbound/config.proto | 2 - proxy/vmess/inbound/inbound.go | 34 +- proxy/vmess/outbound/command.go | 31 +- proxy/vmess/outbound/config.pb.go | 6 +- proxy/vmess/outbound/config.proto | 2 +- proxy/vmess/outbound/outbound.go | 31 +- testing/scenarios/command_test.go | 44 +-- testing/scenarios/dokodemo_test.go | 36 +- testing/scenarios/feature_test.go | 94 ++--- testing/scenarios/policy_test.go | 44 +-- testing/scenarios/reverse_test.go | 44 +-- testing/scenarios/shadowsocks_test.go | 70 ++-- testing/scenarios/socks_test.go | 68 ++-- testing/scenarios/tls_test.go | 180 ++++----- testing/scenarios/transport_test.go | 18 +- testing/scenarios/vless_test.go | 76 ++-- testing/scenarios/vmess_test.go | 409 ++++++------------- 61 files changed, 829 insertions(+), 2394 deletions(-) delete mode 100644 app/proxyman/inbound/dynamic.go delete mode 100644 common/protocol/server_picker.go delete mode 100644 common/protocol/server_picker_test.go delete mode 100644 common/protocol/server_spec_test.go delete mode 100644 proxy/vmess/encoding/commands_test.go diff --git a/app/proxyman/config.go b/app/proxyman/config.go index 76d7a194..871971aa 100644 --- a/app/proxyman/config.go +++ b/app/proxyman/config.go @@ -1,23 +1 @@ package proxyman - -func (s *AllocationStrategy) GetConcurrencyValue() uint32 { - if s == nil || s.Concurrency == nil { - return 3 - } - return s.Concurrency.Value -} - -func (s *AllocationStrategy) GetRefreshValue() uint32 { - if s == nil || s.Refresh == nil { - return 5 - } - return s.Refresh.Value -} - -func (c *ReceiverConfig) GetEffectiveSniffingSettings() *SniffingConfig { - if c.SniffingSettings != nil { - return c.SniffingSettings - } - - return nil -} diff --git a/app/proxyman/config.pb.go b/app/proxyman/config.pb.go index 9713521b..0a0f7625 100644 --- a/app/proxyman/config.pb.go +++ b/app/proxyman/config.pb.go @@ -23,58 +23,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type AllocationStrategy_Type int32 - -const ( - // Always allocate all connection handlers. - AllocationStrategy_Always AllocationStrategy_Type = 0 - // Randomly allocate specific range of handlers. - AllocationStrategy_Random AllocationStrategy_Type = 1 - // External. Not supported yet. - AllocationStrategy_External AllocationStrategy_Type = 2 -) - -// Enum value maps for AllocationStrategy_Type. -var ( - AllocationStrategy_Type_name = map[int32]string{ - 0: "Always", - 1: "Random", - 2: "External", - } - AllocationStrategy_Type_value = map[string]int32{ - "Always": 0, - "Random": 1, - "External": 2, - } -) - -func (x AllocationStrategy_Type) Enum() *AllocationStrategy_Type { - p := new(AllocationStrategy_Type) - *p = x - return p -} - -func (x AllocationStrategy_Type) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (AllocationStrategy_Type) Descriptor() protoreflect.EnumDescriptor { - return file_app_proxyman_config_proto_enumTypes[0].Descriptor() -} - -func (AllocationStrategy_Type) Type() protoreflect.EnumType { - return &file_app_proxyman_config_proto_enumTypes[0] -} - -func (x AllocationStrategy_Type) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use AllocationStrategy_Type.Descriptor instead. -func (AllocationStrategy_Type) EnumDescriptor() ([]byte, []int) { - return file_app_proxyman_config_proto_rawDescGZIP(), []int{1, 0} -} - type InboundConfig struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -111,71 +59,6 @@ func (*InboundConfig) Descriptor() ([]byte, []int) { return file_app_proxyman_config_proto_rawDescGZIP(), []int{0} } -type AllocationStrategy struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Type AllocationStrategy_Type `protobuf:"varint,1,opt,name=type,proto3,enum=xray.app.proxyman.AllocationStrategy_Type" json:"type,omitempty"` - // Number of handlers (ports) running in parallel. - // Default value is 3 if unset. - Concurrency *AllocationStrategy_AllocationStrategyConcurrency `protobuf:"bytes,2,opt,name=concurrency,proto3" json:"concurrency,omitempty"` - // Number of minutes before a handler is regenerated. - // Default value is 5 if unset. - Refresh *AllocationStrategy_AllocationStrategyRefresh `protobuf:"bytes,3,opt,name=refresh,proto3" json:"refresh,omitempty"` -} - -func (x *AllocationStrategy) Reset() { - *x = AllocationStrategy{} - mi := &file_app_proxyman_config_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AllocationStrategy) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AllocationStrategy) ProtoMessage() {} - -func (x *AllocationStrategy) ProtoReflect() protoreflect.Message { - mi := &file_app_proxyman_config_proto_msgTypes[1] - 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 AllocationStrategy.ProtoReflect.Descriptor instead. -func (*AllocationStrategy) Descriptor() ([]byte, []int) { - return file_app_proxyman_config_proto_rawDescGZIP(), []int{1} -} - -func (x *AllocationStrategy) GetType() AllocationStrategy_Type { - if x != nil { - return x.Type - } - return AllocationStrategy_Always -} - -func (x *AllocationStrategy) GetConcurrency() *AllocationStrategy_AllocationStrategyConcurrency { - if x != nil { - return x.Concurrency - } - return nil -} - -func (x *AllocationStrategy) GetRefresh() *AllocationStrategy_AllocationStrategyRefresh { - if x != nil { - return x.Refresh - } - return nil -} - type SniffingConfig struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -196,7 +79,7 @@ type SniffingConfig struct { func (x *SniffingConfig) Reset() { *x = SniffingConfig{} - mi := &file_app_proxyman_config_proto_msgTypes[2] + mi := &file_app_proxyman_config_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -208,7 +91,7 @@ func (x *SniffingConfig) String() string { func (*SniffingConfig) ProtoMessage() {} func (x *SniffingConfig) ProtoReflect() protoreflect.Message { - mi := &file_app_proxyman_config_proto_msgTypes[2] + mi := &file_app_proxyman_config_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -221,7 +104,7 @@ func (x *SniffingConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use SniffingConfig.ProtoReflect.Descriptor instead. func (*SniffingConfig) Descriptor() ([]byte, []int) { - return file_app_proxyman_config_proto_rawDescGZIP(), []int{2} + return file_app_proxyman_config_proto_rawDescGZIP(), []int{1} } func (x *SniffingConfig) GetEnabled() bool { @@ -268,15 +151,14 @@ type ReceiverConfig struct { PortList *net.PortList `protobuf:"bytes,1,opt,name=port_list,json=portList,proto3" json:"port_list,omitempty"` // Listen specifies the IP address that the Receiver should listen on. Listen *net.IPOrDomain `protobuf:"bytes,2,opt,name=listen,proto3" json:"listen,omitempty"` - AllocationStrategy *AllocationStrategy `protobuf:"bytes,3,opt,name=allocation_strategy,json=allocationStrategy,proto3" json:"allocation_strategy,omitempty"` - StreamSettings *internet.StreamConfig `protobuf:"bytes,4,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"` - ReceiveOriginalDestination bool `protobuf:"varint,5,opt,name=receive_original_destination,json=receiveOriginalDestination,proto3" json:"receive_original_destination,omitempty"` - SniffingSettings *SniffingConfig `protobuf:"bytes,7,opt,name=sniffing_settings,json=sniffingSettings,proto3" json:"sniffing_settings,omitempty"` + StreamSettings *internet.StreamConfig `protobuf:"bytes,3,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"` + ReceiveOriginalDestination bool `protobuf:"varint,4,opt,name=receive_original_destination,json=receiveOriginalDestination,proto3" json:"receive_original_destination,omitempty"` + SniffingSettings *SniffingConfig `protobuf:"bytes,6,opt,name=sniffing_settings,json=sniffingSettings,proto3" json:"sniffing_settings,omitempty"` } func (x *ReceiverConfig) Reset() { *x = ReceiverConfig{} - mi := &file_app_proxyman_config_proto_msgTypes[3] + mi := &file_app_proxyman_config_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -288,7 +170,7 @@ func (x *ReceiverConfig) String() string { func (*ReceiverConfig) ProtoMessage() {} func (x *ReceiverConfig) ProtoReflect() protoreflect.Message { - mi := &file_app_proxyman_config_proto_msgTypes[3] + mi := &file_app_proxyman_config_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -301,7 +183,7 @@ func (x *ReceiverConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ReceiverConfig.ProtoReflect.Descriptor instead. func (*ReceiverConfig) Descriptor() ([]byte, []int) { - return file_app_proxyman_config_proto_rawDescGZIP(), []int{3} + return file_app_proxyman_config_proto_rawDescGZIP(), []int{2} } func (x *ReceiverConfig) GetPortList() *net.PortList { @@ -318,13 +200,6 @@ func (x *ReceiverConfig) GetListen() *net.IPOrDomain { return nil } -func (x *ReceiverConfig) GetAllocationStrategy() *AllocationStrategy { - if x != nil { - return x.AllocationStrategy - } - return nil -} - func (x *ReceiverConfig) GetStreamSettings() *internet.StreamConfig { if x != nil { return x.StreamSettings @@ -358,7 +233,7 @@ type InboundHandlerConfig struct { func (x *InboundHandlerConfig) Reset() { *x = InboundHandlerConfig{} - mi := &file_app_proxyman_config_proto_msgTypes[4] + mi := &file_app_proxyman_config_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -370,7 +245,7 @@ func (x *InboundHandlerConfig) String() string { func (*InboundHandlerConfig) ProtoMessage() {} func (x *InboundHandlerConfig) ProtoReflect() protoreflect.Message { - mi := &file_app_proxyman_config_proto_msgTypes[4] + mi := &file_app_proxyman_config_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -383,7 +258,7 @@ func (x *InboundHandlerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use InboundHandlerConfig.ProtoReflect.Descriptor instead. func (*InboundHandlerConfig) Descriptor() ([]byte, []int) { - return file_app_proxyman_config_proto_rawDescGZIP(), []int{4} + return file_app_proxyman_config_proto_rawDescGZIP(), []int{3} } func (x *InboundHandlerConfig) GetTag() string { @@ -415,7 +290,7 @@ type OutboundConfig struct { func (x *OutboundConfig) Reset() { *x = OutboundConfig{} - mi := &file_app_proxyman_config_proto_msgTypes[5] + mi := &file_app_proxyman_config_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -427,7 +302,7 @@ func (x *OutboundConfig) String() string { func (*OutboundConfig) ProtoMessage() {} func (x *OutboundConfig) ProtoReflect() protoreflect.Message { - mi := &file_app_proxyman_config_proto_msgTypes[5] + mi := &file_app_proxyman_config_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -440,7 +315,7 @@ func (x *OutboundConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use OutboundConfig.ProtoReflect.Descriptor instead. func (*OutboundConfig) Descriptor() ([]byte, []int) { - return file_app_proxyman_config_proto_rawDescGZIP(), []int{5} + return file_app_proxyman_config_proto_rawDescGZIP(), []int{4} } type SenderConfig struct { @@ -459,7 +334,7 @@ type SenderConfig struct { func (x *SenderConfig) Reset() { *x = SenderConfig{} - mi := &file_app_proxyman_config_proto_msgTypes[6] + mi := &file_app_proxyman_config_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -471,7 +346,7 @@ func (x *SenderConfig) String() string { func (*SenderConfig) ProtoMessage() {} func (x *SenderConfig) ProtoReflect() protoreflect.Message { - mi := &file_app_proxyman_config_proto_msgTypes[6] + mi := &file_app_proxyman_config_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -484,7 +359,7 @@ func (x *SenderConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use SenderConfig.ProtoReflect.Descriptor instead. func (*SenderConfig) Descriptor() ([]byte, []int) { - return file_app_proxyman_config_proto_rawDescGZIP(), []int{6} + return file_app_proxyman_config_proto_rawDescGZIP(), []int{5} } func (x *SenderConfig) GetVia() *net.IPOrDomain { @@ -546,7 +421,7 @@ type MultiplexingConfig struct { func (x *MultiplexingConfig) Reset() { *x = MultiplexingConfig{} - mi := &file_app_proxyman_config_proto_msgTypes[7] + mi := &file_app_proxyman_config_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -558,7 +433,7 @@ func (x *MultiplexingConfig) String() string { func (*MultiplexingConfig) ProtoMessage() {} func (x *MultiplexingConfig) ProtoReflect() protoreflect.Message { - mi := &file_app_proxyman_config_proto_msgTypes[7] + mi := &file_app_proxyman_config_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -571,7 +446,7 @@ func (x *MultiplexingConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use MultiplexingConfig.ProtoReflect.Descriptor instead. func (*MultiplexingConfig) Descriptor() ([]byte, []int) { - return file_app_proxyman_config_proto_rawDescGZIP(), []int{7} + return file_app_proxyman_config_proto_rawDescGZIP(), []int{6} } func (x *MultiplexingConfig) GetEnabled() bool { @@ -602,96 +477,6 @@ func (x *MultiplexingConfig) GetXudpProxyUDP443() string { return "" } -type AllocationStrategy_AllocationStrategyConcurrency struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` -} - -func (x *AllocationStrategy_AllocationStrategyConcurrency) Reset() { - *x = AllocationStrategy_AllocationStrategyConcurrency{} - mi := &file_app_proxyman_config_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AllocationStrategy_AllocationStrategyConcurrency) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AllocationStrategy_AllocationStrategyConcurrency) ProtoMessage() {} - -func (x *AllocationStrategy_AllocationStrategyConcurrency) ProtoReflect() protoreflect.Message { - mi := &file_app_proxyman_config_proto_msgTypes[8] - 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 AllocationStrategy_AllocationStrategyConcurrency.ProtoReflect.Descriptor instead. -func (*AllocationStrategy_AllocationStrategyConcurrency) Descriptor() ([]byte, []int) { - return file_app_proxyman_config_proto_rawDescGZIP(), []int{1, 0} -} - -func (x *AllocationStrategy_AllocationStrategyConcurrency) GetValue() uint32 { - if x != nil { - return x.Value - } - return 0 -} - -type AllocationStrategy_AllocationStrategyRefresh struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` -} - -func (x *AllocationStrategy_AllocationStrategyRefresh) Reset() { - *x = AllocationStrategy_AllocationStrategyRefresh{} - mi := &file_app_proxyman_config_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AllocationStrategy_AllocationStrategyRefresh) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AllocationStrategy_AllocationStrategyRefresh) ProtoMessage() {} - -func (x *AllocationStrategy_AllocationStrategyRefresh) ProtoReflect() protoreflect.Message { - mi := &file_app_proxyman_config_proto_msgTypes[9] - 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 AllocationStrategy_AllocationStrategyRefresh.ProtoReflect.Descriptor instead. -func (*AllocationStrategy_AllocationStrategyRefresh) Descriptor() ([]byte, []int) { - return file_app_proxyman_config_proto_rawDescGZIP(), []int{1, 1} -} - -func (x *AllocationStrategy_AllocationStrategyRefresh) GetValue() uint32 { - if x != nil { - return x.Value - } - return 0 -} - var File_app_proxyman_config_proto protoreflect.FileDescriptor var file_app_proxyman_config_proto_rawDesc = []byte{ @@ -706,130 +491,98 @@ var file_app_proxyman_config_proto_rawDesc = []byte{ 0x1a, 0x21, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x0f, 0x0a, 0x0d, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x22, 0xae, 0x03, 0x0a, 0x12, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x3e, 0x0a, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2a, 0x2e, 0x78, 0x72, 0x61, 0x79, - 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x41, 0x6c, - 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, - 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x65, 0x0a, 0x0b, 0x63, - 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x43, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, - 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, - 0x72, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, - 0x63, 0x79, 0x12, 0x59, 0x0a, 0x07, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, - 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x65, 0x66, - 0x72, 0x65, 0x73, 0x68, 0x52, 0x07, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x1a, 0x35, 0x0a, - 0x1d, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, - 0x65, 0x67, 0x79, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x31, 0x0a, 0x19, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, - 0x68, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x2c, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x0a, 0x0a, 0x06, 0x41, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x52, - 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x45, 0x78, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x10, 0x02, 0x22, 0xcc, 0x01, 0x0a, 0x0e, 0x53, 0x6e, 0x69, 0x66, 0x66, 0x69, - 0x6e, 0x67, 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, 0x31, 0x0a, 0x14, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x13, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x76, 0x65, - 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, - 0x5f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x0f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, - 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6f, 0x6e, 0x6c, - 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x6f, - 0x6e, 0x6c, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0xbd, 0x03, 0x0a, 0x0e, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x09, 0x70, 0x6f, 0x72, 0x74, 0x5f, - 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, - 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x50, 0x6f, 0x72, - 0x74, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x12, - 0x33, 0x0a, 0x06, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, - 0x74, 0x2e, 0x49, 0x50, 0x4f, 0x72, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x06, 0x6c, 0x69, - 0x73, 0x74, 0x65, 0x6e, 0x12, 0x56, 0x0a, 0x13, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, - 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x4e, 0x0a, 0x0f, - 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 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, - 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x73, 0x74, - 0x72, 0x65, 0x61, 0x6d, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x40, 0x0a, 0x1c, - 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, - 0x5f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x1a, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, - 0x6e, 0x61, 0x6c, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4e, - 0x0a, 0x11, 0x73, 0x6e, 0x69, 0x66, 0x66, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, - 0x6e, 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x78, 0x72, 0x61, 0x79, - 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x53, 0x6e, - 0x69, 0x66, 0x66, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x10, 0x73, 0x6e, - 0x69, 0x66, 0x66, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x4a, 0x04, - 0x08, 0x06, 0x10, 0x07, 0x22, 0xc0, 0x01, 0x0a, 0x14, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, - 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, - 0x4d, 0x0a, 0x11, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x74, 0x74, - 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, - 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, - 0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x10, 0x72, 0x65, - 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x47, - 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65, - 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x53, - 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x10, 0x0a, 0x0e, 0x4f, 0x75, 0x74, 0x62, 0x6f, - 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x9d, 0x03, 0x0a, 0x0c, 0x53, 0x65, - 0x6e, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2d, 0x0a, 0x03, 0x76, 0x69, - 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x49, 0x50, 0x4f, 0x72, 0x44, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x03, 0x76, 0x69, 0x61, 0x12, 0x4e, 0x0a, 0x0f, 0x73, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x25, 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, 0x53, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x73, 0x74, 0x72, 0x65, 0x61, - 0x6d, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x4b, 0x0a, 0x0e, 0x70, 0x72, 0x6f, - 0x78, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x24, 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, 0x50, 0x72, 0x6f, 0x78, - 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x65, - 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x54, 0x0a, 0x12, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, - 0x6c, 0x65, 0x78, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, - 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, - 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x6d, 0x75, 0x6c, 0x74, 0x69, - 0x70, 0x6c, 0x65, 0x78, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x19, 0x0a, 0x08, - 0x76, 0x69, 0x61, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x76, 0x69, 0x61, 0x43, 0x69, 0x64, 0x72, 0x12, 0x50, 0x0a, 0x0f, 0x74, 0x61, 0x72, 0x67, 0x65, - 0x74, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x27, 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, 0x44, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, - 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x22, 0xa4, 0x01, 0x0a, 0x12, 0x4d, 0x75, + 0x6e, 0x66, 0x69, 0x67, 0x22, 0xcc, 0x01, 0x0a, 0x0e, 0x53, 0x6e, 0x69, 0x66, 0x66, 0x69, 0x6e, + 0x67, 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, 0x31, 0x0a, 0x14, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x13, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x76, 0x65, 0x72, + 0x72, 0x69, 0x64, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x5f, + 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x12, + 0x23, 0x0a, 0x0d, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x6f, 0x6e, + 0x6c, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x4f, + 0x6e, 0x6c, 0x79, 0x22, 0xe5, 0x02, 0x0a, 0x0e, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x09, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x6c, + 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, 0x79, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x33, + 0x0a, 0x06, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, + 0x2e, 0x49, 0x50, 0x4f, 0x72, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x06, 0x6c, 0x69, 0x73, + 0x74, 0x65, 0x6e, 0x12, 0x4e, 0x0a, 0x0f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x73, 0x65, + 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 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, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x0e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x12, 0x40, 0x0a, 0x1c, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x5f, 0x6f, + 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x65, 0x63, 0x65, 0x69, + 0x76, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4e, 0x0a, 0x11, 0x73, 0x6e, 0x69, 0x66, 0x66, 0x69, 0x6e, + 0x67, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x21, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x53, 0x6e, 0x69, 0x66, 0x66, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x10, 0x73, 0x6e, 0x69, 0x66, 0x66, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x74, + 0x74, 0x69, 0x6e, 0x67, 0x73, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, 0xc0, 0x01, 0x0a, 0x14, + 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x4d, 0x0a, 0x11, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, + 0x65, 0x72, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x52, 0x10, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x53, 0x65, 0x74, + 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x47, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x73, + 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x10, + 0x0a, 0x0e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x22, 0x9d, 0x03, 0x0a, 0x0c, 0x53, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x2d, 0x0a, 0x03, 0x76, 0x69, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, + 0x2e, 0x49, 0x50, 0x4f, 0x72, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x03, 0x76, 0x69, 0x61, + 0x12, 0x4e, 0x0a, 0x0f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 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, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x0e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x12, 0x4b, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, + 0x67, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 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, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, + 0x70, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x54, 0x0a, + 0x12, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, + 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x69, 0x6e, 0x67, 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, 0x20, 0x0a, 0x0b, 0x63, 0x6f, - 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x0f, - 0x78, 0x75, 0x64, 0x70, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x78, 0x75, 0x64, 0x70, 0x43, 0x6f, 0x6e, 0x63, 0x75, - 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x78, 0x75, 0x64, 0x70, 0x50, 0x72, - 0x6f, 0x78, 0x79, 0x55, 0x44, 0x50, 0x34, 0x34, 0x33, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x52, 0x11, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x53, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x76, 0x69, 0x61, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x69, 0x61, 0x43, 0x69, 0x64, 0x72, 0x12, 0x50, + 0x0a, 0x0f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, + 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 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, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, + 0x52, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, + 0x22, 0xa4, 0x01, 0x0a, 0x12, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x69, 0x6e, + 0x67, 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, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, + 0x6e, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x78, 0x75, 0x64, 0x70, 0x43, 0x6f, 0x6e, 0x63, 0x75, + 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x78, 0x75, + 0x64, 0x70, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x78, 0x75, 0x64, 0x70, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x55, 0x44, 0x50, 0x34, 0x34, 0x33, - 0x42, 0x55, 0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, - 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x50, 0x01, 0x5a, 0x26, 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, 0x61, 0x70, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, - 0x6d, 0x61, 0x6e, 0xaa, 0x02, 0x11, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x50, - 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x78, 0x75, 0x64, 0x70, 0x50, 0x72, 0x6f, 0x78, + 0x79, 0x55, 0x44, 0x50, 0x34, 0x34, 0x33, 0x42, 0x55, 0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, + 0x50, 0x01, 0x5a, 0x26, 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, 0x61, 0x70, + 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0xaa, 0x02, 0x11, 0x58, 0x72, 0x61, + 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -844,48 +597,39 @@ func file_app_proxyman_config_proto_rawDescGZIP() []byte { return file_app_proxyman_config_proto_rawDescData } -var file_app_proxyman_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_app_proxyman_config_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_app_proxyman_config_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_app_proxyman_config_proto_goTypes = []any{ - (AllocationStrategy_Type)(0), // 0: xray.app.proxyman.AllocationStrategy.Type - (*InboundConfig)(nil), // 1: xray.app.proxyman.InboundConfig - (*AllocationStrategy)(nil), // 2: xray.app.proxyman.AllocationStrategy - (*SniffingConfig)(nil), // 3: xray.app.proxyman.SniffingConfig - (*ReceiverConfig)(nil), // 4: xray.app.proxyman.ReceiverConfig - (*InboundHandlerConfig)(nil), // 5: xray.app.proxyman.InboundHandlerConfig - (*OutboundConfig)(nil), // 6: xray.app.proxyman.OutboundConfig - (*SenderConfig)(nil), // 7: xray.app.proxyman.SenderConfig - (*MultiplexingConfig)(nil), // 8: xray.app.proxyman.MultiplexingConfig - (*AllocationStrategy_AllocationStrategyConcurrency)(nil), // 9: xray.app.proxyman.AllocationStrategy.AllocationStrategyConcurrency - (*AllocationStrategy_AllocationStrategyRefresh)(nil), // 10: xray.app.proxyman.AllocationStrategy.AllocationStrategyRefresh - (*net.PortList)(nil), // 11: xray.common.net.PortList - (*net.IPOrDomain)(nil), // 12: xray.common.net.IPOrDomain - (*internet.StreamConfig)(nil), // 13: xray.transport.internet.StreamConfig - (*serial.TypedMessage)(nil), // 14: xray.common.serial.TypedMessage - (*internet.ProxyConfig)(nil), // 15: xray.transport.internet.ProxyConfig - (internet.DomainStrategy)(0), // 16: xray.transport.internet.DomainStrategy + (*InboundConfig)(nil), // 0: xray.app.proxyman.InboundConfig + (*SniffingConfig)(nil), // 1: xray.app.proxyman.SniffingConfig + (*ReceiverConfig)(nil), // 2: xray.app.proxyman.ReceiverConfig + (*InboundHandlerConfig)(nil), // 3: xray.app.proxyman.InboundHandlerConfig + (*OutboundConfig)(nil), // 4: xray.app.proxyman.OutboundConfig + (*SenderConfig)(nil), // 5: xray.app.proxyman.SenderConfig + (*MultiplexingConfig)(nil), // 6: xray.app.proxyman.MultiplexingConfig + (*net.PortList)(nil), // 7: xray.common.net.PortList + (*net.IPOrDomain)(nil), // 8: xray.common.net.IPOrDomain + (*internet.StreamConfig)(nil), // 9: xray.transport.internet.StreamConfig + (*serial.TypedMessage)(nil), // 10: xray.common.serial.TypedMessage + (*internet.ProxyConfig)(nil), // 11: xray.transport.internet.ProxyConfig + (internet.DomainStrategy)(0), // 12: xray.transport.internet.DomainStrategy } var file_app_proxyman_config_proto_depIdxs = []int32{ - 0, // 0: xray.app.proxyman.AllocationStrategy.type:type_name -> xray.app.proxyman.AllocationStrategy.Type - 9, // 1: xray.app.proxyman.AllocationStrategy.concurrency:type_name -> xray.app.proxyman.AllocationStrategy.AllocationStrategyConcurrency - 10, // 2: xray.app.proxyman.AllocationStrategy.refresh:type_name -> xray.app.proxyman.AllocationStrategy.AllocationStrategyRefresh - 11, // 3: xray.app.proxyman.ReceiverConfig.port_list:type_name -> xray.common.net.PortList - 12, // 4: xray.app.proxyman.ReceiverConfig.listen:type_name -> xray.common.net.IPOrDomain - 2, // 5: xray.app.proxyman.ReceiverConfig.allocation_strategy:type_name -> xray.app.proxyman.AllocationStrategy - 13, // 6: xray.app.proxyman.ReceiverConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig - 3, // 7: xray.app.proxyman.ReceiverConfig.sniffing_settings:type_name -> xray.app.proxyman.SniffingConfig - 14, // 8: xray.app.proxyman.InboundHandlerConfig.receiver_settings:type_name -> xray.common.serial.TypedMessage - 14, // 9: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage - 12, // 10: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain - 13, // 11: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig - 15, // 12: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig - 8, // 13: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig - 16, // 14: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy - 15, // [15:15] is the sub-list for method output_type - 15, // [15:15] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 7, // 0: xray.app.proxyman.ReceiverConfig.port_list:type_name -> xray.common.net.PortList + 8, // 1: xray.app.proxyman.ReceiverConfig.listen:type_name -> xray.common.net.IPOrDomain + 9, // 2: xray.app.proxyman.ReceiverConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig + 1, // 3: xray.app.proxyman.ReceiverConfig.sniffing_settings:type_name -> xray.app.proxyman.SniffingConfig + 10, // 4: xray.app.proxyman.InboundHandlerConfig.receiver_settings:type_name -> xray.common.serial.TypedMessage + 10, // 5: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage + 8, // 6: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain + 9, // 7: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig + 11, // 8: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig + 6, // 9: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig + 12, // 10: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy + 11, // [11:11] is the sub-list for method output_type + 11, // [11:11] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_app_proxyman_config_proto_init() } @@ -898,14 +642,13 @@ func file_app_proxyman_config_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_app_proxyman_config_proto_rawDesc, - NumEnums: 1, - NumMessages: 10, + NumEnums: 0, + NumMessages: 7, NumExtensions: 0, NumServices: 0, }, GoTypes: file_app_proxyman_config_proto_goTypes, DependencyIndexes: file_app_proxyman_config_proto_depIdxs, - EnumInfos: file_app_proxyman_config_proto_enumTypes, MessageInfos: file_app_proxyman_config_proto_msgTypes, }.Build() File_app_proxyman_config_proto = out.File diff --git a/app/proxyman/config.proto b/app/proxyman/config.proto index a267a00b..4f1298b9 100644 --- a/app/proxyman/config.proto +++ b/app/proxyman/config.proto @@ -13,33 +13,6 @@ import "common/serial/typed_message.proto"; message InboundConfig {} -message AllocationStrategy { - enum Type { - // Always allocate all connection handlers. - Always = 0; - - // Randomly allocate specific range of handlers. - Random = 1; - - // External. Not supported yet. - External = 2; - } - - Type type = 1; - - message AllocationStrategyConcurrency { uint32 value = 1; } - - // Number of handlers (ports) running in parallel. - // Default value is 3 if unset. - AllocationStrategyConcurrency concurrency = 2; - - message AllocationStrategyRefresh { uint32 value = 1; } - - // Number of minutes before a handler is regenerated. - // Default value is 5 if unset. - AllocationStrategyRefresh refresh = 3; -} - message SniffingConfig { // Whether or not to enable content sniffing on an inbound connection. bool enabled = 1; @@ -62,11 +35,10 @@ message ReceiverConfig { xray.common.net.PortList port_list = 1; // Listen specifies the IP address that the Receiver should listen on. xray.common.net.IPOrDomain listen = 2; - AllocationStrategy allocation_strategy = 3; - xray.transport.internet.StreamConfig stream_settings = 4; - bool receive_original_destination = 5; - reserved 6; - SniffingConfig sniffing_settings = 7; + xray.transport.internet.StreamConfig stream_settings = 3; + bool receive_original_destination = 4; + reserved 5; + SniffingConfig sniffing_settings = 6; } message InboundHandlerConfig { diff --git a/app/proxyman/inbound/always.go b/app/proxyman/inbound/always.go index f6a769fb..6fb3ba43 100644 --- a/app/proxyman/inbound/always.go +++ b/app/proxyman/inbound/always.go @@ -5,7 +5,6 @@ import ( "github.com/xtls/xray-core/app/proxyman" "github.com/xtls/xray-core/common" - "github.com/xtls/xray-core/common/dice" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/mux" "github.com/xtls/xray-core/common/net" @@ -103,7 +102,7 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig * stream: mss, tag: tag, dispatcher: h.mux, - sniffingConfig: receiverConfig.GetEffectiveSniffingSettings(), + sniffingConfig: receiverConfig.SniffingSettings, uplinkCounter: uplinkCounter, downlinkCounter: downlinkCounter, ctx: ctx, @@ -125,7 +124,7 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig * recvOrigDest: receiverConfig.ReceiveOriginalDestination, tag: tag, dispatcher: h.mux, - sniffingConfig: receiverConfig.GetEffectiveSniffingSettings(), + sniffingConfig: receiverConfig.SniffingSettings, uplinkCounter: uplinkCounter, downlinkCounter: downlinkCounter, ctx: ctx, @@ -140,7 +139,7 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig * address: address, port: net.Port(port), dispatcher: h.mux, - sniffingConfig: receiverConfig.GetEffectiveSniffingSettings(), + sniffingConfig: receiverConfig.SniffingSettings, uplinkCounter: uplinkCounter, downlinkCounter: downlinkCounter, stream: mss, @@ -178,14 +177,6 @@ func (h *AlwaysOnInboundHandler) Close() error { return nil } -func (h *AlwaysOnInboundHandler) GetRandomInboundProxy() (interface{}, net.Port, int) { - if len(h.workers) == 0 { - return nil, 0, 0 - } - w := h.workers[dice.Roll(len(h.workers))] - return w.Proxy(), w.Port(), 9999 -} - func (h *AlwaysOnInboundHandler) Tag() string { return h.tag } diff --git a/app/proxyman/inbound/dynamic.go b/app/proxyman/inbound/dynamic.go deleted file mode 100644 index f14a9952..00000000 --- a/app/proxyman/inbound/dynamic.go +++ /dev/null @@ -1,222 +0,0 @@ -package inbound - -import ( - "context" - "sync" - "time" - - "github.com/xtls/xray-core/app/proxyman" - "github.com/xtls/xray-core/common/dice" - "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/mux" - "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/common/serial" - "github.com/xtls/xray-core/common/task" - "github.com/xtls/xray-core/core" - "github.com/xtls/xray-core/proxy" - "github.com/xtls/xray-core/transport/internet" - "google.golang.org/protobuf/proto" -) - -type DynamicInboundHandler struct { - tag string - v *core.Instance - proxyConfig interface{} - receiverConfig *proxyman.ReceiverConfig - streamSettings *internet.MemoryStreamConfig - portMutex sync.Mutex - portsInUse map[net.Port]struct{} - workerMutex sync.RWMutex - worker []worker - lastRefresh time.Time - mux *mux.Server - task *task.Periodic - - ctx context.Context -} - -func NewDynamicInboundHandler(ctx context.Context, tag string, receiverConfig *proxyman.ReceiverConfig, proxyConfig interface{}) (*DynamicInboundHandler, error) { - v := core.MustFromContext(ctx) - h := &DynamicInboundHandler{ - tag: tag, - proxyConfig: proxyConfig, - receiverConfig: receiverConfig, - portsInUse: make(map[net.Port]struct{}), - mux: mux.NewServer(ctx), - v: v, - ctx: ctx, - } - - mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings) - if err != nil { - return nil, errors.New("failed to parse stream settings").Base(err).AtWarning() - } - if receiverConfig.ReceiveOriginalDestination { - if mss.SocketSettings == nil { - mss.SocketSettings = &internet.SocketConfig{} - } - if mss.SocketSettings.Tproxy == internet.SocketConfig_Off { - mss.SocketSettings.Tproxy = internet.SocketConfig_Redirect - } - mss.SocketSettings.ReceiveOriginalDestAddress = true - } - - h.streamSettings = mss - - h.task = &task.Periodic{ - Interval: time.Minute * time.Duration(h.receiverConfig.AllocationStrategy.GetRefreshValue()), - Execute: h.refresh, - } - - return h, nil -} - -func (h *DynamicInboundHandler) allocatePort() net.Port { - allPorts := []int32{} - for _, pr := range h.receiverConfig.PortList.Range { - for i := pr.From; i <= pr.To; i++ { - allPorts = append(allPorts, int32(i)) - } - } - h.portMutex.Lock() - defer h.portMutex.Unlock() - - for { - r := dice.Roll(len(allPorts)) - port := net.Port(allPorts[r]) - _, used := h.portsInUse[port] - if !used { - h.portsInUse[port] = struct{}{} - return port - } - } -} - -func (h *DynamicInboundHandler) closeWorkers(workers []worker) { - ports2Del := make([]net.Port, len(workers)) - for idx, worker := range workers { - ports2Del[idx] = worker.Port() - if err := worker.Close(); err != nil { - errors.LogInfoInner(h.ctx, err, "failed to close worker") - } - } - - h.portMutex.Lock() - for _, port := range ports2Del { - delete(h.portsInUse, port) - } - h.portMutex.Unlock() -} - -func (h *DynamicInboundHandler) refresh() error { - h.lastRefresh = time.Now() - - timeout := time.Minute * time.Duration(h.receiverConfig.AllocationStrategy.GetRefreshValue()) * 2 - concurrency := h.receiverConfig.AllocationStrategy.GetConcurrencyValue() - workers := make([]worker, 0, concurrency) - - address := h.receiverConfig.Listen.AsAddress() - if address == nil { - address = net.AnyIP - } - - uplinkCounter, downlinkCounter := getStatCounter(h.v, h.tag) - - for i := uint32(0); i < concurrency; i++ { - port := h.allocatePort() - rawProxy, err := core.CreateObject(h.v, h.proxyConfig) - if err != nil { - errors.LogWarningInner(h.ctx, err, "failed to create proxy instance") - continue - } - p := rawProxy.(proxy.Inbound) - nl := p.Network() - if net.HasNetwork(nl, net.Network_TCP) { - worker := &tcpWorker{ - tag: h.tag, - address: address, - port: port, - proxy: p, - stream: h.streamSettings, - recvOrigDest: h.receiverConfig.ReceiveOriginalDestination, - dispatcher: h.mux, - sniffingConfig: h.receiverConfig.GetEffectiveSniffingSettings(), - uplinkCounter: uplinkCounter, - downlinkCounter: downlinkCounter, - ctx: h.ctx, - } - if err := worker.Start(); err != nil { - errors.LogWarningInner(h.ctx, err, "failed to create TCP worker") - continue - } - workers = append(workers, worker) - } - - if net.HasNetwork(nl, net.Network_UDP) { - worker := &udpWorker{ - tag: h.tag, - proxy: p, - address: address, - port: port, - dispatcher: h.mux, - sniffingConfig: h.receiverConfig.GetEffectiveSniffingSettings(), - uplinkCounter: uplinkCounter, - downlinkCounter: downlinkCounter, - stream: h.streamSettings, - ctx: h.ctx, - } - if err := worker.Start(); err != nil { - errors.LogWarningInner(h.ctx, err, "failed to create UDP worker") - continue - } - workers = append(workers, worker) - } - } - - h.workerMutex.Lock() - h.worker = workers - h.workerMutex.Unlock() - - time.AfterFunc(timeout, func() { - h.closeWorkers(workers) - }) - - return nil -} - -func (h *DynamicInboundHandler) Start() error { - return h.task.Start() -} - -func (h *DynamicInboundHandler) Close() error { - return h.task.Close() -} - -func (h *DynamicInboundHandler) GetRandomInboundProxy() (interface{}, net.Port, int) { - h.workerMutex.RLock() - defer h.workerMutex.RUnlock() - - if len(h.worker) == 0 { - return nil, 0, 0 - } - w := h.worker[dice.Roll(len(h.worker))] - expire := h.receiverConfig.AllocationStrategy.GetRefreshValue() - uint32(time.Since(h.lastRefresh)/time.Minute) - return w.Proxy(), w.Port(), int(expire) -} - -func (h *DynamicInboundHandler) Tag() string { - return h.tag -} - -// ReceiverSettings implements inbound.Handler. -func (h *DynamicInboundHandler) ReceiverSettings() *serial.TypedMessage { - return serial.ToTypedMessage(h.receiverConfig) -} - -// ProxySettings implements inbound.Handler. -func (h *DynamicInboundHandler) ProxySettings() *serial.TypedMessage { - if v, ok := h.proxyConfig.(proto.Message); ok { - return serial.ToTypedMessage(v) - } - return nil -} diff --git a/app/proxyman/inbound/inbound.go b/app/proxyman/inbound/inbound.go index 8e484965..374a0428 100644 --- a/app/proxyman/inbound/inbound.go +++ b/app/proxyman/inbound/inbound.go @@ -178,15 +178,7 @@ func NewHandler(ctx context.Context, config *core.InboundHandlerConfig) (inbound ctx = session.ContextWithAllowedNetwork(ctx, net.Network_UDP) } - allocStrategy := receiverSettings.AllocationStrategy - if allocStrategy == nil || allocStrategy.Type == proxyman.AllocationStrategy_Always { - return NewAlwaysOnInboundHandler(ctx, tag, receiverSettings, proxySettings) - } - - if allocStrategy.Type == proxyman.AllocationStrategy_Random { - return NewDynamicInboundHandler(ctx, tag, receiverSettings, proxySettings) - } - return nil, errors.New("unknown allocation strategy: ", receiverSettings.AllocationStrategy.Type).AtError() + return NewAlwaysOnInboundHandler(ctx, tag, receiverSettings, proxySettings) } func init() { diff --git a/common/protocol/headers.go b/common/protocol/headers.go index f6614ec7..bc90ca6c 100644 --- a/common/protocol/headers.go +++ b/common/protocol/headers.go @@ -5,7 +5,6 @@ import ( "github.com/xtls/xray-core/common/bitmask" "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/common/uuid" "golang.org/x/sys/cpu" ) @@ -71,14 +70,6 @@ type ResponseHeader struct { Command ResponseCommand } -type CommandSwitchAccount struct { - Host net.Address - Port net.Port - ID uuid.UUID - Level uint32 - ValidMin byte -} - var ( // Keep in sync with crypto/tls/cipher_suites.go. hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ && cpu.X86.HasSSE41 && cpu.X86.HasSSSE3 diff --git a/common/protocol/server_picker.go b/common/protocol/server_picker.go deleted file mode 100644 index 62aa404e..00000000 --- a/common/protocol/server_picker.go +++ /dev/null @@ -1,89 +0,0 @@ -package protocol - -import ( - "sync" -) - -type ServerList struct { - sync.RWMutex - servers []*ServerSpec -} - -func NewServerList() *ServerList { - return &ServerList{} -} - -func (sl *ServerList) AddServer(server *ServerSpec) { - sl.Lock() - defer sl.Unlock() - - sl.servers = append(sl.servers, server) -} - -func (sl *ServerList) Size() uint32 { - sl.RLock() - defer sl.RUnlock() - - return uint32(len(sl.servers)) -} - -func (sl *ServerList) GetServer(idx uint32) *ServerSpec { - sl.Lock() - defer sl.Unlock() - - for { - if idx >= uint32(len(sl.servers)) { - return nil - } - - server := sl.servers[idx] - if !server.IsValid() { - sl.removeServer(idx) - continue - } - - return server - } -} - -func (sl *ServerList) removeServer(idx uint32) { - n := len(sl.servers) - sl.servers[idx] = sl.servers[n-1] - sl.servers = sl.servers[:n-1] -} - -type ServerPicker interface { - PickServer() *ServerSpec -} - -type RoundRobinServerPicker struct { - sync.Mutex - serverlist *ServerList - nextIndex uint32 -} - -func NewRoundRobinServerPicker(serverlist *ServerList) *RoundRobinServerPicker { - return &RoundRobinServerPicker{ - serverlist: serverlist, - nextIndex: 0, - } -} - -func (p *RoundRobinServerPicker) PickServer() *ServerSpec { - p.Lock() - defer p.Unlock() - - next := p.nextIndex - server := p.serverlist.GetServer(next) - if server == nil { - next = 0 - server = p.serverlist.GetServer(0) - } - next++ - if next >= p.serverlist.Size() { - next = 0 - } - p.nextIndex = next - - return server -} diff --git a/common/protocol/server_picker_test.go b/common/protocol/server_picker_test.go deleted file mode 100644 index 8919b10b..00000000 --- a/common/protocol/server_picker_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package protocol_test - -import ( - "testing" - "time" - - "github.com/xtls/xray-core/common/net" - . "github.com/xtls/xray-core/common/protocol" -) - -func TestServerList(t *testing.T) { - list := NewServerList() - list.AddServer(NewServerSpec(net.TCPDestination(net.LocalHostIP, net.Port(1)), AlwaysValid())) - if list.Size() != 1 { - t.Error("list size: ", list.Size()) - } - list.AddServer(NewServerSpec(net.TCPDestination(net.LocalHostIP, net.Port(2)), BeforeTime(time.Now().Add(time.Second)))) - if list.Size() != 2 { - t.Error("list.size: ", list.Size()) - } - - server := list.GetServer(1) - if server.Destination().Port != 2 { - t.Error("server: ", server.Destination()) - } - time.Sleep(2 * time.Second) - server = list.GetServer(1) - if server != nil { - t.Error("server: ", server) - } - - server = list.GetServer(0) - if server.Destination().Port != 1 { - t.Error("server: ", server.Destination()) - } -} - -func TestServerPicker(t *testing.T) { - list := NewServerList() - list.AddServer(NewServerSpec(net.TCPDestination(net.LocalHostIP, net.Port(1)), AlwaysValid())) - list.AddServer(NewServerSpec(net.TCPDestination(net.LocalHostIP, net.Port(2)), BeforeTime(time.Now().Add(time.Second)))) - list.AddServer(NewServerSpec(net.TCPDestination(net.LocalHostIP, net.Port(3)), BeforeTime(time.Now().Add(time.Second)))) - - picker := NewRoundRobinServerPicker(list) - server := picker.PickServer() - if server.Destination().Port != 1 { - t.Error("server: ", server.Destination()) - } - server = picker.PickServer() - if server.Destination().Port != 2 { - t.Error("server: ", server.Destination()) - } - server = picker.PickServer() - if server.Destination().Port != 3 { - t.Error("server: ", server.Destination()) - } - server = picker.PickServer() - if server.Destination().Port != 1 { - t.Error("server: ", server.Destination()) - } - - time.Sleep(2 * time.Second) - server = picker.PickServer() - if server.Destination().Port != 1 { - t.Error("server: ", server.Destination()) - } - server = picker.PickServer() - if server.Destination().Port != 1 { - t.Error("server: ", server.Destination()) - } -} diff --git a/common/protocol/server_spec.go b/common/protocol/server_spec.go index 24b778bb..d3ab0089 100644 --- a/common/protocol/server_spec.go +++ b/common/protocol/server_spec.go @@ -1,122 +1,30 @@ package protocol import ( - "sync" - "time" - - "github.com/xtls/xray-core/common/dice" "github.com/xtls/xray-core/common/net" ) -type ValidationStrategy interface { - IsValid() bool - Invalidate() -} - -type alwaysValidStrategy struct{} - -func AlwaysValid() ValidationStrategy { - return alwaysValidStrategy{} -} - -func (alwaysValidStrategy) IsValid() bool { - return true -} - -func (alwaysValidStrategy) Invalidate() {} - -type timeoutValidStrategy struct { - until time.Time -} - -func BeforeTime(t time.Time) ValidationStrategy { - return &timeoutValidStrategy{ - until: t, - } -} - -func (s *timeoutValidStrategy) IsValid() bool { - return s.until.After(time.Now()) -} - -func (s *timeoutValidStrategy) Invalidate() { - s.until = time.Time{} -} - type ServerSpec struct { - sync.RWMutex - dest net.Destination - users []*MemoryUser - valid ValidationStrategy + Destination net.Destination + User *MemoryUser } -func NewServerSpec(dest net.Destination, valid ValidationStrategy, users ...*MemoryUser) *ServerSpec { +func NewServerSpec(dest net.Destination, user *MemoryUser) *ServerSpec { return &ServerSpec{ - dest: dest, - users: users, - valid: valid, + Destination: dest, + User: user, } } func NewServerSpecFromPB(spec *ServerEndpoint) (*ServerSpec, error) { dest := net.TCPDestination(spec.Address.AsAddress(), net.Port(spec.Port)) - mUsers := make([]*MemoryUser, len(spec.User)) - for idx, u := range spec.User { - mUser, err := u.ToMemoryUser() + var dUser *MemoryUser + if spec.User != nil { + user, err := spec.User.ToMemoryUser() if err != nil { return nil, err } - mUsers[idx] = mUser + dUser = user } - return NewServerSpec(dest, AlwaysValid(), mUsers...), nil -} - -func (s *ServerSpec) Destination() net.Destination { - return s.dest -} - -func (s *ServerSpec) HasUser(user *MemoryUser) bool { - s.RLock() - defer s.RUnlock() - - for _, u := range s.users { - if u.Account.Equals(user.Account) { - return true - } - } - return false -} - -func (s *ServerSpec) AddUser(user *MemoryUser) { - if s.HasUser(user) { - return - } - - s.Lock() - defer s.Unlock() - - s.users = append(s.users, user) -} - -func (s *ServerSpec) PickUser() *MemoryUser { - s.RLock() - defer s.RUnlock() - - userCount := len(s.users) - switch userCount { - case 0: - return nil - case 1: - return s.users[0] - default: - return s.users[dice.Roll(userCount)] - } -} - -func (s *ServerSpec) IsValid() bool { - return s.valid.IsValid() -} - -func (s *ServerSpec) Invalidate() { - s.valid.Invalidate() + return NewServerSpec(dest, dUser), nil } diff --git a/common/protocol/server_spec.pb.go b/common/protocol/server_spec.pb.go index f196930e..4a98aa05 100644 --- a/common/protocol/server_spec.pb.go +++ b/common/protocol/server_spec.pb.go @@ -28,7 +28,7 @@ type ServerEndpoint struct { Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` - User []*User `protobuf:"bytes,3,rep,name=user,proto3" json:"user,omitempty"` + User *User `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"` } func (x *ServerEndpoint) Reset() { @@ -75,7 +75,7 @@ func (x *ServerEndpoint) GetPort() uint32 { return 0 } -func (x *ServerEndpoint) GetUser() []*User { +func (x *ServerEndpoint) GetUser() *User { if x != nil { return x.User } @@ -98,7 +98,7 @@ var file_common_protocol_server_spec_proto_rawDesc = []byte{ 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x49, 0x50, 0x4f, 0x72, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, - 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x42, 0x5e, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, diff --git a/common/protocol/server_spec.proto b/common/protocol/server_spec.proto index 6b9cc04c..79c6e58e 100644 --- a/common/protocol/server_spec.proto +++ b/common/protocol/server_spec.proto @@ -12,5 +12,5 @@ import "common/protocol/user.proto"; message ServerEndpoint { xray.common.net.IPOrDomain address = 1; uint32 port = 2; - repeated xray.common.protocol.User user = 3; + xray.common.protocol.User user = 3; } diff --git a/common/protocol/server_spec_test.go b/common/protocol/server_spec_test.go deleted file mode 100644 index 10dfb8c5..00000000 --- a/common/protocol/server_spec_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package protocol_test - -import ( - "strings" - "testing" - "time" - - "github.com/xtls/xray-core/common" - "github.com/xtls/xray-core/common/net" - . "github.com/xtls/xray-core/common/protocol" - "github.com/xtls/xray-core/common/uuid" - "github.com/xtls/xray-core/proxy/vmess" -) - -func TestAlwaysValidStrategy(t *testing.T) { - strategy := AlwaysValid() - if !strategy.IsValid() { - t.Error("strategy not valid") - } - strategy.Invalidate() - if !strategy.IsValid() { - t.Error("strategy not valid") - } -} - -func TestTimeoutValidStrategy(t *testing.T) { - strategy := BeforeTime(time.Now().Add(2 * time.Second)) - if !strategy.IsValid() { - t.Error("strategy not valid") - } - time.Sleep(3 * time.Second) - if strategy.IsValid() { - t.Error("strategy is valid") - } - - strategy = BeforeTime(time.Now().Add(2 * time.Second)) - strategy.Invalidate() - if strategy.IsValid() { - t.Error("strategy is valid") - } -} - -func TestUserInServerSpec(t *testing.T) { - uuid1 := uuid.New() - uuid2 := uuid.New() - - toAccount := func(a *vmess.Account) Account { - account, err := a.AsAccount() - common.Must(err) - return account - } - - spec := NewServerSpec(net.Destination{}, AlwaysValid(), &MemoryUser{ - Email: "test1@example.com", - Account: toAccount(&vmess.Account{Id: uuid1.String()}), - }) - if spec.HasUser(&MemoryUser{ - Email: "test1@example.com", - Account: toAccount(&vmess.Account{Id: uuid2.String()}), - }) { - t.Error("has user: ", uuid2) - } - - spec.AddUser(&MemoryUser{Email: "test2@example.com"}) - if !spec.HasUser(&MemoryUser{ - Email: "test1@example.com", - Account: toAccount(&vmess.Account{Id: uuid1.String()}), - }) { - t.Error("not having user: ", uuid1) - } -} - -func TestPickUser(t *testing.T) { - spec := NewServerSpec(net.Destination{}, AlwaysValid(), &MemoryUser{Email: "test1@example.com"}, &MemoryUser{Email: "test2@example.com"}, &MemoryUser{Email: "test3@example.com"}) - user := spec.PickUser() - if !strings.HasSuffix(user.Email, "@example.com") { - t.Error("user: ", user.Email) - } -} diff --git a/core/xray_test.go b/core/xray_test.go index f4cb11ab..65ff3fc8 100644 --- a/core/xray_test.go +++ b/core/xray_test.go @@ -63,17 +63,13 @@ func TestXrayClose(t *testing.T) { Outbound: []*OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(0), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(0), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), diff --git a/features/inbound/inbound.go b/features/inbound/inbound.go index 1d3ba7b1..11caf3f9 100644 --- a/features/inbound/inbound.go +++ b/features/inbound/inbound.go @@ -4,7 +4,6 @@ import ( "context" "github.com/xtls/xray-core/common" - "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/features" ) @@ -20,9 +19,6 @@ type Handler interface { ReceiverSettings() *serial.TypedMessage // Returns the active proxy settings. ProxySettings() *serial.TypedMessage - - // Deprecated: Do not use in new code. - GetRandomInboundProxy() (interface{}, net.Port, int) } // Manager is a feature that manages InboundHandlers. diff --git a/infra/conf/http.go b/infra/conf/http.go index d4092f88..7da8f3f5 100644 --- a/infra/conf/http.go +++ b/infra/conf/http.go @@ -68,12 +68,19 @@ func (v *HTTPClientConfig) Build() (proto.Message, error) { { Address: v.Address, Port: v.Port, - Users: []json.RawMessage{{}}, }, } + if len(v.Username) > 0 { + v.Servers[0].Users = []json.RawMessage{{}} + } } - config.Server = make([]*protocol.ServerEndpoint, len(v.Servers)) - for idx, serverConfig := range v.Servers { + if len(v.Servers) != 1 { + return nil, errors.New(`HTTP settings: "servers" should have one and only one member. Multiple endpoints in "servers" should use multiple HTTP outbounds and routing balancer instead`) + } + for _, serverConfig := range v.Servers { + if len(serverConfig.Users) > 1 { + return nil, errors.New(`HTTP servers: "users" should have one member at most. Multiple members in "users" should use multiple HTTP outbounds and routing balancer instead`) + } server := &protocol.ServerEndpoint{ Address: serverConfig.Address.Build(), Port: uint32(serverConfig.Port), @@ -98,9 +105,11 @@ func (v *HTTPClientConfig) Build() (proto.Message, error) { } } user.Account = serial.ToTypedMessage(account.Build()) - server.User = append(server.User, user) + server.User = user + break } - config.Server[idx] = server + config.Server = server + break } config.Header = make([]*http.Header, 0, 32) for key, value := range v.Headers { diff --git a/infra/conf/shadowsocks.go b/infra/conf/shadowsocks.go index a05c1dc5..4a04d977 100644 --- a/infra/conf/shadowsocks.go +++ b/infra/conf/shadowsocks.go @@ -172,16 +172,16 @@ type ShadowsocksServerTarget struct { } type ShadowsocksClientConfig struct { - Address *Address `json:"address"` - Port uint16 `json:"port"` - Level byte `json:"level"` - Email string `json:"email"` - Cipher string `json:"method"` - Password string `json:"password"` - IVCheck bool `json:"ivCheck"` - UoT bool `json:"uot"` - UoTVersion int `json:"uotVersion"` - Servers []*ShadowsocksServerTarget `json:"servers"` + Address *Address `json:"address"` + Port uint16 `json:"port"` + Level byte `json:"level"` + Email string `json:"email"` + Cipher string `json:"method"` + Password string `json:"password"` + IVCheck bool `json:"ivCheck"` + UoT bool `json:"uot"` + UoTVersion int `json:"uotVersion"` + Servers []*ShadowsocksServerTarget `json:"servers"` } func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { @@ -200,8 +200,8 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { }, } } - if len(v.Servers) == 0 { - return nil, errors.New("0 Shadowsocks server configured.") + if len(v.Servers) != 1 { + return nil, errors.New(`Shadowsocks settings: "servers" should have one and only one member. Multiple endpoints in "servers" should use multiple Shadowsocks outbounds and routing balancer instead`) } if len(v.Servers) == 1 { @@ -229,8 +229,7 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { } config := new(shadowsocks.ClientConfig) - serverSpecs := make([]*protocol.ServerEndpoint, len(v.Servers)) - for idx, server := range v.Servers { + for _, server := range v.Servers { if C.Contains(shadowaead_2022.List, server.Cipher) { return nil, errors.New("Shadowsocks 2022 accept no multi servers") } @@ -256,19 +255,16 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { ss := &protocol.ServerEndpoint{ Address: server.Address.Build(), Port: uint32(server.Port), - User: []*protocol.User{ - { - Level: uint32(server.Level), - Email: server.Email, - Account: serial.ToTypedMessage(account), - }, + User: &protocol.User{ + Level: uint32(server.Level), + Email: server.Email, + Account: serial.ToTypedMessage(account), }, } - serverSpecs[idx] = ss + config.Server = ss + break } - config.Server = serverSpecs - return config, nil } diff --git a/infra/conf/socks.go b/infra/conf/socks.go index 6955001f..d0d68d81 100644 --- a/infra/conf/socks.go +++ b/infra/conf/socks.go @@ -86,12 +86,19 @@ func (v *SocksClientConfig) Build() (proto.Message, error) { { Address: v.Address, Port: v.Port, - Users: []json.RawMessage{{}}, }, } + if len(v.Username) > 0 { + v.Servers[0].Users = []json.RawMessage{{}} + } } - config.Server = make([]*protocol.ServerEndpoint, len(v.Servers)) - for idx, serverConfig := range v.Servers { + if len(v.Servers) != 1 { + return nil, errors.New(`SOCKS settings: "servers" should have one and only one member. Multiple endpoints in "servers" should use multiple SOCKS outbounds and routing balancer instead`) + } + for _, serverConfig := range v.Servers { + if len(serverConfig.Users) > 1 { + return nil, errors.New(`SOCKS servers: "users" should have one member at most. Multiple members in "users" should use multiple SOCKS outbounds and routing balancer instead`) + } server := &protocol.ServerEndpoint{ Address: serverConfig.Address.Build(), Port: uint32(serverConfig.Port), @@ -116,9 +123,11 @@ func (v *SocksClientConfig) Build() (proto.Message, error) { } } user.Account = serial.ToTypedMessage(account.Build()) - server.User = append(server.User, user) + server.User = user + break } - config.Server[idx] = server + config.Server = server + break } return config, nil } diff --git a/infra/conf/socks_test.go b/infra/conf/socks_test.go index da609de8..d94802ec 100644 --- a/infra/conf/socks_test.go +++ b/infra/conf/socks_test.go @@ -65,24 +65,20 @@ func TestSocksOutboundConfig(t *testing.T) { }`, Parser: loadJSON(creator), Output: &socks.ClientConfig{ - Server: []*protocol.ServerEndpoint{ - { - Address: &net.IPOrDomain{ - Address: &net.IPOrDomain_Ip{ - Ip: []byte{127, 0, 0, 1}, - }, - }, - Port: 1234, - User: []*protocol.User{ - { - Email: "test@email.com", - Account: serial.ToTypedMessage(&socks.Account{ - Username: "test user", - Password: "test pass", - }), - }, + Server: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, }, }, + Port: 1234, + User: &protocol.User{ + Email: "test@email.com", + Account: serial.ToTypedMessage(&socks.Account{ + Username: "test user", + Password: "test pass", + }), + }, }, }, }, @@ -96,24 +92,20 @@ func TestSocksOutboundConfig(t *testing.T) { }`, Parser: loadJSON(creator), Output: &socks.ClientConfig{ - Server: []*protocol.ServerEndpoint{ - { - Address: &net.IPOrDomain{ - Address: &net.IPOrDomain_Ip{ - Ip: []byte{127, 0, 0, 1}, - }, - }, - Port: 1234, - User: []*protocol.User{ - { - Email: "test@email.com", - Account: serial.ToTypedMessage(&socks.Account{ - Username: "test user", - Password: "test pass", - }), - }, + Server: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, }, }, + Port: 1234, + User: &protocol.User{ + Email: "test@email.com", + Account: serial.ToTypedMessage(&socks.Account{ + Username: "test user", + Password: "test pass", + }), + }, }, }, }, diff --git a/infra/conf/trojan.go b/infra/conf/trojan.go index d7d4ea17..d9907019 100644 --- a/infra/conf/trojan.go +++ b/infra/conf/trojan.go @@ -28,13 +28,13 @@ type TrojanServerTarget struct { // TrojanClientConfig is configuration of trojan servers type TrojanClientConfig struct { - Address *Address `json:"address"` - Port uint16 `json:"port"` - Level byte `json:"level"` - Email string `json:"email"` - Password string `json:"password"` - Flow string `json:"flow"` - Servers []*TrojanServerTarget `json:"servers"` + Address *Address `json:"address"` + Port uint16 `json:"port"` + Level byte `json:"level"` + Email string `json:"email"` + Password string `json:"password"` + Flow string `json:"flow"` + Servers []*TrojanServerTarget `json:"servers"` } // Build implements Buildable @@ -51,15 +51,13 @@ func (c *TrojanClientConfig) Build() (proto.Message, error) { }, } } - if len(c.Servers) == 0 { - return nil, errors.New("0 Trojan server configured.") + if len(c.Servers) != 1 { + return nil, errors.New(`Trojan settings: "servers" should have one and only one member. Multiple endpoints in "servers" should use multiple Trojan outbounds and routing balancer instead`) } - config := &trojan.ClientConfig{ - Server: make([]*protocol.ServerEndpoint, len(c.Servers)), - } + config := &trojan.ClientConfig{} - for idx, rec := range c.Servers { + for _, rec := range c.Servers { if rec.Address == nil { return nil, errors.New("Trojan server address is not set.") } @@ -73,19 +71,19 @@ func (c *TrojanClientConfig) Build() (proto.Message, error) { return nil, errors.PrintRemovedFeatureError(`Flow for Trojan`, ``) } - config.Server[idx] = &protocol.ServerEndpoint{ + config.Server = &protocol.ServerEndpoint{ Address: rec.Address.Build(), Port: uint32(rec.Port), - User: []*protocol.User{ - { - Level: uint32(rec.Level), - Email: rec.Email, - Account: serial.ToTypedMessage(&trojan.Account{ - Password: rec.Password, - }), - }, + User: &protocol.User{ + Level: uint32(rec.Level), + Email: rec.Email, + Account: serial.ToTypedMessage(&trojan.Account{ + Password: rec.Password, + }), }, } + + break } return config, nil diff --git a/infra/conf/vless.go b/infra/conf/vless.go index aa410e5c..efb480a0 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -228,22 +228,20 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { } } if len(c.Vnext) != 1 { - return nil, errors.New(`VLESS settings: "vnext" should have one and only one member`) + return nil, errors.New(`VLESS settings: "vnext" should have one and only one member. Multiple endpoints in "vnext" should use multiple VLESS outbounds and routing balancer instead`) } - config.Vnext = make([]*protocol.ServerEndpoint, len(c.Vnext)) - for idx, rec := range c.Vnext { + for _, rec := range c.Vnext { if rec.Address == nil { return nil, errors.New(`VLESS vnext: "address" is not set`) } if len(rec.Users) != 1 { - return nil, errors.New(`VLESS vnext: "users" should have one and only one member`) + return nil, errors.New(`VLESS vnext: "users" should have one and only one member. Multiple members in "users" should use multiple VLESS outbounds and routing balancer instead`) } spec := &protocol.ServerEndpoint{ Address: rec.Address.Build(), Port: uint32(rec.Port), - User: make([]*protocol.User, len(rec.Users)), } - for idx, rawUser := range rec.Users { + for _, rawUser := range rec.Users { user := new(protocol.User) if c.Address != nil { user.Level = c.Level @@ -327,9 +325,11 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { } user.Account = serial.ToTypedMessage(account) - spec.User[idx] = user + spec.User = user + break } - config.Vnext[idx] = spec + config.Vnext = spec + break } return config, nil diff --git a/infra/conf/vless_test.go b/infra/conf/vless_test.go index 480d3b86..2fd97915 100644 --- a/infra/conf/vless_test.go +++ b/infra/conf/vless_test.go @@ -35,25 +35,21 @@ func TestVLessOutbound(t *testing.T) { }`, Parser: loadJSON(creator), Output: &outbound.Config{ - Vnext: []*protocol.ServerEndpoint{ - { - Address: &net.IPOrDomain{ - Address: &net.IPOrDomain_Domain{ - Domain: "example.com", - }, - }, - Port: 443, - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vless.Account{ - Id: "27848739-7e62-4138-9fd3-098a63964b6b", - Flow: "xtls-rprx-vision-udp443", - Encryption: "none", - }), - Level: 0, - }, + Vnext: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Domain{ + Domain: "example.com", }, }, + Port: 443, + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: "27848739-7e62-4138-9fd3-098a63964b6b", + Flow: "xtls-rprx-vision-udp443", + Encryption: "none", + }), + Level: 0, + }, }, }, }, @@ -68,25 +64,21 @@ func TestVLessOutbound(t *testing.T) { }`, Parser: loadJSON(creator), Output: &outbound.Config{ - Vnext: []*protocol.ServerEndpoint{ - { - Address: &net.IPOrDomain{ - Address: &net.IPOrDomain_Domain{ - Domain: "example.com", - }, - }, - Port: 443, - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vless.Account{ - Id: "27848739-7e62-4138-9fd3-098a63964b6b", - Flow: "xtls-rprx-vision-udp443", - Encryption: "none", - }), - Level: 0, - }, + Vnext: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Domain{ + Domain: "example.com", }, }, + Port: 443, + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: "27848739-7e62-4138-9fd3-098a63964b6b", + Flow: "xtls-rprx-vision-udp443", + Encryption: "none", + }), + Level: 0, + }, }, }, }, diff --git a/infra/conf/vmess.go b/infra/conf/vmess.go index c0171c4b..4db061de 100644 --- a/infra/conf/vmess.go +++ b/infra/conf/vmess.go @@ -46,17 +46,6 @@ func (a *VMessAccount) Build() *vmess.Account { } } -type VMessDetourConfig struct { - ToTag string `json:"to"` -} - -// Build implements Buildable -func (c *VMessDetourConfig) Build() *inbound.DetourConfig { - return &inbound.DetourConfig{ - To: c.ToTag, - } -} - type VMessDefaultConfig struct { Level byte `json:"level"` } @@ -71,7 +60,6 @@ func (c *VMessDefaultConfig) Build() *inbound.DefaultConfig { type VMessInboundConfig struct { Users []json.RawMessage `json:"clients"` Defaults *VMessDefaultConfig `json:"default"` - DetourConfig *VMessDetourConfig `json:"detour"` } // Build implements Buildable @@ -82,10 +70,6 @@ func (c *VMessInboundConfig) Build() (proto.Message, error) { config.Default = c.Defaults.Build() } - if c.DetourConfig != nil { - config.Detour = c.DetourConfig.Build() - } - config.User = make([]*protocol.User, len(c.Users)) for idx, rawData := range c.Users { user := new(protocol.User) @@ -139,16 +123,15 @@ func (c *VMessOutboundConfig) Build() (proto.Message, error) { }, } } - if len(c.Receivers) == 0 { - return nil, errors.New("0 VMess receiver configured") + if len(c.Receivers) != 1 { + return nil, errors.New(`VMess settings: "vnext" should have one and only one member. Multiple endpoints in "vnext" should use multiple VMess outbounds and routing balancer instead`) } - serverSpecs := make([]*protocol.ServerEndpoint, len(c.Receivers)) - for idx, rec := range c.Receivers { - if len(rec.Users) == 0 { - return nil, errors.New("0 user configured for VMess outbound") + for _, rec := range c.Receivers { + if len(rec.Users) != 1 { + return nil, errors.New(`VMess vnext: "users" should have one and only one member. Multiple members in "users" should use multiple VMess outbounds and routing balancer instead`) } if rec.Address == nil { - return nil, errors.New("address is not set in VMess outbound config") + return nil, errors.New(`VMess vnext: "address" is not set`) } spec := &protocol.ServerEndpoint{ Address: rec.Address.Build(), @@ -182,10 +165,11 @@ func (c *VMessOutboundConfig) Build() (proto.Message, error) { account.ID = u.String() user.Account = serial.ToTypedMessage(account.Build()) - spec.User = append(spec.User, user) + spec.User = user + break } - serverSpecs[idx] = spec + config.Receiver = spec + break } - config.Receiver = serverSpecs return config, nil } diff --git a/infra/conf/vmess_test.go b/infra/conf/vmess_test.go index e3bf1349..b94db83e 100644 --- a/infra/conf/vmess_test.go +++ b/infra/conf/vmess_test.go @@ -34,26 +34,22 @@ func TestVMessOutbound(t *testing.T) { }`, Parser: loadJSON(creator), Output: &outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: &net.IPOrDomain{ - Address: &net.IPOrDomain_Ip{ - Ip: []byte{127, 0, 0, 1}, - }, + Receiver: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, }, - Port: 80, - User: []*protocol.User{ - { - Email: "love@example.com", - Level: 255, - Account: serial.ToTypedMessage(&vmess.Account{ - Id: "e641f5ad-9397-41e3-bf1a-e8740dfed019", - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AUTO, - }, - }), + }, + Port: 80, + User: &protocol.User{ + Email: "love@example.com", + Level: 255, + Account: serial.ToTypedMessage(&vmess.Account{ + Id: "e641f5ad-9397-41e3-bf1a-e8740dfed019", + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AUTO, }, - }, + }), }, }, }, @@ -68,26 +64,22 @@ func TestVMessOutbound(t *testing.T) { }`, Parser: loadJSON(creator), Output: &outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: &net.IPOrDomain{ - Address: &net.IPOrDomain_Ip{ - Ip: []byte{127, 0, 0, 1}, - }, + Receiver: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, }, - Port: 80, - User: []*protocol.User{ - { - Email: "love@example.com", - Level: 255, - Account: serial.ToTypedMessage(&vmess.Account{ - Id: "e641f5ad-9397-41e3-bf1a-e8740dfed019", - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AUTO, - }, - }), + }, + Port: 80, + User: &protocol.User{ + Email: "love@example.com", + Level: 255, + Account: serial.ToTypedMessage(&vmess.Account{ + Id: "e641f5ad-9397-41e3-bf1a-e8740dfed019", + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AUTO, }, - }, + }), }, }, }, @@ -113,11 +105,7 @@ func TestVMessInbound(t *testing.T) { ], "default": { "level": 0 - }, - "detour": { - "to": "tag_to_detour" - }, - "disableInsecureEncryption": true + } }`, Parser: loadJSON(creator), Output: &inbound.Config{ @@ -136,9 +124,6 @@ func TestVMessInbound(t *testing.T) { Default: &inbound.DefaultConfig{ Level: 0, }, - Detour: &inbound.DetourConfig{ - To: "tag_to_detour", - }, }, }, }) diff --git a/infra/conf/xray.go b/infra/conf/xray.go index 88f01322..e9cacc87 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -3,7 +3,6 @@ package conf import ( "context" "encoding/json" - "fmt" "log" "os" "path/filepath" @@ -120,47 +119,12 @@ func (m *MuxConfig) Build() (*proxyman.MultiplexingConfig, error) { }, nil } -type InboundDetourAllocationConfig struct { - Strategy string `json:"strategy"` - Concurrency *uint32 `json:"concurrency"` - RefreshMin *uint32 `json:"refresh"` -} - -// Build implements Buildable. -func (c *InboundDetourAllocationConfig) Build() (*proxyman.AllocationStrategy, error) { - config := new(proxyman.AllocationStrategy) - switch strings.ToLower(c.Strategy) { - case "always": - config.Type = proxyman.AllocationStrategy_Always - case "random": - config.Type = proxyman.AllocationStrategy_Random - case "external": - config.Type = proxyman.AllocationStrategy_External - default: - return nil, errors.New("unknown allocation strategy: ", c.Strategy) - } - if c.Concurrency != nil { - config.Concurrency = &proxyman.AllocationStrategy_AllocationStrategyConcurrency{ - Value: *c.Concurrency, - } - } - - if c.RefreshMin != nil { - config.Refresh = &proxyman.AllocationStrategy_AllocationStrategyRefresh{ - Value: *c.RefreshMin, - } - } - - return config, nil -} - type InboundDetourConfig struct { Protocol string `json:"protocol"` PortList *PortList `json:"port"` ListenOn *Address `json:"listen"` Settings *json.RawMessage `json:"settings"` Tag string `json:"tag"` - Allocation *InboundDetourAllocationConfig `json:"allocate"` StreamSetting *StreamConfig `json:"streamSettings"` SniffingConfig *SniffingConfig `json:"sniffing"` } @@ -197,30 +161,6 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) { } } - if c.Allocation != nil { - concurrency := -1 - if c.Allocation.Concurrency != nil && c.Allocation.Strategy == "random" { - concurrency = int(*c.Allocation.Concurrency) - } - portRange := 0 - - for _, pr := range c.PortList.Range { - portRange += int(pr.To - pr.From + 1) - } - if concurrency >= 0 && concurrency >= portRange { - var ports strings.Builder - for _, pr := range c.PortList.Range { - fmt.Fprintf(&ports, "%d-%d ", pr.From, pr.To) - } - return nil, errors.New("not enough ports. concurrency = ", concurrency, " ports: ", ports.String()) - } - - as, err := c.Allocation.Build() - if err != nil { - return nil, err - } - receiverSettings.AllocationStrategy = as - } if c.StreamSetting != nil { ss, err := c.StreamSetting.Build() if err != nil { diff --git a/infra/conf/xray_test.go b/infra/conf/xray_test.go index 1c05ec2e..c1349fd2 100644 --- a/infra/conf/xray_test.go +++ b/infra/conf/xray_test.go @@ -58,10 +58,6 @@ func TestXrayConfig(t *testing.T) { }, "protocol": "vmess", "port": "443-500", - "allocate": { - "strategy": "random", - "concurrency": 3 - }, "settings": { "clients": [ { @@ -123,12 +119,6 @@ func TestXrayConfig(t *testing.T) { From: 443, To: 500, }}}, - AllocationStrategy: &proxyman.AllocationStrategy{ - Type: proxyman.AllocationStrategy_Random, - Concurrency: &proxyman.AllocationStrategy_AllocationStrategyConcurrency{ - Value: 3, - }, - }, StreamSettings: &internet.StreamConfig{ ProtocolName: "websocket", TransportSettings: []*internet.TransportConfig{ diff --git a/proxy/http/client.go b/proxy/http/client.go index b1326bec..d5be2acb 100644 --- a/proxy/http/client.go +++ b/proxy/http/client.go @@ -31,7 +31,7 @@ import ( ) type Client struct { - serverPicker protocol.ServerPicker + server *protocol.ServerSpec policyManager policy.Manager header []*Header } @@ -48,21 +48,17 @@ var ( // NewClient create a new http client based on the given config. func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { - serverList := protocol.NewServerList() - for _, rec := range config.Server { - s, err := protocol.NewServerSpecFromPB(rec) - if err != nil { - return nil, errors.New("failed to get server spec").Base(err) - } - serverList.AddServer(s) + if config.Server == nil { + return nil, errors.New(`no target server found`) } - if serverList.Size() == 0 { - return nil, errors.New("0 target server") + server, err := protocol.NewServerSpecFromPB(config.Server) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err) } v := core.MustFromContext(ctx) return &Client{ - serverPicker: protocol.NewRoundRobinServerPicker(serverList), + server: server, policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), header: config.Header, }, nil @@ -84,7 +80,9 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter return errors.New("UDP is not supported by HTTP outbound") } - var user *protocol.MemoryUser + server := c.server + dest := server.Destination + user := server.User var conn stat.Connection mbuf, _ := link.Reader.ReadMultiBuffer() @@ -102,10 +100,6 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter } if err := retry.ExponentialBackoff(5, 100).On(func() error { - server := c.serverPicker.PickServer() - dest := server.Destination() - user = server.PickUser() - netConn, err := setUpHTTPTunnel(ctx, dest, targetAddr, user, dialer, header, firstPayload) if netConn != nil { if _, ok := netConn.(*http2Conn); !ok { diff --git a/proxy/http/config.pb.go b/proxy/http/config.pb.go index 03e03441..aef3a759 100644 --- a/proxy/http/config.pb.go +++ b/proxy/http/config.pb.go @@ -196,8 +196,8 @@ type ClientConfig struct { unknownFields protoimpl.UnknownFields // Sever is a list of HTTP server addresses. - Server []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=server,proto3" json:"server,omitempty"` - Header []*Header `protobuf:"bytes,2,rep,name=header,proto3" json:"header,omitempty"` + Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + Header []*Header `protobuf:"bytes,2,rep,name=header,proto3" json:"header,omitempty"` } func (x *ClientConfig) Reset() { @@ -230,7 +230,7 @@ func (*ClientConfig) Descriptor() ([]byte, []int) { return file_proxy_http_config_proto_rawDescGZIP(), []int{3} } -func (x *ClientConfig) GetServer() []*protocol.ServerEndpoint { +func (x *ClientConfig) GetServer() *protocol.ServerEndpoint { if x != nil { return x.Server } @@ -275,7 +275,7 @@ var file_proxy_http_config_proto_rawDesc = []byte{ 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x7d, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x2f, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, diff --git a/proxy/http/config.proto b/proxy/http/config.proto index bc939df7..240c06b5 100644 --- a/proxy/http/config.proto +++ b/proxy/http/config.proto @@ -28,6 +28,6 @@ message Header { // ClientConfig is the protobuf config for HTTP proxy client. message ClientConfig { // Sever is a list of HTTP server addresses. - repeated xray.common.protocol.ServerEndpoint server = 1; + xray.common.protocol.ServerEndpoint server = 1; repeated Header header = 2; } diff --git a/proxy/shadowsocks/client.go b/proxy/shadowsocks/client.go index c2ad1c1c..29cb0456 100644 --- a/proxy/shadowsocks/client.go +++ b/proxy/shadowsocks/client.go @@ -22,27 +22,23 @@ import ( // Client is a inbound handler for Shadowsocks protocol type Client struct { - serverPicker protocol.ServerPicker + server *protocol.ServerSpec policyManager policy.Manager } // NewClient create a new Shadowsocks client. func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { - serverList := protocol.NewServerList() - for _, rec := range config.Server { - s, err := protocol.NewServerSpecFromPB(rec) - if err != nil { - return nil, errors.New("failed to parse server spec").Base(err) - } - serverList.AddServer(s) + if config.Server == nil { + return nil, errors.New(`no target server found`) } - if serverList.Size() == 0 { - return nil, errors.New("0 server") + server, err := protocol.NewServerSpecFromPB(config.Server) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err) } v := core.MustFromContext(ctx) client := &Client{ - serverPicker: protocol.NewRoundRobinServerPicker(serverList), + server: server, policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), } return client, nil @@ -60,13 +56,12 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter destination := ob.Target network := destination.Network - var server *protocol.ServerSpec + server := c.server + dest := server.Destination + dest.Network = network var conn stat.Connection err := retry.ExponentialBackoff(5, 100).On(func() error { - server = c.serverPicker.PickServer() - dest := server.Destination() - dest.Network = network rawConn, err := dialer.Dial(ctx, dest) if err != nil { return err @@ -78,7 +73,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter if err != nil { return errors.New("failed to find an available destination").AtWarning().Base(err) } - errors.LogInfo(ctx, "tunneling request to ", destination, " via ", network, ":", server.Destination().NetAddr()) + errors.LogInfo(ctx, "tunneling request to ", destination, " via ", network, ":", server.Destination.NetAddr()) defer conn.Close() @@ -93,7 +88,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter request.Command = protocol.RequestCommandUDP } - user := server.PickUser() + user := server.User _, ok := user.Account.(*MemoryAccount) if !ok { return errors.New("user account is not valid") diff --git a/proxy/shadowsocks/config.pb.go b/proxy/shadowsocks/config.pb.go index 56cddb67..3419c509 100644 --- a/proxy/shadowsocks/config.pb.go +++ b/proxy/shadowsocks/config.pb.go @@ -199,7 +199,7 @@ type ClientConfig struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Server []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=server,proto3" json:"server,omitempty"` + Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` } func (x *ClientConfig) Reset() { @@ -232,7 +232,7 @@ func (*ClientConfig) Descriptor() ([]byte, []int) { return file_proxy_shadowsocks_config_proto_rawDescGZIP(), []int{2} } -func (x *ClientConfig) GetServer() []*protocol.ServerEndpoint { +func (x *ClientConfig) GetServer() *protocol.ServerEndpoint { if x != nil { return x.Server } @@ -268,7 +268,7 @@ var file_proxy_shadowsocks_config_proto_rawDesc = []byte{ 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x22, 0x4c, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x3c, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x3c, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2a, 0x74, 0x0a, diff --git a/proxy/shadowsocks/config.proto b/proxy/shadowsocks/config.proto index 8f9fb891..879fb77b 100644 --- a/proxy/shadowsocks/config.proto +++ b/proxy/shadowsocks/config.proto @@ -32,5 +32,5 @@ message ServerConfig { } message ClientConfig { - repeated xray.common.protocol.ServerEndpoint server = 1; + xray.common.protocol.ServerEndpoint server = 1; } diff --git a/proxy/socks/client.go b/proxy/socks/client.go index 232215ec..eac3f35b 100644 --- a/proxy/socks/client.go +++ b/proxy/socks/client.go @@ -22,27 +22,23 @@ import ( // Client is a Socks5 client. type Client struct { - serverPicker protocol.ServerPicker + server *protocol.ServerSpec policyManager policy.Manager } // NewClient create a new Socks5 client based on the given config. func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { - serverList := protocol.NewServerList() - for _, rec := range config.Server { - s, err := protocol.NewServerSpecFromPB(rec) - if err != nil { - return nil, errors.New("failed to get server spec").Base(err) - } - serverList.AddServer(s) + if config.Server == nil { + return nil, errors.New(`no target server found`) } - if serverList.Size() == 0 { - return nil, errors.New("0 target server") + server, err := protocol.NewServerSpecFromPB(config.Server) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err) } v := core.MustFromContext(ctx) c := &Client{ - serverPicker: protocol.NewRoundRobinServerPicker(serverList), + server: server, policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), } @@ -62,15 +58,12 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter destination := ob.Target // Outbound server. - var server *protocol.ServerSpec - // Outbound server's destination. - var dest net.Destination + server := c.server + dest := server.Destination // Connection to the outbound server. var conn stat.Connection if err := retry.ExponentialBackoff(5, 100).On(func() error { - server = c.serverPicker.PickServer() - dest = server.Destination() rawConn, err := dialer.Dial(ctx, dest) if err != nil { return err @@ -101,7 +94,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter request.Command = protocol.RequestCommandUDP } - user := server.PickUser() + user := server.User if user != nil { request.User = user p = c.policyManager.ForLevel(user.Level) diff --git a/proxy/socks/config.pb.go b/proxy/socks/config.pb.go index ee94d0bc..4363c0ec 100644 --- a/proxy/socks/config.pb.go +++ b/proxy/socks/config.pb.go @@ -210,7 +210,7 @@ type ClientConfig struct { unknownFields protoimpl.UnknownFields // Sever is a list of Socks server addresses. - Server []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=server,proto3" json:"server,omitempty"` + Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` } func (x *ClientConfig) Reset() { @@ -243,7 +243,7 @@ func (*ClientConfig) Descriptor() ([]byte, []int) { return file_proxy_socks_config_proto_rawDescGZIP(), []int{2} } -func (x *ClientConfig) GetServer() []*protocol.ServerEndpoint { +func (x *ClientConfig) GetServer() *protocol.ServerEndpoint { if x != nil { return x.Server } @@ -286,7 +286,7 @@ var file_proxy_socks_config_proto_rawDesc = []byte{ 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x4c, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2a, 0x25, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, diff --git a/proxy/socks/config.proto b/proxy/socks/config.proto index 32d553d3..b53d565d 100644 --- a/proxy/socks/config.proto +++ b/proxy/socks/config.proto @@ -35,5 +35,5 @@ message ServerConfig { // ClientConfig is the protobuf config for Socks client. message ClientConfig { // Sever is a list of Socks server addresses. - repeated xray.common.protocol.ServerEndpoint server = 1; + xray.common.protocol.ServerEndpoint server = 1; } diff --git a/proxy/trojan/client.go b/proxy/trojan/client.go index a667d352..4af8c019 100644 --- a/proxy/trojan/client.go +++ b/proxy/trojan/client.go @@ -22,27 +22,23 @@ import ( // Client is a inbound handler for trojan protocol type Client struct { - serverPicker protocol.ServerPicker + server *protocol.ServerSpec policyManager policy.Manager } // NewClient create a new trojan client. func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { - serverList := protocol.NewServerList() - for _, rec := range config.Server { - s, err := protocol.NewServerSpecFromPB(rec) - if err != nil { - return nil, errors.New("failed to parse server spec").Base(err) - } - serverList.AddServer(s) + if config.Server == nil { + return nil, errors.New(`no target server found`) } - if serverList.Size() == 0 { - return nil, errors.New("0 server") + server, err := protocol.NewServerSpecFromPB(config.Server) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err) } v := core.MustFromContext(ctx) client := &Client{ - serverPicker: protocol.NewRoundRobinServerPicker(serverList), + server: server, policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), } return client, nil @@ -60,12 +56,11 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter destination := ob.Target network := destination.Network - var server *protocol.ServerSpec + server := c.server var conn stat.Connection err := retry.ExponentialBackoff(5, 100).On(func() error { - server = c.serverPicker.PickServer() - rawConn, err := dialer.Dial(ctx, server.Destination()) + rawConn, err := dialer.Dial(ctx, server.Destination) if err != nil { return err } @@ -76,11 +71,11 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter if err != nil { return errors.New("failed to find an available destination").AtWarning().Base(err) } - errors.LogInfo(ctx, "tunneling request to ", destination, " via ", server.Destination().NetAddr()) + errors.LogInfo(ctx, "tunneling request to ", destination, " via ", server.Destination.NetAddr()) defer conn.Close() - user := server.PickUser() + user := server.User account, ok := user.Account.(*MemoryAccount) if !ok { return errors.New("user account is not valid") diff --git a/proxy/trojan/config.pb.go b/proxy/trojan/config.pb.go index dd555ea4..d3a6f8b9 100644 --- a/proxy/trojan/config.pb.go +++ b/proxy/trojan/config.pb.go @@ -156,7 +156,7 @@ type ClientConfig struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Server []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=server,proto3" json:"server,omitempty"` + Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` } func (x *ClientConfig) Reset() { @@ -189,7 +189,7 @@ func (*ClientConfig) Descriptor() ([]byte, []int) { return file_proxy_trojan_config_proto_rawDescGZIP(), []int{2} } -func (x *ClientConfig) GetServer() []*protocol.ServerEndpoint { +func (x *ClientConfig) GetServer() *protocol.ServerEndpoint { if x != nil { return x.Server } @@ -271,7 +271,7 @@ var file_proxy_trojan_config_proto_rawDesc = []byte{ 0x04, 0x64, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x78, 0x76, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x78, 0x76, 0x65, 0x72, 0x22, 0x4c, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, + 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x7b, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x65, diff --git a/proxy/trojan/config.proto b/proxy/trojan/config.proto index 4229e2ed..30dedbf4 100644 --- a/proxy/trojan/config.proto +++ b/proxy/trojan/config.proto @@ -23,7 +23,7 @@ message Fallback { } message ClientConfig { - repeated xray.common.protocol.ServerEndpoint server = 1; + xray.common.protocol.ServerEndpoint server = 1; } message ServerConfig { diff --git a/proxy/vless/outbound/config.pb.go b/proxy/vless/outbound/config.pb.go index 76d2f768..ed5db785 100644 --- a/proxy/vless/outbound/config.pb.go +++ b/proxy/vless/outbound/config.pb.go @@ -26,7 +26,7 @@ type Config struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Vnext []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=vnext,proto3" json:"vnext,omitempty"` + Vnext *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=vnext,proto3" json:"vnext,omitempty"` } func (x *Config) Reset() { @@ -59,7 +59,7 @@ func (*Config) Descriptor() ([]byte, []int) { return file_proxy_vless_outbound_config_proto_rawDescGZIP(), []int{0} } -func (x *Config) GetVnext() []*protocol.ServerEndpoint { +func (x *Config) GetVnext() *protocol.ServerEndpoint { if x != nil { return x.Vnext } @@ -76,7 +76,7 @@ var file_proxy_vless_outbound_config_proto_rawDesc = []byte{ 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x44, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3a, 0x0a, 0x05, 0x76, - 0x6e, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, + 0x6e, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x05, 0x76, 0x6e, 0x65, 0x78, 0x74, 0x42, 0x6d, 0x0a, 0x1d, 0x63, 0x6f, 0x6d, 0x2e, 0x78, diff --git a/proxy/vless/outbound/config.proto b/proxy/vless/outbound/config.proto index 55455cbf..150b2d6c 100644 --- a/proxy/vless/outbound/config.proto +++ b/proxy/vless/outbound/config.proto @@ -9,5 +9,5 @@ option java_multiple_files = true; import "common/protocol/server_spec.proto"; message Config { - repeated xray.common.protocol.ServerEndpoint vnext = 1; + xray.common.protocol.ServerEndpoint vnext = 1; } diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 30b9dcf9..2d4e3ab6 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -47,8 +47,7 @@ func init() { // Handler is an outbound connection handler for VLess protocol. type Handler struct { - serverList *protocol.ServerList - serverPicker protocol.ServerPicker + server *protocol.ServerSpec policyManager policy.Manager cone bool encryption *encryption.ClientInstance @@ -57,24 +56,22 @@ type Handler struct { // New creates a new VLess outbound handler. func New(ctx context.Context, config *Config) (*Handler, error) { - serverList := protocol.NewServerList() - for _, rec := range config.Vnext { - s, err := protocol.NewServerSpecFromPB(rec) - if err != nil { - return nil, errors.New("failed to parse server spec").Base(err).AtError() - } - serverList.AddServer(s) + if config.Vnext == nil { + return nil, errors.New(`no vnext found`) + } + server, err := protocol.NewServerSpecFromPB(config.Vnext) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err).AtError() } v := core.MustFromContext(ctx) handler := &Handler{ - serverList: serverList, - serverPicker: protocol.NewRoundRobinServerPicker(serverList), + server: server, policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), cone: ctx.Value("cone").(bool), } - a := handler.serverPicker.PickServer().PickUser().Account.(*vless.MemoryAccount) + a := handler.server.User.Account.(*vless.MemoryAccount) if a.Encryption != "" && a.Encryption != "none" { s := strings.Split(a.Encryption, ".") var nfsPKeysBytes [][]byte @@ -125,12 +122,12 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte } ob.Name = "vless" - var rec *protocol.ServerSpec + rec := h.server var conn stat.Connection + if err := retry.ExponentialBackoff(5, 200).On(func() error { - rec = h.serverPicker.PickServer() var err error - conn, err = dialer.Dial(ctx, rec.Destination()) + conn, err = dialer.Dial(ctx, rec.Destination) if err != nil { return err } @@ -145,7 +142,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte iConn = statConn.Connection } 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()) if h.encryption != nil { var err error @@ -172,7 +169,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte request := &protocol.RequestHeader{ Version: encoding.Version, - User: rec.PickUser(), + User: rec.User, Command: command, Address: target.Address, Port: target.Port, diff --git a/proxy/vmess/encoding/commands.go b/proxy/vmess/encoding/commands.go index cdab871b..698052c5 100644 --- a/proxy/vmess/encoding/commands.go +++ b/proxy/vmess/encoding/commands.go @@ -7,10 +7,7 @@ import ( "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" - "github.com/xtls/xray-core/common/serial" - "github.com/xtls/xray-core/common/uuid" ) var ( @@ -29,9 +26,6 @@ func MarshalCommand(command interface{}, writer io.Writer) error { var cmdID byte var factory CommandFactory switch command.(type) { - case *protocol.CommandSwitchAccount: - factory = new(CommandSwitchAccountFactory) - cmdID = 1 default: return ErrUnknownCommand } @@ -67,8 +61,6 @@ func UnmarshalCommand(cmdID byte, data []byte) (protocol.ResponseCommand, error) var factory CommandFactory switch cmdID { - case 1: - factory = new(CommandSwitchAccountFactory) default: return nil, ErrUnknownCommand } @@ -79,67 +71,3 @@ type CommandFactory interface { Marshal(command interface{}, writer io.Writer) error Unmarshal(data []byte) (interface{}, error) } - -type CommandSwitchAccountFactory struct{} - -func (f *CommandSwitchAccountFactory) Marshal(command interface{}, writer io.Writer) error { - cmd, ok := command.(*protocol.CommandSwitchAccount) - if !ok { - return ErrCommandTypeMismatch - } - - hostStr := "" - if cmd.Host != nil { - hostStr = cmd.Host.String() - } - common.Must2(writer.Write([]byte{byte(len(hostStr))})) - - if len(hostStr) > 0 { - common.Must2(writer.Write([]byte(hostStr))) - } - - common.Must2(serial.WriteUint16(writer, cmd.Port.Value())) - - idBytes := cmd.ID.Bytes() - common.Must2(writer.Write(idBytes)) - common.Must2(serial.WriteUint16(writer, 0)) // compatible with legacy alterId - common.Must2(writer.Write([]byte{byte(cmd.Level)})) - - common.Must2(writer.Write([]byte{cmd.ValidMin})) - return nil -} - -func (f *CommandSwitchAccountFactory) Unmarshal(data []byte) (interface{}, error) { - cmd := new(protocol.CommandSwitchAccount) - if len(data) == 0 { - return nil, ErrInsufficientLength - } - lenHost := int(data[0]) - if len(data) < lenHost+1 { - return nil, ErrInsufficientLength - } - if lenHost > 0 { - cmd.Host = net.ParseAddress(string(data[1 : 1+lenHost])) - } - portStart := 1 + lenHost - if len(data) < portStart+2 { - return nil, ErrInsufficientLength - } - cmd.Port = net.PortFromBytes(data[portStart : portStart+2]) - idStart := portStart + 2 - if len(data) < idStart+16 { - return nil, ErrInsufficientLength - } - cmd.ID, _ = uuid.ParseBytes(data[idStart : idStart+16]) - levelStart := idStart + 16 + 2 - if len(data) < levelStart+1 { - return nil, ErrInsufficientLength - } - cmd.Level = uint32(data[levelStart]) - timeStart := levelStart + 1 - if len(data) < timeStart+1 { - return nil, ErrInsufficientLength - } - cmd.ValidMin = data[timeStart] - return cmd, nil -} diff --git a/proxy/vmess/encoding/commands_test.go b/proxy/vmess/encoding/commands_test.go deleted file mode 100644 index c5415959..00000000 --- a/proxy/vmess/encoding/commands_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package encoding_test - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/assert" - "github.com/xtls/xray-core/common" - "github.com/xtls/xray-core/common/buf" - "github.com/xtls/xray-core/common/protocol" - "github.com/xtls/xray-core/common/uuid" - . "github.com/xtls/xray-core/proxy/vmess/encoding" -) - -func TestSwitchAccount(t *testing.T) { - sa := &protocol.CommandSwitchAccount{ - Port: 1234, - ID: uuid.New(), - Level: 128, - ValidMin: 16, - } - - buffer := buf.New() - common.Must(MarshalCommand(sa, buffer)) - - cmd, err := UnmarshalCommand(1, buffer.BytesFrom(2)) - common.Must(err) - - sa2, ok := cmd.(*protocol.CommandSwitchAccount) - if !ok { - t.Fatal("failed to convert command to CommandSwitchAccount") - } - if r := cmp.Diff(sa2, sa); r != "" { - t.Error(r) - } -} - -func TestSwitchAccountBugOffByOne(t *testing.T) { - sa := &protocol.CommandSwitchAccount{ - Port: 1234, - ID: uuid.New(), - Level: 128, - ValidMin: 16, - } - - buffer := buf.New() - csaf := CommandSwitchAccountFactory{} - common.Must(csaf.Marshal(sa, buffer)) - - Payload := buffer.Bytes() - - cmd, err := csaf.Unmarshal(Payload[:len(Payload)-1]) - assert.Error(t, err) - assert.Nil(t, cmd) -} diff --git a/proxy/vmess/inbound/config.pb.go b/proxy/vmess/inbound/config.pb.go index 4aa259e8..b713d691 100644 --- a/proxy/vmess/inbound/config.pb.go +++ b/proxy/vmess/inbound/config.pb.go @@ -118,7 +118,6 @@ type Config struct { User []*protocol.User `protobuf:"bytes,1,rep,name=user,proto3" json:"user,omitempty"` Default *DefaultConfig `protobuf:"bytes,2,opt,name=default,proto3" json:"default,omitempty"` - Detour *DetourConfig `protobuf:"bytes,3,opt,name=detour,proto3" json:"detour,omitempty"` // 4 is for legacy setting } func (x *Config) Reset() { @@ -165,13 +164,6 @@ func (x *Config) GetDefault() *DefaultConfig { return nil } -func (x *Config) GetDetour() *DetourConfig { - if x != nil { - return x.Detour - } - return nil -} - var File_proxy_vmess_inbound_config_proto protoreflect.FileDescriptor var file_proxy_vmess_inbound_config_proto_rawDesc = []byte{ @@ -185,26 +177,21 @@ var file_proxy_vmess_inbound_config_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, 0x22, 0x25, 0x0a, 0x0d, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, - 0xbb, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2e, 0x0a, 0x04, 0x75, 0x73, - 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, - 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x07, 0x64, 0x65, - 0x66, 0x61, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x78, 0x72, - 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x69, - 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x2e, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x12, 0x3e, 0x0a, - 0x06, 0x64, 0x65, 0x74, 0x6f, 0x75, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, - 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6d, 0x65, 0x73, 0x73, - 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x2e, 0x44, 0x65, 0x74, 0x6f, 0x75, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x64, 0x65, 0x74, 0x6f, 0x75, 0x72, 0x42, 0x6a, 0x0a, - 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, - 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01, 0x5a, - 0x2d, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, - 0x2f, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0xaa, 0x02, - 0x18, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6d, 0x65, 0x73, - 0x73, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x7b, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2e, 0x0a, 0x04, 0x75, 0x73, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x55, + 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x07, 0x64, 0x65, 0x66, + 0x61, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x2e, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x42, 0x6a, 0x0a, 0x1c, + 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, + 0x6d, 0x65, 0x73, 0x73, 0x2e, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2d, + 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, + 0x76, 0x6d, 0x65, 0x73, 0x73, 0x2f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0xaa, 0x02, 0x18, + 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6d, 0x65, 0x73, 0x73, + 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -229,12 +216,11 @@ var file_proxy_vmess_inbound_config_proto_goTypes = []any{ var file_proxy_vmess_inbound_config_proto_depIdxs = []int32{ 3, // 0: xray.proxy.vmess.inbound.Config.user:type_name -> xray.common.protocol.User 1, // 1: xray.proxy.vmess.inbound.Config.default:type_name -> xray.proxy.vmess.inbound.DefaultConfig - 0, // 2: xray.proxy.vmess.inbound.Config.detour:type_name -> xray.proxy.vmess.inbound.DetourConfig - 3, // [3:3] is the sub-list for method output_type - 3, // [3:3] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_proxy_vmess_inbound_config_proto_init() } diff --git a/proxy/vmess/inbound/config.proto b/proxy/vmess/inbound/config.proto index 7da1d581..89311ddf 100644 --- a/proxy/vmess/inbound/config.proto +++ b/proxy/vmess/inbound/config.proto @@ -19,6 +19,4 @@ message DefaultConfig { message Config { repeated xray.common.protocol.User user = 1; DefaultConfig default = 2; - DetourConfig detour = 3; - // 4 is for legacy setting } diff --git a/proxy/vmess/inbound/inbound.go b/proxy/vmess/inbound/inbound.go index 28b560d8..7975551b 100644 --- a/proxy/vmess/inbound/inbound.go +++ b/proxy/vmess/inbound/inbound.go @@ -106,7 +106,6 @@ type Handler struct { inboundHandlerManager feature_inbound.Manager clients *vmess.TimedUserValidator usersByEmail *userByEmail - detours *DetourConfig sessionHistory *encoding.SessionHistory } @@ -117,7 +116,6 @@ func New(ctx context.Context, config *Config) (*Handler, error) { policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), inboundHandlerManager: v.GetFeature(feature_inbound.ManagerType()).(feature_inbound.Manager), clients: vmess.NewTimedUserValidator(), - detours: config.Detour, usersByEmail: newUserByEmail(config.GetDefaultValue()), sessionHistory: encoding.NewSessionHistory(), } @@ -323,38 +321,8 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s return nil } +// Stub command generator func (h *Handler) generateCommand(ctx context.Context, request *protocol.RequestHeader) protocol.ResponseCommand { - if h.detours != nil { - tag := h.detours.To - if h.inboundHandlerManager != nil { - handler, err := h.inboundHandlerManager.GetHandler(ctx, tag) - if err != nil { - errors.LogWarningInner(ctx, err, "failed to get detour handler: ", tag) - return nil - } - proxyHandler, port, availableMin := handler.GetRandomInboundProxy() - inboundHandler, ok := proxyHandler.(*Handler) - if ok && inboundHandler != nil { - if availableMin > 255 { - availableMin = 255 - } - - errors.LogDebug(ctx, "pick detour handler for port ", port, " for ", availableMin, " minutes.") - user := inboundHandler.GetOrGenerateUser(request.User.Email) - if user == nil { - return nil - } - account := user.Account.(*vmess.MemoryAccount) - return &protocol.CommandSwitchAccount{ - Port: port, - ID: account.ID.UUID(), - Level: user.Level, - ValidMin: byte(availableMin), - } - } - } - } - return nil } diff --git a/proxy/vmess/outbound/command.go b/proxy/vmess/outbound/command.go index 2d4747dc..c284bfb6 100644 --- a/proxy/vmess/outbound/command.go +++ b/proxy/vmess/outbound/command.go @@ -1,41 +1,14 @@ package outbound import ( - "time" - "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" - "github.com/xtls/xray-core/proxy/vmess" ) -func (h *Handler) handleSwitchAccount(cmd *protocol.CommandSwitchAccount) { - rawAccount := &vmess.Account{ - Id: cmd.ID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AUTO, - }, - } - - account, err := rawAccount.AsAccount() - common.Must(err) - user := &protocol.MemoryUser{ - Email: "", - Level: cmd.Level, - Account: account, - } - dest := net.TCPDestination(cmd.Host, cmd.Port) - until := time.Now().Add(time.Duration(cmd.ValidMin) * time.Minute) - h.serverList.AddServer(protocol.NewServerSpec(dest, protocol.BeforeTime(until), user)) -} - +// As a stub command consumer. func (h *Handler) handleCommand(dest net.Destination, cmd protocol.ResponseCommand) { - switch typedCommand := cmd.(type) { - case *protocol.CommandSwitchAccount: - if typedCommand.Host == nil { - typedCommand.Host = dest.Address - } - h.handleSwitchAccount(typedCommand) + switch cmd.(type) { default: } } diff --git a/proxy/vmess/outbound/config.pb.go b/proxy/vmess/outbound/config.pb.go index 0bc4d925..c5a8a185 100644 --- a/proxy/vmess/outbound/config.pb.go +++ b/proxy/vmess/outbound/config.pb.go @@ -26,7 +26,7 @@ type Config struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Receiver []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=Receiver,proto3" json:"Receiver,omitempty"` + Receiver *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=Receiver,proto3" json:"Receiver,omitempty"` } func (x *Config) Reset() { @@ -59,7 +59,7 @@ func (*Config) Descriptor() ([]byte, []int) { return file_proxy_vmess_outbound_config_proto_rawDescGZIP(), []int{0} } -func (x *Config) GetReceiver() []*protocol.ServerEndpoint { +func (x *Config) GetReceiver() *protocol.ServerEndpoint { if x != nil { return x.Receiver } @@ -76,7 +76,7 @@ var file_proxy_vmess_outbound_config_proto_rawDesc = []byte{ 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x4a, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x08, 0x52, - 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, + 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x08, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x42, 0x6d, 0x0a, diff --git a/proxy/vmess/outbound/config.proto b/proxy/vmess/outbound/config.proto index 2a827158..fe3041c6 100644 --- a/proxy/vmess/outbound/config.proto +++ b/proxy/vmess/outbound/config.proto @@ -9,5 +9,5 @@ option java_multiple_files = true; import "common/protocol/server_spec.proto"; message Config { - repeated xray.common.protocol.ServerEndpoint Receiver = 1; + xray.common.protocol.ServerEndpoint Receiver = 1; } diff --git a/proxy/vmess/outbound/outbound.go b/proxy/vmess/outbound/outbound.go index 27079e03..4850e728 100644 --- a/proxy/vmess/outbound/outbound.go +++ b/proxy/vmess/outbound/outbound.go @@ -29,27 +29,24 @@ import ( // Handler is an outbound connection handler for VMess protocol. type Handler struct { - serverList *protocol.ServerList - serverPicker protocol.ServerPicker + server *protocol.ServerSpec policyManager policy.Manager cone bool } // New creates a new VMess outbound handler. func New(ctx context.Context, config *Config) (*Handler, error) { - serverList := protocol.NewServerList() - for _, rec := range config.Receiver { - s, err := protocol.NewServerSpecFromPB(rec) - if err != nil { - return nil, errors.New("failed to parse server spec").Base(err) - } - serverList.AddServer(s) + if config.Receiver == nil { + return nil, errors.New(`no vnext found`) + } + server, err := protocol.NewServerSpecFromPB(config.Receiver) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err) } v := core.MustFromContext(ctx) handler := &Handler{ - serverList: serverList, - serverPicker: protocol.NewRoundRobinServerPicker(serverList), + server: server, policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), cone: ctx.Value("cone").(bool), } @@ -67,11 +64,11 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte ob.Name = "vmess" ob.CanSpliceCopy = 3 - var rec *protocol.ServerSpec + rec := h.server var conn stat.Connection + err := retry.ExponentialBackoff(5, 200).On(func() error { - rec = h.serverPicker.PickServer() - rawConn, err := dialer.Dial(ctx, rec.Destination()) + rawConn, err := dialer.Dial(ctx, rec.Destination) if err != nil { return err } @@ -85,7 +82,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte defer conn.Close() 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()) command := protocol.RequestCommandTCP if target.Network == net.Network_UDP { @@ -95,7 +92,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte command = protocol.RequestCommandMux } - user := rec.PickUser() + user := rec.User request := &protocol.RequestHeader{ Version: encoding.Version, User: user, @@ -202,7 +199,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte if err != nil { return errors.New("failed to read header").Base(err) } - h.handleCommand(rec.Destination(), header.Command) + h.handleCommand(rec.Destination, header.Command) bodyReader, err := session.DecodeResponseBody(request, reader) if err != nil { diff --git a/testing/scenarios/command_test.go b/testing/scenarios/command_test.go index 8ee9e4bf..7afe47d0 100644 --- a/testing/scenarios/command_test.go +++ b/testing/scenarios/command_test.go @@ -423,20 +423,16 @@ func TestCommanderAddRemoveUser(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: u2.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: u2.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), @@ -600,20 +596,16 @@ func TestCommanderStats(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), diff --git a/testing/scenarios/dokodemo_test.go b/testing/scenarios/dokodemo_test.go index f74fb4bd..81234cd1 100644 --- a/testing/scenarios/dokodemo_test.go +++ b/testing/scenarios/dokodemo_test.go @@ -94,17 +94,13 @@ func TestDokodemoTCP(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), @@ -190,17 +186,13 @@ func TestDokodemoUDP(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), diff --git a/testing/scenarios/feature_test.go b/testing/scenarios/feature_test.go index 6fe0d850..e668587e 100644 --- a/testing/scenarios/feature_test.go +++ b/testing/scenarios/feature_test.go @@ -170,17 +170,13 @@ func TestProxy(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: serverUserID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: serverUserID.String(), + }), }, }, }), @@ -193,17 +189,13 @@ func TestProxy(t *testing.T) { { Tag: "proxy", ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(proxyPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: proxyUserID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(proxyPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: proxyUserID.String(), + }), }, }, }), @@ -308,17 +300,13 @@ func TestProxyOverKCP(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: serverUserID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: serverUserID.String(), + }), }, }, }), @@ -334,17 +322,13 @@ func TestProxyOverKCP(t *testing.T) { { Tag: "proxy", ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(proxyPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: proxyUserID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(proxyPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: proxyUserID.String(), + }), }, }, }), @@ -685,20 +669,16 @@ func TestDialXray(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), diff --git a/testing/scenarios/policy_test.go b/testing/scenarios/policy_test.go index 644b313c..c5d68a50 100644 --- a/testing/scenarios/policy_test.go +++ b/testing/scenarios/policy_test.go @@ -119,20 +119,16 @@ func TestVMessClosing(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), @@ -223,20 +219,16 @@ func TestZeroBuffer(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), diff --git a/testing/scenarios/reverse_test.go b/testing/scenarios/reverse_test.go index f7e2e7af..1e41d2a7 100644 --- a/testing/scenarios/reverse_test.go +++ b/testing/scenarios/reverse_test.go @@ -155,20 +155,16 @@ func TestReverseProxy(t *testing.T) { { Tag: "reverse", ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(reversePort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(reversePort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), @@ -348,20 +344,16 @@ func TestReverseProxyLongRunning(t *testing.T) { { Tag: "reverse", ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(reversePort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(reversePort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), diff --git a/testing/scenarios/shadowsocks_test.go b/testing/scenarios/shadowsocks_test.go index f358cbe2..2fdd6204 100644 --- a/testing/scenarios/shadowsocks_test.go +++ b/testing/scenarios/shadowsocks_test.go @@ -75,15 +75,11 @@ func TestShadowsocksChaCha20Poly1305TCP(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ - Server: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: account, - }, - }, + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: account, }, }, }), @@ -171,15 +167,11 @@ func TestShadowsocksAES256GCMTCP(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ - Server: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: account, - }, - }, + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: account, }, }, }), @@ -268,15 +260,11 @@ func TestShadowsocksAES128GCMUDP(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ - Server: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: account, - }, - }, + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: account, }, }, }), @@ -370,15 +358,11 @@ func TestShadowsocksAES128GCMUDPMux(t *testing.T) { }, }), ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ - Server: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: account, - }, - }, + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: account, }, }, }), @@ -455,15 +439,11 @@ func TestShadowsocksNone(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ - Server: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: account, - }, - }, + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: account, }, }, }), diff --git a/testing/scenarios/socks_test.go b/testing/scenarios/socks_test.go index 83ebebc3..a139e5fe 100644 --- a/testing/scenarios/socks_test.go +++ b/testing/scenarios/socks_test.go @@ -73,18 +73,14 @@ func TestSocksBridgeTCP(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&socks.ClientConfig{ - Server: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&socks.Account{ - Username: "Test Account", - Password: "Test Password", - }), - }, - }, + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&socks.Account{ + Username: "Test Account", + Password: "Test Password", + }), }, }, }), @@ -152,18 +148,14 @@ func TestSocksWithHttpRequest(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&http.ClientConfig{ - Server: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&http.Account{ - Username: "Test Account", - Password: "Test Password", - }), - }, - }, + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&http.Account{ + Username: "Test Account", + Password: "Test Password", + }), }, }, }), @@ -256,18 +248,14 @@ func TestSocksBridageUDP(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&socks.ClientConfig{ - Server: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&socks.Account{ - Username: "Test Account", - Password: "Test Password", - }), - }, - }, + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&socks.Account{ + Username: "Test Account", + Password: "Test Password", + }), }, }, }), @@ -375,11 +363,9 @@ func TestSocksBridageUDPWithRouting(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&socks.ClientConfig{ - Server: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - }, + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), }, }), }, diff --git a/testing/scenarios/tls_test.go b/testing/scenarios/tls_test.go index bf0d2c8a..250e5d47 100644 --- a/testing/scenarios/tls_test.go +++ b/testing/scenarios/tls_test.go @@ -89,17 +89,13 @@ func TestSimpleTLSConnection(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), @@ -204,17 +200,13 @@ func TestAutoIssuingCertificate(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), @@ -309,17 +301,13 @@ func TestTLSOverKCP(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), @@ -409,17 +397,13 @@ func TestTLSOverWebSocket(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), @@ -525,17 +509,13 @@ func TestGRPC(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), @@ -641,17 +621,13 @@ func TestGRPCMultiMode(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), @@ -752,17 +728,13 @@ func TestSimpleTLSConnectionPinned(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), @@ -854,17 +826,13 @@ func TestSimpleTLSConnectionPinnedWrongCert(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), @@ -955,17 +923,13 @@ func TestUTLSConnectionPinned(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), @@ -1058,17 +1022,13 @@ func TestUTLSConnectionPinnedWrongCert(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), diff --git a/testing/scenarios/transport_test.go b/testing/scenarios/transport_test.go index bb16deee..80fd13db 100644 --- a/testing/scenarios/transport_test.go +++ b/testing/scenarios/transport_test.go @@ -85,17 +85,13 @@ func TestHTTPConnectionHeader(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), }, }, }), diff --git a/testing/scenarios/vless_test.go b/testing/scenarios/vless_test.go index 52a6712f..b699f497 100644 --- a/testing/scenarios/vless_test.go +++ b/testing/scenarios/vless_test.go @@ -94,17 +94,13 @@ func TestVless(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Vnext: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vless.Account{ - Id: userID.String(), - }), - }, - }, + Vnext: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), }, }, }), @@ -199,17 +195,13 @@ func TestVlessTls(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Vnext: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vless.Account{ - Id: userID.String(), - }), - }, - }, + Vnext: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), }, }, }), @@ -322,18 +314,14 @@ func TestVlessXtlsVision(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Vnext: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vless.Account{ - Id: userID.String(), - Flow: vless.XRV, - }), - }, - }, + Vnext: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + Flow: vless.XRV, + }), }, }, }), @@ -456,18 +444,14 @@ func TestVlessXtlsVisionReality(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Vnext: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vless.Account{ - Id: userID.String(), - Flow: vless.XRV, - }), - }, - }, + Vnext: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + Flow: vless.XRV, + }), }, }, }), diff --git a/testing/scenarios/vmess_test.go b/testing/scenarios/vmess_test.go index 012fc96c..402cf940 100644 --- a/testing/scenarios/vmess_test.go +++ b/testing/scenarios/vmess_test.go @@ -26,147 +26,6 @@ import ( "golang.org/x/sync/errgroup" ) -func TestVMessDynamicPort(t *testing.T) { - tcpServer := tcp.Server{ - MsgProcessor: xor, - } - dest, err := tcpServer.Start() - common.Must(err) - defer tcpServer.Close() - - userID := protocol.NewID(uuid.New()) - - retry := 1 - serverPort := tcp.PickPort() - for { - serverConfig := &core.Config{ - App: []*serial.TypedMessage{ - serial.ToTypedMessage(&log.Config{ - ErrorLogLevel: clog.Severity_Debug, - ErrorLogType: log.LogType_Console, - }), - }, - Inbound: []*core.InboundHandlerConfig{ - { - ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ - PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, - Listen: net.NewIPOrDomain(net.LocalHostIP), - }), - ProxySettings: serial.ToTypedMessage(&inbound.Config{ - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, - Detour: &inbound.DetourConfig{ - To: "detour", - }, - }), - }, - { - ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ - PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort + 100)}}, - Listen: net.NewIPOrDomain(net.LocalHostIP), - }), - ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ - Address: net.NewIPOrDomain(dest.Address), - Port: uint32(dest.Port), - Networks: []net.Network{net.Network_TCP}, - }), - }, - { - ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ - PortList: &net.PortList{ - Range: []*net.PortRange{{From: uint32(serverPort + 1), To: uint32(serverPort + 99)}}, - }, - - Listen: net.NewIPOrDomain(net.LocalHostIP), - AllocationStrategy: &proxyman.AllocationStrategy{ - Type: proxyman.AllocationStrategy_Random, - Concurrency: &proxyman.AllocationStrategy_AllocationStrategyConcurrency{ - Value: 2, - }, - Refresh: &proxyman.AllocationStrategy_AllocationStrategyRefresh{ - Value: 5, - }, - }, - }), - ProxySettings: serial.ToTypedMessage(&inbound.Config{}), - Tag: "detour", - }, - }, - Outbound: []*core.OutboundHandlerConfig{ - { - ProxySettings: serial.ToTypedMessage(&freedom.Config{}), - }, - }, - } - - server, _ := InitializeServerConfig(serverConfig) - if server != nil && WaitConnAvailableWithTest(t, testTCPConn(serverPort+100, 1024, time.Second*2)) { - defer CloseServer(server) - break - } - retry += 1 - if retry > 5 { - t.Fatal("All attempts failed to start server") - } - serverPort = tcp.PickPort() - } - - clientPort := tcp.PickPort() - clientConfig := &core.Config{ - App: []*serial.TypedMessage{ - serial.ToTypedMessage(&log.Config{ - ErrorLogLevel: clog.Severity_Debug, - ErrorLogType: log.LogType_Console, - }), - }, - Inbound: []*core.InboundHandlerConfig{ - { - ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ - PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, - Listen: net.NewIPOrDomain(net.LocalHostIP), - }), - ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ - Address: net.NewIPOrDomain(dest.Address), - Port: uint32(dest.Port), - Networks: []net.Network{net.Network_TCP}, - }), - }, - }, - Outbound: []*core.OutboundHandlerConfig{ - { - ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - }), - }, - }, - }, - }, - }), - }, - }, - } - - server, err := InitializeServerConfig(clientConfig) - common.Must(err) - defer CloseServer(server) - - if !WaitConnAvailableWithTest(t, testTCPConn(clientPort, 1024, time.Second*2)) { - t.Fail() - } -} - func TestVMessGCM(t *testing.T) { tcpServer := tcp.Server{ MsgProcessor: xor, @@ -232,20 +91,16 @@ func TestVMessGCM(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), @@ -334,20 +189,16 @@ func TestVMessGCMReadv(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), @@ -439,20 +290,16 @@ func TestVMessGCMUDP(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), @@ -538,20 +385,16 @@ func TestVMessChacha20(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_CHACHA20_POLY1305, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_CHACHA20_POLY1305, }, - }, + }), }, }, }), @@ -638,20 +481,16 @@ func TestVMessNone(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_NONE, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_NONE, }, - }, + }), }, }, }), @@ -740,20 +579,16 @@ func TestVMessKCP(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), @@ -866,20 +701,16 @@ func TestVMessKCPLarge(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), @@ -999,20 +830,16 @@ func TestVMessGCMMux(t *testing.T) { }, }), ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), @@ -1126,20 +953,16 @@ func TestVMessGCMMuxUDP(t *testing.T) { }, }), ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + }), }, }, }), @@ -1233,20 +1056,16 @@ func TestVMessZero(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_ZERO, - }, - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_ZERO, }, - }, + }), }, }, }), @@ -1332,21 +1151,17 @@ func TestVMessGCMLengthAuth(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - TestsEnabled: "AuthenticatedLength", - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + TestsEnabled: "AuthenticatedLength", + }), }, }, }), @@ -1436,21 +1251,17 @@ func TestVMessGCMLengthAuthPlusNoTerminationSignal(t *testing.T) { Outbound: []*core.OutboundHandlerConfig{ { ProxySettings: serial.ToTypedMessage(&outbound.Config{ - Receiver: []*protocol.ServerEndpoint{ - { - Address: net.NewIPOrDomain(net.LocalHostIP), - Port: uint32(serverPort), - User: []*protocol.User{ - { - Account: serial.ToTypedMessage(&vmess.Account{ - Id: userID.String(), - SecuritySettings: &protocol.SecurityConfig{ - Type: protocol.SecurityType_AES128_GCM, - }, - TestsEnabled: "AuthenticatedLength|NoTerminationSignal", - }), + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, }, - }, + TestsEnabled: "AuthenticatedLength|NoTerminationSignal", + }), }, }, }), From a6ebb3061ce211625866aa0ac6f4049dd2998c37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 23:01:07 -0400 Subject: [PATCH 002/136] Bump google.golang.org/protobuf from 1.36.9 to 1.36.10 (#5203) Bumps google.golang.org/protobuf from 1.36.9 to 1.36.10. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-version: 1.36.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 63b208e9..9e46c393 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( golang.org/x/sys v0.36.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 google.golang.org/grpc v1.75.1 - google.golang.org/protobuf v1.36.9 + google.golang.org/protobuf v1.36.10 gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h12.io/socks v1.0.3 lukechampine.com/blake3 v1.4.1 diff --git a/go.sum b/go.sum index 3308a286..04a2da76 100644 --- a/go.sum +++ b/go.sum @@ -145,8 +145,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From d0344bcff81df2f787c195187d41cdc23e139b96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 23:01:23 -0400 Subject: [PATCH 003/136] Bump github.com/quic-go/quic-go from 0.54.0 to 0.54.1 (#5180) Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.54.0 to 0.54.1. - [Release notes](https://github.com/quic-go/quic-go/releases) - [Commits](https://github.com/quic-go/quic-go/compare/v0.54.0...v0.54.1) --- updated-dependencies: - dependency-name: github.com/quic-go/quic-go dependency-version: 0.54.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9e46c393..da380f06 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/miekg/dns v1.1.68 github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 - github.com/quic-go/quic-go v0.54.0 + github.com/quic-go/quic-go v0.54.1 github.com/refraction-networking/utls v1.8.0 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 diff --git a/go.sum b/go.sum index 04a2da76..2a661ea1 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= -github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg= +github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE= github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= From c0c88f3d731b6d691c8c0447a5a687b2c565f35e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sun, 5 Oct 2025 11:04:18 +0800 Subject: [PATCH 004/136] Fix vless reverse panic in vision (#5189) * Fix vless reverse panic in vision * Add panic --- proxy/vless/outbound/outbound.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 2d4e3ab6..041cc605 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -198,7 +198,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte } case protocol.RequestCommandMux: fallthrough // let server break Mux connections that contain TCP requests - case protocol.RequestCommandTCP: + case protocol.RequestCommandTCP, protocol.RequestCommandRvs: var t reflect.Type var p uintptr if commonConn, ok := conn.(*encryption.CommonConn); ok { @@ -223,6 +223,8 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte r, _ := t.FieldByName("rawInput") input = (*bytes.Reader)(unsafe.Pointer(p + i.Offset)) rawInput = (*bytes.Buffer)(unsafe.Pointer(p + r.Offset)) + default: + panic("unknown VLESS request command") } default: ob.CanSpliceCopy = 3 From 2f366aed2e0023b074676a0d0573ed5ea2c34376 Mon Sep 17 00:00:00 2001 From: Yury Kastov Date: Sun, 5 Oct 2025 06:13:47 +0300 Subject: [PATCH 005/136] feat(config): add unix socket HTTP config loader support (#5200) Adds support for loading configuration from HTTP endpoints served over Unix domain sockets using the http+unix:// protocol scheme. --- main/confloader/external/external.go | 59 ++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/main/confloader/external/external.go b/main/confloader/external/external.go index f1ad437b..787de985 100644 --- a/main/confloader/external/external.go +++ b/main/confloader/external/external.go @@ -2,6 +2,8 @@ package external import ( "bytes" + "context" + "net" "io" "net/http" "net/url" @@ -18,6 +20,9 @@ import ( func ConfigLoader(arg string) (out io.Reader, err error) { var data []byte switch { + case strings.HasPrefix(arg, "http+unix://"): + data, err = FetchUnixSocketHTTPContent(arg) + case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"): data, err = FetchHTTPContent(arg) @@ -70,6 +75,60 @@ func FetchHTTPContent(target string) ([]byte, error) { return content, nil } +// Format: http+unix:///path/to/socket.sock/api/endpoint +func FetchUnixSocketHTTPContent(target string) ([]byte, error) { + path := strings.TrimPrefix(target, "http+unix://") + + if !strings.HasPrefix(path, "/") { + return nil, errors.New("unix socket path must be absolute") + } + + var socketPath, httpPath string + + sockIdx := strings.Index(path, ".sock") + if sockIdx != -1 { + socketPath = path[:sockIdx+5] + httpPath = path[sockIdx+5:] + if httpPath == "" { + httpPath = "/" + } + } else { + return nil, errors.New("cannot determine socket path, socket file should have .sock extension") + } + + if _, err := os.Stat(socketPath); err != nil { + return nil, errors.New("socket file not found: ", socketPath).Base(err) + } + + client := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "unix", socketPath) + }, + }, + } + defer client.CloseIdleConnections() + + resp, err := client.Get("http://localhost" + httpPath) + if err != nil { + return nil, errors.New("failed to fetch from unix socket: ", socketPath).Base(err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, errors.New("unexpected HTTP status code: ", resp.StatusCode) + } + + content, err := buf.ReadAllToBytes(resp.Body) + if err != nil { + return nil, errors.New("failed to read response").Base(err) + } + + return content, nil +} + func ExtConfigLoader(files []string, reader io.Reader) (io.Reader, error) { buf, err := ctlcmd.Run(append([]string{"convert"}, files...), reader) if err != nil { From 514c9e5a22bd366878693a9df7881c3562dfacfc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:14:03 -0400 Subject: [PATCH 006/136] Bump github.com/quic-go/quic-go from 0.54.1 to 0.55.0 (#5208) Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.54.1 to 0.55.0. - [Release notes](https://github.com/quic-go/quic-go/releases) - [Commits](https://github.com/quic-go/quic-go/compare/v0.54.1...v0.55.0) --- updated-dependencies: - dependency-name: github.com/quic-go/quic-go dependency-version: 0.55.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 3 +-- go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index da380f06..9763bbea 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/miekg/dns v1.1.68 github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 - github.com/quic-go/quic-go v0.54.1 + github.com/quic-go/quic-go v0.55.0 github.com/refraction-networking/utls v1.8.0 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 @@ -46,7 +46,6 @@ require ( github.com/quic-go/qpack v0.5.1 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/vishvananda/netns v0.0.5 // indirect - go.uber.org/mock v0.5.0 // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.7.0 // indirect diff --git a/go.sum b/go.sum index 2a661ea1..53ae6aaf 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg= -github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE= github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= @@ -90,8 +90,8 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= From 4a825c0260221cb88645f2003cdb9a9afca25ca5 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Mon, 6 Oct 2025 08:14:45 +0800 Subject: [PATCH 007/136] fix: darwin arm64 always has AESGCMHardwareSupport (#5176) https://github.com/refraction-networking/utls/pull/371 --- common/protocol/headers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/protocol/headers.go b/common/protocol/headers.go index bc90ca6c..9c6573cb 100644 --- a/common/protocol/headers.go +++ b/common/protocol/headers.go @@ -73,7 +73,7 @@ type ResponseHeader struct { var ( // Keep in sync with crypto/tls/cipher_suites.go. hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ && cpu.X86.HasSSE41 && cpu.X86.HasSSSE3 - hasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL + hasGCMAsmARM64 = (cpu.ARM64.HasAES && cpu.ARM64.HasPMULL) || (runtime.GOOS == "darwin" && runtime.GOARCH == "arm64") hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCTR && cpu.S390X.HasGHASH hasGCMAsmPPC64 = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le" From 195248801d71169eb49a28f7d667ae98cebed3df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Mon, 6 Oct 2025 08:15:53 +0800 Subject: [PATCH 008/136] Fix shadowsocks2022 memory leak (#5166) * Fix ss2022 gouroutine leak * ErrReadTimeout --- common/singbridge/pipe.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/common/singbridge/pipe.go b/common/singbridge/pipe.go index d04ebda4..d6c0f3d8 100644 --- a/common/singbridge/pipe.go +++ b/common/singbridge/pipe.go @@ -4,8 +4,10 @@ import ( "context" "io" "net" + "time" "github.com/sagernet/sing/common/bufio" + "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/transport" ) @@ -33,8 +35,26 @@ func (w *PipeConnWrapper) Close() error { return nil } +// This Read implemented a timeout to avoid goroutine leak. +// as a temporarily solution func (w *PipeConnWrapper) Read(b []byte) (n int, err error) { - return w.R.Read(b) + type readResult struct { + n int + err error + } + c := make(chan readResult, 1) + go func() { + n, err := w.R.Read(b) + c <- readResult{n: n, err: err} + }() + select { + case result := <-c: + return result.n, result.err + case <-time.After(300 * time.Second): + common.Close(w.R) + common.Interrupt(w.R) + return 0, buf.ErrReadTimeout + } } func (w *PipeConnWrapper) Write(p []byte) (n int, err error) { From 1762d6c8cc87d05068359dd4afbbe7e71c4ef52a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 00:08:11 +0800 Subject: [PATCH 009/136] Bump github.com/refraction-networking/utls from 1.8.0 to 1.8.1 (#5229) Bumps [github.com/refraction-networking/utls](https://github.com/refraction-networking/utls) from 1.8.0 to 1.8.1. - [Release notes](https://github.com/refraction-networking/utls/releases) - [Commits](https://github.com/refraction-networking/utls/compare/v1.8.0...v1.8.1) --- updated-dependencies: - dependency-name: github.com/refraction-networking/utls dependency-version: 1.8.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9763bbea..07108326 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 github.com/quic-go/quic-go v0.55.0 - github.com/refraction-networking/utls v1.8.0 + github.com/refraction-networking/utls v1.8.1 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 diff --git a/go.sum b/go.sum index 53ae6aaf..c6e30942 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,8 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= -github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE= -github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= +github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= +github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= From 40f0a541bf8de347b00f6ca980279d7b0d5e6af4 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 14 Oct 2025 19:12:14 +0000 Subject: [PATCH 010/136] transport/internet/reality/reality.go: Safely get negotiated CurveID in VerifyPeerCertificate() Requires github.com/refraction-networking/utls v1.8.1+ --- transport/internet/reality/reality.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/transport/internet/reality/reality.go b/transport/internet/reality/reality.go index 20f13ba5..8cb59342 100644 --- a/transport/internet/reality/reality.go +++ b/transport/internet/reality/reality.go @@ -75,8 +75,7 @@ func (c *UConn) HandshakeAddress() net.Address { func (c *UConn) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { if c.Config.Show { localAddr := c.LocalAddr().String() - curveID := *(*utls.CurveID)(unsafe.Pointer(reflect.ValueOf(c).Elem().FieldByName("curveID").UnsafeAddr())) - fmt.Printf("REALITY localAddr: %v\tis using X25519MLKEM768 for TLS' communication: %v\n", localAddr, curveID == utls.X25519MLKEM768) + fmt.Printf("REALITY localAddr: %v\tis using X25519MLKEM768 for TLS' communication: %v\n", localAddr, c.HandshakeState.ServerHello.ServerShare.Group == utls.X25519MLKEM768) fmt.Printf("REALITY localAddr: %v\tis using ML-DSA-65 for cert's extra verification: %v\n", localAddr, len(c.Config.Mldsa65Verify) > 0) } p, _ := reflect.TypeOf(c.Conn).Elem().FieldByName("peerCertificates") From 8dd0e388a22ea1cde03854a513d7d97f3833bca0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 19:18:46 +0000 Subject: [PATCH 011/136] Bump google.golang.org/grpc from 1.75.1 to 1.76.0 (#5212) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.75.1 to 1.76.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.75.1...v1.76.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.76.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 07108326..9acd879a 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( golang.org/x/sync v0.17.0 golang.org/x/sys v0.36.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 - google.golang.org/grpc v1.75.1 + google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.10 gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h12.io/socks v1.0.3 @@ -51,7 +51,7 @@ require ( golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.36.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c6e30942..cf600471 100644 --- a/go.sum +++ b/go.sum @@ -141,10 +141,10 @@ golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uI golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= -google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 898db92d5104698354fba184071274aa0c93edb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 19:57:40 +0000 Subject: [PATCH 012/136] Bump golang.org/x/net from 0.44.0 to 0.46.0 (#5215) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.44.0 to 0.46.0. - [Commits](https://github.com/golang/net/compare/v0.44.0...v0.46.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.46.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 9acd879a..356aae1a 100644 --- a/go.mod +++ b/go.mod @@ -21,10 +21,10 @@ require ( github.com/vishvananda/netlink v1.3.1 github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.42.0 - golang.org/x/net v0.44.0 + golang.org/x/crypto v0.43.0 + golang.org/x/net v0.46.0 golang.org/x/sync v0.17.0 - golang.org/x/sys v0.36.0 + golang.org/x/sys v0.37.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.10 @@ -46,10 +46,10 @@ require ( github.com/quic-go/qpack v0.5.1 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/vishvananda/netns v0.0.5 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/text v0.30.0 // indirect golang.org/x/time v0.7.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/tools v0.37.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index cf600471..350aeaf2 100644 --- a/go.sum +++ b/go.sum @@ -96,16 +96,16 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= 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-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= @@ -117,21 +117,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= 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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From dcfde8dc9200eb3cf305b8fa3bec8e11896b2178 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 14 Oct 2025 20:16:20 +0000 Subject: [PATCH 013/136] Update github.com/xtls/reality to 20251014195629 https://github.com/XTLS/REALITY/commit/e4eec45205357b2de598321460fa958f2b142d76 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 356aae1a..dd368e82 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e github.com/vishvananda/netlink v1.3.1 - github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c + github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.43.0 golang.org/x/net v0.46.0 diff --git a/go.sum b/go.sum index 350aeaf2..fb7a7205 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU= -github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0= +github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 h1:nwobseOLLRtdbP6z7Z2aVI97u8ZptTgD1ofovhAKmeU= +github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= From 7f436f5318b108292d122733e4457b808e152fc1 Mon Sep 17 00:00:00 2001 From: Random Guy <50927468+M03ED@users.noreply.github.com> Date: Wed, 15 Oct 2025 00:04:14 +0330 Subject: [PATCH 014/136] README.md: Add PasarGuard to Web Panels (#5224) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2d53dbc3..105995ea 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ - [wulabing/xray_docker](https://github.com/wulabing/xray_docker) - Web Panel - **WARNING: Please DO NOT USE plain HTTP panels like 3X-UI**, as they are believed to be bribed by Iran GFW for supporting plain HTTP by default and refused to change (https://github.com/XTLS/Xray-core/pull/3884#issuecomment-2439595331), which has already put many users' data security in danger in the past few years. **If you are already using 3X-UI, please switch to the following panels, which are verified to support HTTPS and SSH port forwarding only:** - [X-Panel](https://github.com/xeefei/X-Panel) + - [PasarGuard](https://github.com/PasarGuard/panel) - [Remnawave](https://github.com/remnawave/panel) - [Marzban](https://github.com/Gozargah/Marzban) - [Xray-UI](https://github.com/qist/xray-ui) From 21a965851928da12c06f2d39f3fa32e1e46f766a Mon Sep 17 00:00:00 2001 From: patterniha <71074308+patterniha@users.noreply.github.com> Date: Wed, 15 Oct 2025 00:29:04 +0330 Subject: [PATCH 015/136] Router: Use built-in-dns only once for all rules (in "IPOnDemand"/"IPIfNonMatch" mode) (#5210) --- features/routing/dns/context.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/features/routing/dns/context.go b/features/routing/dns/context.go index a65895f6..2cf9d261 100644 --- a/features/routing/dns/context.go +++ b/features/routing/dns/context.go @@ -12,14 +12,19 @@ import ( // ResolvableContext is an implementation of routing.Context, with domain resolving capability. type ResolvableContext struct { routing.Context - dnsClient dns.Client - resolvedIPs []net.IP + dnsClient dns.Client + cacheIPs []net.IP + hasError bool } // GetTargetIPs overrides original routing.Context's implementation. func (ctx *ResolvableContext) GetTargetIPs() []net.IP { - if len(ctx.resolvedIPs) > 0 { - return ctx.resolvedIPs + if len(ctx.cacheIPs) > 0 { + return ctx.cacheIPs + } + + if ctx.hasError { + return nil } if domain := ctx.GetTargetDomain(); len(domain) != 0 { @@ -29,16 +34,18 @@ func (ctx *ResolvableContext) GetTargetIPs() []net.IP { FakeEnable: false, }) if err == nil { - ctx.resolvedIPs = ips + ctx.cacheIPs = ips return ips } errors.LogInfoInner(context.Background(), err, "resolve ip for ", domain) } if ips := ctx.Context.GetTargetIPs(); len(ips) != 0 { + ctx.cacheIPs = ips return ips } + ctx.hasError = true return nil } From 9cc7907234a8297a87a0ff77fc40db373b74a0f2 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 14 Oct 2025 23:33:06 +0000 Subject: [PATCH 016/136] XHTTP client: Change default `maxConcurrency` to 1 for speed testing https://t.me/projectXray/4386271 --- infra/conf/transport_internet.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 4a12761d..2f3b8009 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -289,8 +289,8 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { return nil, errors.New("maxConnections cannot be specified together with maxConcurrency") } if c.Xmux == (XmuxConfig{}) { - c.Xmux.MaxConcurrency.From = 16 - c.Xmux.MaxConcurrency.To = 32 + c.Xmux.MaxConcurrency.From = 1 + c.Xmux.MaxConcurrency.To = 1 c.Xmux.HMaxRequestTimes.From = 600 c.Xmux.HMaxRequestTimes.To = 900 c.Xmux.HMaxReusableSecs.From = 1800 From 12f4a014e0d29354ac2e1ab77bbb42d786e9d08a Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Wed, 15 Oct 2025 07:05:52 +0000 Subject: [PATCH 017/136] VLESS Reverse Proxy: Transfer real Source & Local (IP & port), enabled by default https://t.me/projectXtls/1039 https://github.com/XTLS/Xray-core/pull/5101#issuecomment-3404979909 --- app/proxyman/outbound/handler.go | 2 +- app/reverse/bridge.go | 16 ++++--- common/mux/client.go | 8 +++- common/mux/frame.go | 72 +++++++++++++++++++++++++++++--- common/mux/mux_test.go | 25 +++++------ common/mux/server.go | 12 +++++- common/mux/writer.go | 6 ++- common/session/context.go | 24 +++++------ proxy/vless/inbound/inbound.go | 2 +- proxy/vless/outbound/outbound.go | 11 +++-- 10 files changed, 130 insertions(+), 48 deletions(-) diff --git a/app/proxyman/outbound/handler.go b/app/proxyman/outbound/handler.go index 361c88ad..3c3c6918 100644 --- a/app/proxyman/outbound/handler.go +++ b/app/proxyman/outbound/handler.go @@ -108,7 +108,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou } h.proxyConfig = proxyConfig - ctx = session.ContextWithHandler(ctx, h) + ctx = session.ContextWithFullHandler(ctx, h) rawProxyHandler, err := common.CreateObject(ctx, proxyConfig) if err != nil { diff --git a/app/reverse/bridge.go b/app/reverse/bridge.go index 74e20497..fc83a740 100644 --- a/app/reverse/bridge.go +++ b/app/reverse/bridge.go @@ -198,9 +198,11 @@ func (w *BridgeWorker) handleInternalConn(link *transport.Link) { func (w *BridgeWorker) Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) { if !isInternalDomain(dest) { - ctx = session.ContextWithInbound(ctx, &session.Inbound{ - Tag: w.Tag, - }) + if session.InboundFromContext(ctx) == nil { + ctx = session.ContextWithInbound(ctx, &session.Inbound{ + Tag: w.Tag, + }) + } return w.Dispatcher.Dispatch(ctx, dest) } @@ -221,9 +223,11 @@ func (w *BridgeWorker) Dispatch(ctx context.Context, dest net.Destination) (*tra func (w *BridgeWorker) DispatchLink(ctx context.Context, dest net.Destination, link *transport.Link) error { if !isInternalDomain(dest) { - ctx = session.ContextWithInbound(ctx, &session.Inbound{ - Tag: w.Tag, - }) + if session.InboundFromContext(ctx) == nil { + ctx = session.ContextWithInbound(ctx, &session.Inbound{ + Tag: w.Tag, + }) + } return w.Dispatcher.DispatchLink(ctx, dest, link) } diff --git a/common/mux/client.go b/common/mux/client.go index 93357574..28380331 100644 --- a/common/mux/client.go +++ b/common/mux/client.go @@ -264,7 +264,11 @@ func fetchInput(ctx context.Context, s *Session, output buf.Writer) { transferType = protocol.TransferTypePacket } s.transferType = transferType - writer := NewWriter(s.ID, ob.Target, output, transferType, xudp.GetGlobalID(ctx)) + var inbound *session.Inbound + if session.IsReverseMuxFromContext(ctx) { + inbound = session.InboundFromContext(ctx) + } + writer := NewWriter(s.ID, ob.Target, output, transferType, xudp.GetGlobalID(ctx), inbound) defer s.Close(false) defer writer.Close() @@ -384,7 +388,7 @@ func (m *ClientWorker) fetchOutput() { var meta FrameMetadata for { - err := meta.Unmarshal(reader) + err := meta.Unmarshal(reader, false) if err != nil { if errors.Cause(err) != io.EOF { errors.LogInfoInner(context.Background(), err, "failed to read metadata") diff --git a/common/mux/frame.go b/common/mux/frame.go index bdf5cc8c..f248fbdf 100644 --- a/common/mux/frame.go +++ b/common/mux/frame.go @@ -11,6 +11,7 @@ import ( "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/session" ) type SessionStatus byte @@ -60,6 +61,7 @@ type FrameMetadata struct { Option bitmask.Byte SessionStatus SessionStatus GlobalID [8]byte + Inbound *session.Inbound } func (f FrameMetadata) WriteTo(b *buf.Buffer) error { @@ -79,11 +81,23 @@ func (f FrameMetadata) WriteTo(b *buf.Buffer) error { case net.Network_UDP: common.Must(b.WriteByte(byte(TargetNetworkUDP))) } - if err := addrParser.WriteAddressPort(b, f.Target.Address, f.Target.Port); err != nil { return err } - if b.UDP != nil { // make sure it's user's proxy request + if f.Inbound != nil { + if f.Inbound.Source.Network == net.Network_TCP || f.Inbound.Source.Network == net.Network_UDP { + common.Must(b.WriteByte(byte(f.Inbound.Source.Network - 1))) + if err := addrParser.WriteAddressPort(b, f.Inbound.Source.Address, f.Inbound.Source.Port); err != nil { + return err + } + if f.Inbound.Local.Network == net.Network_TCP || f.Inbound.Local.Network == net.Network_UDP { + common.Must(b.WriteByte(byte(f.Inbound.Local.Network - 1))) + if err := addrParser.WriteAddressPort(b, f.Inbound.Local.Address, f.Inbound.Local.Port); err != nil { + return err + } + } + } + } else if b.UDP != nil { // make sure it's user's proxy request b.Write(f.GlobalID[:]) // no need to check whether it's empty } } else if b.UDP != nil { @@ -97,7 +111,7 @@ func (f FrameMetadata) WriteTo(b *buf.Buffer) error { } // Unmarshal reads FrameMetadata from the given reader. -func (f *FrameMetadata) Unmarshal(reader io.Reader) error { +func (f *FrameMetadata) Unmarshal(reader io.Reader, readSourceAndLocal bool) error { metaLen, err := serial.ReadUint16(reader) if err != nil { return err @@ -112,12 +126,12 @@ func (f *FrameMetadata) Unmarshal(reader io.Reader) error { if _, err := b.ReadFullFrom(reader, int32(metaLen)); err != nil { return err } - return f.UnmarshalFromBuffer(b) + return f.UnmarshalFromBuffer(b, readSourceAndLocal) } // UnmarshalFromBuffer reads a FrameMetadata from the given buffer. // Visible for testing only. -func (f *FrameMetadata) UnmarshalFromBuffer(b *buf.Buffer) error { +func (f *FrameMetadata) UnmarshalFromBuffer(b *buf.Buffer, readSourceAndLocal bool) error { if b.Len() < 4 { return errors.New("insufficient buffer: ", b.Len()) } @@ -150,6 +164,54 @@ func (f *FrameMetadata) UnmarshalFromBuffer(b *buf.Buffer) error { } } + if f.SessionStatus == SessionStatusNew && readSourceAndLocal { + f.Inbound = &session.Inbound{} + + if b.Len() == 0 { + return nil // for heartbeat, etc. + } + network := TargetNetwork(b.Byte(0)) + if network == 0 { + return nil // may be padding + } + b.Advance(1) + addr, port, err := addrParser.ReadAddressPort(nil, b) + if err != nil { + return errors.New("reading source: failed to parse address and port").Base(err) + } + switch network { + case TargetNetworkTCP: + f.Inbound.Source = net.TCPDestination(addr, port) + case TargetNetworkUDP: + f.Inbound.Source = net.UDPDestination(addr, port) + default: + return errors.New("reading source: unknown network type: ", network) + } + + if b.Len() == 0 { + return nil + } + network = TargetNetwork(b.Byte(0)) + if network == 0 { + return nil + } + b.Advance(1) + addr, port, err = addrParser.ReadAddressPort(nil, b) + if err != nil { + return errors.New("reading local: failed to parse address and port").Base(err) + } + switch network { + case TargetNetworkTCP: + f.Inbound.Local = net.TCPDestination(addr, port) + case TargetNetworkUDP: + f.Inbound.Local = net.UDPDestination(addr, port) + default: + return errors.New("reading local: unknown network type: ", network) + } + + return nil + } + // Application data is essential, to test whether the pipe is closed. if f.SessionStatus == SessionStatusNew && f.Option.Has(OptionData) && f.Target.Network == net.Network_UDP && b.Len() >= 8 { diff --git a/common/mux/mux_test.go b/common/mux/mux_test.go index f326ffd7..db01d372 100644 --- a/common/mux/mux_test.go +++ b/common/mux/mux_test.go @@ -10,6 +10,7 @@ import ( . "github.com/xtls/xray-core/common/mux" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/transport/pipe" ) @@ -32,13 +33,13 @@ func TestReaderWriter(t *testing.T) { pReader, pWriter := pipe.New(pipe.WithSizeLimit(1024)) dest := net.TCPDestination(net.DomainAddress("example.com"), 80) - writer := NewWriter(1, dest, pWriter, protocol.TransferTypeStream, [8]byte{}) + writer := NewWriter(1, dest, pWriter, protocol.TransferTypeStream, [8]byte{}, &session.Inbound{}) dest2 := net.TCPDestination(net.LocalHostIP, 443) - writer2 := NewWriter(2, dest2, pWriter, protocol.TransferTypeStream, [8]byte{}) + writer2 := NewWriter(2, dest2, pWriter, protocol.TransferTypeStream, [8]byte{}, &session.Inbound{}) dest3 := net.TCPDestination(net.LocalHostIPv6, 18374) - writer3 := NewWriter(3, dest3, pWriter, protocol.TransferTypeStream, [8]byte{}) + writer3 := NewWriter(3, dest3, pWriter, protocol.TransferTypeStream, [8]byte{}, &session.Inbound{}) writePayload := func(writer *Writer, payload ...byte) error { b := buf.New() @@ -62,7 +63,7 @@ func TestReaderWriter(t *testing.T) { { var meta FrameMetadata - common.Must(meta.Unmarshal(bytesReader)) + common.Must(meta.Unmarshal(bytesReader, false)) if r := cmp.Diff(meta, FrameMetadata{ SessionID: 1, SessionStatus: SessionStatusNew, @@ -81,7 +82,7 @@ func TestReaderWriter(t *testing.T) { { var meta FrameMetadata - common.Must(meta.Unmarshal(bytesReader)) + common.Must(meta.Unmarshal(bytesReader, false)) if r := cmp.Diff(meta, FrameMetadata{ SessionStatus: SessionStatusNew, SessionID: 2, @@ -94,7 +95,7 @@ func TestReaderWriter(t *testing.T) { { var meta FrameMetadata - common.Must(meta.Unmarshal(bytesReader)) + common.Must(meta.Unmarshal(bytesReader, false)) if r := cmp.Diff(meta, FrameMetadata{ SessionID: 1, SessionStatus: SessionStatusKeep, @@ -112,7 +113,7 @@ func TestReaderWriter(t *testing.T) { { var meta FrameMetadata - common.Must(meta.Unmarshal(bytesReader)) + common.Must(meta.Unmarshal(bytesReader, false)) if r := cmp.Diff(meta, FrameMetadata{ SessionID: 3, SessionStatus: SessionStatusNew, @@ -131,7 +132,7 @@ func TestReaderWriter(t *testing.T) { { var meta FrameMetadata - common.Must(meta.Unmarshal(bytesReader)) + common.Must(meta.Unmarshal(bytesReader, false)) if r := cmp.Diff(meta, FrameMetadata{ SessionID: 1, SessionStatus: SessionStatusEnd, @@ -143,7 +144,7 @@ func TestReaderWriter(t *testing.T) { { var meta FrameMetadata - common.Must(meta.Unmarshal(bytesReader)) + common.Must(meta.Unmarshal(bytesReader, false)) if r := cmp.Diff(meta, FrameMetadata{ SessionID: 3, SessionStatus: SessionStatusEnd, @@ -155,7 +156,7 @@ func TestReaderWriter(t *testing.T) { { var meta FrameMetadata - common.Must(meta.Unmarshal(bytesReader)) + common.Must(meta.Unmarshal(bytesReader, false)) if r := cmp.Diff(meta, FrameMetadata{ SessionID: 2, SessionStatus: SessionStatusKeep, @@ -173,7 +174,7 @@ func TestReaderWriter(t *testing.T) { { var meta FrameMetadata - common.Must(meta.Unmarshal(bytesReader)) + common.Must(meta.Unmarshal(bytesReader, false)) if r := cmp.Diff(meta, FrameMetadata{ SessionID: 2, SessionStatus: SessionStatusEnd, @@ -187,7 +188,7 @@ func TestReaderWriter(t *testing.T) { { var meta FrameMetadata - err := meta.Unmarshal(bytesReader) + err := meta.Unmarshal(bytesReader, false) if err == nil { t.Error("nil error") } diff --git a/common/mux/server.go b/common/mux/server.go index 70c5ed24..f01c325d 100644 --- a/common/mux/server.go +++ b/common/mux/server.go @@ -166,6 +166,14 @@ func (w *ServerWorker) handleStatusKeepAlive(meta *FrameMetadata, reader *buf.Bu func (w *ServerWorker) handleStatusNew(ctx context.Context, meta *FrameMetadata, reader *buf.BufferedReader) error { ctx = session.SubContextFromMuxInbound(ctx) + if meta.Inbound != nil && meta.Inbound.Source.IsValid() && meta.Inbound.Local.IsValid() { + if inbound := session.InboundFromContext(ctx); inbound != nil { + newInbound := *inbound + newInbound.Source = meta.Inbound.Source + newInbound.Local = meta.Inbound.Local + ctx = session.ContextWithInbound(ctx, &newInbound) + } + } errors.LogInfo(ctx, "received request for ", meta.Target) { msg := &log.AccessMessage{ @@ -329,7 +337,7 @@ func (w *ServerWorker) handleStatusEnd(meta *FrameMetadata, reader *buf.Buffered func (w *ServerWorker) handleFrame(ctx context.Context, reader *buf.BufferedReader) error { var meta FrameMetadata - err := meta.Unmarshal(reader) + err := meta.Unmarshal(reader, session.IsReverseMuxFromContext(ctx)) if err != nil { return errors.New("failed to read metadata").Base(err) } @@ -340,7 +348,7 @@ func (w *ServerWorker) handleFrame(ctx context.Context, reader *buf.BufferedRead case SessionStatusEnd: err = w.handleStatusEnd(&meta, reader) case SessionStatusNew: - err = w.handleStatusNew(ctx, &meta, reader) + err = w.handleStatusNew(session.ContextWithIsReverseMux(ctx, false), &meta, reader) case SessionStatusKeep: err = w.handleStatusKeep(&meta, reader) default: diff --git a/common/mux/writer.go b/common/mux/writer.go index a6dc551d..0429f4fa 100644 --- a/common/mux/writer.go +++ b/common/mux/writer.go @@ -6,6 +6,7 @@ import ( "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/session" ) type Writer struct { @@ -16,9 +17,10 @@ type Writer struct { hasError bool transferType protocol.TransferType globalID [8]byte + inbound *session.Inbound } -func NewWriter(id uint16, dest net.Destination, writer buf.Writer, transferType protocol.TransferType, globalID [8]byte) *Writer { +func NewWriter(id uint16, dest net.Destination, writer buf.Writer, transferType protocol.TransferType, globalID [8]byte, inbound *session.Inbound) *Writer { return &Writer{ id: id, dest: dest, @@ -26,6 +28,7 @@ func NewWriter(id uint16, dest net.Destination, writer buf.Writer, transferType followup: false, transferType: transferType, globalID: globalID, + inbound: inbound, } } @@ -43,6 +46,7 @@ func (w *Writer) getNextFrameMeta() FrameMetadata { SessionID: w.id, Target: w.dest, GlobalID: w.globalID, + Inbound: w.inbound, } if w.followup { diff --git a/common/session/context.go b/common/session/context.go index 6a812f99..c28f2081 100644 --- a/common/session/context.go +++ b/common/session/context.go @@ -17,13 +17,13 @@ const ( inboundSessionKey ctx.SessionKey = 1 outboundSessionKey ctx.SessionKey = 2 contentSessionKey ctx.SessionKey = 3 - muxPreferredSessionKey ctx.SessionKey = 4 // unused + isReverseMuxKey ctx.SessionKey = 4 // is reverse mux sockoptSessionKey ctx.SessionKey = 5 // used by dokodemo to only receive sockopt.Mark trackedConnectionErrorKey ctx.SessionKey = 6 // used by observer to get outbound error dispatcherKey ctx.SessionKey = 7 // used by ss2022 inbounds to get dispatcher timeoutOnlyKey ctx.SessionKey = 8 // mux context's child contexts to only cancel when its own traffic times out allowedNetworkKey ctx.SessionKey = 9 // muxcool server control incoming request tcp/udp - handlerSessionKey ctx.SessionKey = 10 // outbound gets full handler + fullHandlerKey ctx.SessionKey = 10 // outbound gets full handler mitmAlpn11Key ctx.SessionKey = 11 // used by TLS dialer mitmServerNameKey ctx.SessionKey = 12 // used by TLS dialer ) @@ -75,25 +75,21 @@ func ContentFromContext(ctx context.Context) *Content { return nil } -// ContextWithMuxPreferred returns a new context with the given bool -func ContextWithMuxPreferred(ctx context.Context, forced bool) context.Context { - return context.WithValue(ctx, muxPreferredSessionKey, forced) +func ContextWithIsReverseMux(ctx context.Context, isReverseMux bool) context.Context { + return context.WithValue(ctx, isReverseMuxKey, isReverseMux) } -// MuxPreferredFromContext returns value in this context, or false if not contained. -func MuxPreferredFromContext(ctx context.Context) bool { - if val, ok := ctx.Value(muxPreferredSessionKey).(bool); ok { +func IsReverseMuxFromContext(ctx context.Context) bool { + if val, ok := ctx.Value(isReverseMuxKey).(bool); ok { return val } return false } -// ContextWithSockopt returns a new context with Socket configs included func ContextWithSockopt(ctx context.Context, s *Sockopt) context.Context { return context.WithValue(ctx, sockoptSessionKey, s) } -// SockoptFromContext returns Socket configs in this context, or nil if not contained. func SockoptFromContext(ctx context.Context) *Sockopt { if sockopt, ok := ctx.Value(sockoptSessionKey).(*Sockopt); ok { return sockopt @@ -164,12 +160,12 @@ func AllowedNetworkFromContext(ctx context.Context) net.Network { return net.Network_Unknown } -func ContextWithHandler(ctx context.Context, handler outbound.Handler) context.Context { - return context.WithValue(ctx, handlerSessionKey, handler) +func ContextWithFullHandler(ctx context.Context, handler outbound.Handler) context.Context { + return context.WithValue(ctx, fullHandlerKey, handler) } -func HandlerFromContext(ctx context.Context) outbound.Handler { - if val, ok := ctx.Value(handlerSessionKey).(outbound.Handler); ok { +func FullHandlerFromContext(ctx context.Context) outbound.Handler { + if val, ok := ctx.Value(fullHandlerKey).(outbound.Handler); ok { return val } return nil diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 2f15b5d8..223aade0 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -666,7 +666,7 @@ func (r *Reverse) Dispatch(ctx context.Context, link *transport.Link) { link.Reader = &buf.EndpointOverrideReader{Reader: link.Reader, Dest: ob.Target.Address, OriginalDest: ob.OriginalTarget.Address} link.Writer = &buf.EndpointOverrideWriter{Writer: link.Writer, Dest: ob.Target.Address, OriginalDest: ob.OriginalTarget.Address} } - r.client.Dispatch(ctx, link) + r.client.Dispatch(session.ContextWithIsReverseMux(ctx, true), link) } } diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 041cc605..c425151f 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -89,8 +89,11 @@ func New(ctx context.Context, config *Config) (*Handler, error) { handler.reverse = &Reverse{ tag: a.Reverse.Tag, dispatcher: v.GetFeature(routing.DispatcherType()).(routing.Dispatcher), - ctx: ctx, - handler: handler, + ctx: session.ContextWithInbound(ctx, &session.Inbound{ + Tag: a.Reverse.Tag, + User: handler.server.User, // TODO: email + }), + handler: handler, } handler.reverse.monitorTask = &task.Periodic{ Execute: handler.reverse.monitor, @@ -397,7 +400,7 @@ func (r *Reverse) monitor() error { Tag: r.tag, Dispatcher: r.dispatcher, } - worker, err := mux.NewServerWorker(r.ctx, w, link1) + worker, err := mux.NewServerWorker(session.ContextWithIsReverseMux(r.ctx, true), w, link1) if err != nil { errors.LogWarningInner(r.ctx, err, "failed to create mux server worker") return nil @@ -408,7 +411,7 @@ func (r *Reverse) monitor() error { ctx := session.ContextWithOutbounds(r.ctx, []*session.Outbound{{ Target: net.Destination{Address: net.DomainAddress("v1.rvs.cool")}, }}) - r.handler.Process(ctx, link2, session.HandlerFromContext(ctx).(*proxyman.Handler)) + r.handler.Process(ctx, link2, session.FullHandlerFromContext(ctx).(*proxyman.Handler)) common.Interrupt(reader1) common.Interrupt(reader2) }() From b69a376aa1b61ad94c9c05b2c2cd263567b8cc23 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Wed, 15 Oct 2025 08:07:23 +0000 Subject: [PATCH 018/136] v25.10.15 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 --- core/core.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/core.go b/core/core.go index 1f4fc963..42197697 100644 --- a/core/core.go +++ b/core/core.go @@ -18,8 +18,8 @@ import ( var ( Version_x byte = 25 - Version_y byte = 9 - Version_z byte = 11 + Version_y byte = 10 + Version_z byte = 15 ) var ( From cb4f943f5065c1ff93367be4027ef27254b52f1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 11:07:19 +0000 Subject: [PATCH 019/136] Bump actions/upload-artifact from 4 to 5 (#5259) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release-win7.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-win7.yml b/.github/workflows/release-win7.yml index 00778029..294a5e1b 100644 --- a/.github/workflows/release-win7.yml +++ b/.github/workflows/release-win7.yml @@ -132,7 +132,7 @@ jobs: mv build_assets Xray-${{ env.ASSET_NAME }} - name: Upload files to Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Xray-${{ env.ASSET_NAME }} path: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eaf919b6..b717f3e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -238,7 +238,7 @@ jobs: mv build_assets Xray-${{ env.ASSET_NAME }} - name: Upload files to Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Xray-${{ env.ASSET_NAME }} path: | From 9491b67f3ca2559ed79fcd370fa74de81e02e4d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 20:51:29 -0500 Subject: [PATCH 020/136] Bump github.com/quic-go/quic-go from 0.55.0 to 0.56.0 (#5292) Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.55.0 to 0.56.0. - [Release notes](https://github.com/quic-go/quic-go/releases) - [Commits](https://github.com/quic-go/quic-go/compare/v0.55.0...v0.56.0) --- updated-dependencies: - dependency-name: github.com/quic-go/quic-go dependency-version: 0.56.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 13 ++++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index dd368e82..e328a3b8 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/miekg/dns v1.1.68 github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 - github.com/quic-go/quic-go v0.55.0 + github.com/quic-go/quic-go v0.56.0 github.com/refraction-networking/utls v1.8.1 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 @@ -41,14 +41,14 @@ require ( github.com/juju/ratelimit v1.0.2 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/cpuid/v2 v2.0.12 // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/vishvananda/netns v0.0.5 // indirect golang.org/x/mod v0.28.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.7.0 // indirect + golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.37.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect diff --git a/go.sum b/go.sum index fb7a7205..1071edf5 100644 --- a/go.sum +++ b/go.sum @@ -46,19 +46,18 @@ github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoU github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= -github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= +github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY= +github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c= github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= @@ -125,8 +124,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= From 412bc17c126d8e3e247fea40916d60434df63027 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 02:43:07 +0000 Subject: [PATCH 021/136] Bump google.golang.org/grpc from 1.76.0 to 1.77.0 (#5308) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.76.0 to 1.77.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.76.0...v1.77.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.77.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 36 ++++++++++++++++++------------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index e328a3b8..4e19f4de 100644 --- a/go.mod +++ b/go.mod @@ -22,11 +22,11 @@ require ( github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.43.0 - golang.org/x/net v0.46.0 + golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 golang.org/x/sync v0.17.0 golang.org/x/sys v0.37.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 - google.golang.org/grpc v1.76.0 + google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h12.io/socks v1.0.3 @@ -51,7 +51,7 @@ require ( golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.37.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1071edf5..2451ff89 100644 --- a/go.sum +++ b/go.sum @@ -77,18 +77,18 @@ github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZla github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 h1:nwobseOLLRtdbP6z7Z2aVI97u8ZptTgD1ofovhAKmeU= github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= @@ -103,8 +103,8 @@ golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= 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-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 h1:6/3JGEh1C88g7m+qzzTbl3A0FtsLguXieqofVLU/JAo= +golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= @@ -140,10 +140,10 @@ golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uI golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 1a32d18c169418149f81e48a75a9c61127d6688a Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Wed, 19 Nov 2025 03:25:32 +0000 Subject: [PATCH 022/136] REALITY config: Return error when short id is too long (#5276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/XTLS/Xray-core/issues/5273 --------- Co-authored-by: 风扇滑翔翼 --- infra/conf/transport_internet.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 2f3b8009..89dfa645 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -628,6 +628,9 @@ func (c *REALITYConfig) Build() (proto.Message, error) { } config.ShortIds = make([][]byte, len(c.ShortIds)) for i, s := range c.ShortIds { + if len(s) > 16 { + return nil, errors.New(`too long "shortIds[`, i, `]": `, s) + } config.ShortIds[i] = make([]byte, 8) if _, err = hex.Decode(config.ShortIds[i], []byte(s)); err != nil { return nil, errors.New(`invalid "shortIds[`, i, `]": `, s) @@ -679,6 +682,9 @@ func (c *REALITYConfig) Build() (proto.Message, error) { if len(c.ShortIds) != 0 { return nil, errors.New(`non-empty "shortIds", please use "shortId" instead`) } + if len(c.ShortIds) > 16 { + return nil, errors.New(`too long "shortId": `, c.ShortId) + } config.ShortId = make([]byte, 8) if _, err = hex.Decode(config.ShortId, []byte(c.ShortId)); err != nil { return nil, errors.New(`invalid "shortId": `, c.ShortId) From 18a41047371dbd1a0814b9f49dcfad58d27a0a00 Mon Sep 17 00:00:00 2001 From: Exclude0122 <173652916+Exclude0122@users.noreply.github.com> Date: Fri, 14 Nov 2025 20:56:23 -0500 Subject: [PATCH 023/136] Fix wireguard not discarding broken connection on android (#5304) Fixes https://github.com/XTLS/Xray-core/issues/5303 --- proxy/wireguard/bind.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/proxy/wireguard/bind.go b/proxy/wireguard/bind.go index 79de7eba..07ec85ef 100644 --- a/proxy/wireguard/bind.go +++ b/proxy/wireguard/bind.go @@ -3,7 +3,6 @@ package wireguard import ( "context" "errors" - "io" "net" "net/netip" "strconv" @@ -153,7 +152,7 @@ func (bind *netBindClient) connectTo(endpoint *netEndpoint) error { v.endpoint = endpoint v.err = err v.waiter.Done() - if err != nil && errors.Is(err, io.EOF) { + if err != nil { endpoint.conn = nil return } From 4e8ee302a6705247ff73da21ade38dc1b83730b5 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Wed, 19 Nov 2025 06:16:30 +0000 Subject: [PATCH 024/136] README.md: Add Remnawave & Happ to Sponsors Sponsor Xray-core: https://github.com/XTLS/Xray-core/issues/3668 --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 105995ea..5ecd71fa 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ [README](https://github.com/XTLS/Xray-core#readme) is open, so feel free to submit your project [here](https://github.com/XTLS/Xray-core/pulls). +## Sponsors + +[![Remnawave](https://github.com/user-attachments/assets/a22d34ae-01ee-441c-843a-85356748ed1e)](https://docs.rw) + +[![Happ](https://github.com/user-attachments/assets/14055dab-e8bb-48bd-89e8-962709e4098e)](https://happ.su) + +[**Sponsor Xray-core**](https://github.com/XTLS/Xray-core/issues/3668) + ## Donation & NFTs ### [Collect a Project X NFT to support the development of Project X!](https://opensea.io/item/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1) @@ -44,9 +52,9 @@ - [teddysun/xray](https://hub.docker.com/r/teddysun/xray) - [wulabing/xray_docker](https://github.com/wulabing/xray_docker) - Web Panel - **WARNING: Please DO NOT USE plain HTTP panels like 3X-UI**, as they are believed to be bribed by Iran GFW for supporting plain HTTP by default and refused to change (https://github.com/XTLS/Xray-core/pull/3884#issuecomment-2439595331), which has already put many users' data security in danger in the past few years. **If you are already using 3X-UI, please switch to the following panels, which are verified to support HTTPS and SSH port forwarding only:** + - [Remnawave](https://github.com/remnawave/panel) - [X-Panel](https://github.com/xeefei/X-Panel) - [PasarGuard](https://github.com/PasarGuard/panel) - - [Remnawave](https://github.com/remnawave/panel) - [Marzban](https://github.com/Gozargah/Marzban) - [Xray-UI](https://github.com/qist/xray-ui) - [Hiddify](https://github.com/hiddify/Hiddify-Manager) From 8a4b0a9eb084fff56128cc22fe50d1915ce40487 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:39:32 +0000 Subject: [PATCH 025/136] README.md: Add TRX & TON & BTC & XMR & SOL to Donation & NFTs https://t.me/projectXtls/1145 --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 5ecd71fa..6c7c05fa 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,11 @@ [Project X NFT](https://opensea.io/item/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1) +- **TRX(Tron)/USDT/USDC: `TNrDh5VSfwd4RPrwsohr6poyNTfFefNYan`** +- **TON: `UQApeV-u2gm43aC1uP76xAC1m6vCylstaN1gpfBmre_5IyTH`** +- **BTC: `1JpqcziZZuqv3QQJhZGNGBVdCBrGgkL6cT`** +- **XMR: `4ABHQZ3yJZkBnLoqiKvb3f8eqUnX4iMPb6wdant5ZLGQELctcerceSGEfJnoCk6nnyRZm73wrwSgvZ2WmjYLng6R7sR67nq`** +- **SOL/USDT/USDC: `3x5NuXHzB5APG6vRinPZcsUv5ukWUY1tBGRSJiEJWtZa`** - **ETH/USDT/USDC: `0xDc3Fe44F0f25D13CACb1C4896CD0D321df3146Ee`** - **Project X NFT: https://opensea.io/item/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1** - **VLESS NFT: https://opensea.io/collection/vless** From 1ec2966433a0b138f05ff3a5a136b7b550636921 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:11:02 +0000 Subject: [PATCH 026/136] Bump golang.org/x/net (#5318) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.46.1-0.20251013234738-63d1a5100f82 to 0.47.0. - [Commits](https://github.com/golang/net/commits/v0.47.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 4e19f4de..e51632ae 100644 --- a/go.mod +++ b/go.mod @@ -21,10 +21,10 @@ require ( github.com/vishvananda/netlink v1.3.1 github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.43.0 - golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 - golang.org/x/sync v0.17.0 - golang.org/x/sys v0.37.0 + golang.org/x/crypto v0.44.0 + golang.org/x/net v0.47.0 + golang.org/x/sync v0.18.0 + golang.org/x/sys v0.38.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 @@ -46,10 +46,10 @@ require ( github.com/quic-go/qpack v0.5.1 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/vishvananda/netns v0.0.5 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/tools v0.38.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 2451ff89..77ee8b99 100644 --- a/go.sum +++ b/go.sum @@ -95,20 +95,20 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 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-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 h1:6/3JGEh1C88g7m+qzzTbl3A0FtsLguXieqofVLU/JAo= -golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -116,21 +116,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 2f1fabb318ddcaffb46f991fcab667e198ac0086 Mon Sep 17 00:00:00 2001 From: Aron Yang Date: Thu, 20 Nov 2025 20:08:39 +0800 Subject: [PATCH 027/136] README.md: Add v2rayN to macOS & Linux Clients (#5271) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6c7c05fa..7e13caaf 100644 --- a/README.md +++ b/README.md @@ -121,12 +121,14 @@ - [OneXray](https://github.com/OneXray/OneXray) - [GoXRay](https://github.com/goxray/desktop) - [AnyPortal](https://github.com/AnyPortal/AnyPortal) + - [v2rayN](https://github.com/2dust/v2rayN) - Linux - [v2rayA](https://github.com/v2rayA/v2rayA) - [Furious](https://github.com/LorenEteval/Furious) - [GorzRay](https://github.com/ketetefid/GorzRay) - [GoXRay](https://github.com/goxray/desktop) - [AnyPortal](https://github.com/AnyPortal/AnyPortal) + - [v2rayN](https://github.com/2dust/v2rayN) ## Others that support VLESS, XTLS, REALITY, XUDP, PLUX... From b16a5f03fe1a42c5c35bdf04732cd5671e765adf Mon Sep 17 00:00:00 2001 From: vemneyy Date: Thu, 20 Nov 2025 15:21:22 +0300 Subject: [PATCH 028/136] Socks: Fix buffer full panic when encoding large UDP packets (#5252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 风扇滑翔翼 --- proxy/socks/protocol.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index 9bccf607..76ceb47c 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -353,6 +353,11 @@ func EncodeUDPPacket(request *protocol.RequestHeader, data []byte) (*buf.Buffer, b.Release() return nil, err } + // if data is too large, return an empty buffer (drop too big data) + if b.Available() < int32(len(data)) { + b.Clear() + return b, nil + } common.Must2(b.Write(data)) return b, nil } From b24ef88a80eb7e6ff15666c5d2e56e78de8a1e71 Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Thu, 20 Nov 2025 21:20:53 +0800 Subject: [PATCH 029/136] Docker: Use more aggressive inlining for higher efficiency (#5242) Syncs https://github.com/XTLS/Xray-core/pull/5026 --- .github/docker/Dockerfile | 2 +- .github/docker/Dockerfile.usa | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/docker/Dockerfile b/.github/docker/Dockerfile index 0b6f2932..867e671a 100644 --- a/.github/docker/Dockerfile +++ b/.github/docker/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /src COPY . . ARG TARGETOS ARG TARGETARCH -RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -ldflags "-s -w -buildid=" ./main +RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main # Download geodat into a staging directory ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat diff --git a/.github/docker/Dockerfile.usa b/.github/docker/Dockerfile.usa index b307bc4b..80cc523a 100644 --- a/.github/docker/Dockerfile.usa +++ b/.github/docker/Dockerfile.usa @@ -6,7 +6,7 @@ WORKDIR /src COPY . . ARG TARGETOS ARG TARGETARCH -RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -ldflags "-s -w -buildid=" ./main +RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main # Download geodat into a staging directory ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat From f9dd3aef72f013fca94e6b0a604c636df62285d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Thu, 20 Nov 2025 23:53:42 +0800 Subject: [PATCH 030/136] Refactor WrapLink logic (#5288) https://github.com/XTLS/Xray-core/pull/5133 https://github.com/XTLS/Xray-core/pull/5286 --- app/reverse/bridge.go | 5 +++-- common/mux/server.go | 5 +++-- features/routing/dispatcher.go | 6 ++++++ proxy/vless/inbound/inbound.go | 14 ++++++++++---- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/reverse/bridge.go b/app/reverse/bridge.go index fc83a740..324fea59 100644 --- a/app/reverse/bridge.go +++ b/app/reverse/bridge.go @@ -4,7 +4,6 @@ import ( "context" "time" - "github.com/xtls/xray-core/app/dispatcher" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/mux" "github.com/xtls/xray-core/common/net" @@ -231,7 +230,9 @@ func (w *BridgeWorker) DispatchLink(ctx context.Context, dest net.Destination, l return w.Dispatcher.DispatchLink(ctx, dest, link) } - link = w.Dispatcher.(*dispatcher.DefaultDispatcher).WrapLink(ctx, link) + if d, ok := w.Dispatcher.(routing.WrapLinkDispatcher); ok { + link = d.WrapLink(ctx, link) + } w.handleInternalConn(link) return nil diff --git a/common/mux/server.go b/common/mux/server.go index f01c325d..1c090185 100644 --- a/common/mux/server.go +++ b/common/mux/server.go @@ -5,7 +5,6 @@ import ( "io" "time" - "github.com/xtls/xray-core/app/dispatcher" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" @@ -64,7 +63,9 @@ func (s *Server) DispatchLink(ctx context.Context, dest net.Destination, link *t if dest.Address != muxCoolAddress { return s.dispatcher.DispatchLink(ctx, dest, link) } - link = s.dispatcher.(*dispatcher.DefaultDispatcher).WrapLink(ctx, link) + if d, ok := s.dispatcher.(routing.WrapLinkDispatcher); ok { + link = d.WrapLink(ctx, link) + } worker, err := NewServerWorker(ctx, s.dispatcher, link) if err != nil { return err diff --git a/features/routing/dispatcher.go b/features/routing/dispatcher.go index 53d3bf90..c8354446 100644 --- a/features/routing/dispatcher.go +++ b/features/routing/dispatcher.go @@ -26,3 +26,9 @@ type Dispatcher interface { func DispatcherType() interface{} { return (*Dispatcher)(nil) } + +// Just for type assertion +type WrapLinkDispatcher interface { + Dispatcher + WrapLink(ctx context.Context, link *transport.Link) *transport.Link +} diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 223aade0..277ab068 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -12,7 +12,6 @@ import ( "time" "unsafe" - "github.com/xtls/xray-core/app/dispatcher" "github.com/xtls/xray-core/app/reverse" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" @@ -76,7 +75,7 @@ type Handler struct { validator vless.Validator decryption *encryption.ServerInstance outboundHandlerManager outbound.Manager - defaultDispatcher *dispatcher.DefaultDispatcher + wrapLink func(ctx context.Context, link *transport.Link) *transport.Link ctx context.Context fallbacks map[string]map[string]map[string]*Fallback // or nil // regexps map[string]*regexp.Regexp // or nil @@ -85,12 +84,16 @@ type Handler struct { // New creates a new VLess inbound handler. func New(ctx context.Context, config *Config, dc dns.Client, validator vless.Validator) (*Handler, error) { v := core.MustFromContext(ctx) + var wrapLinkFunc func(ctx context.Context, link *transport.Link) *transport.Link + if dispatcher, ok := v.GetFeature(routing.DispatcherType()).(routing.WrapLinkDispatcher); ok { + wrapLinkFunc = dispatcher.WrapLink + } handler := &Handler{ inboundHandlerManager: v.GetFeature(feature_inbound.ManagerType()).(feature_inbound.Manager), policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), validator: validator, outboundHandlerManager: v.GetFeature(outbound.ManagerType()).(outbound.Manager), - defaultDispatcher: v.GetFeature(routing.DispatcherType()).(*dispatcher.DefaultDispatcher), + wrapLink: wrapLinkFunc, ctx: ctx, } @@ -619,7 +622,10 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s if err != nil { return err } - return r.NewMux(ctx, h.defaultDispatcher.WrapLink(ctx, &transport.Link{Reader: clientReader, Writer: clientWriter})) + if h.wrapLink == nil { + return errors.New("VLESS reverse must have a dispatcher that implemented routing.WrapLinkDispatcher") + } + return r.NewMux(ctx, h.wrapLink(ctx, &transport.Link{Reader: clientReader, Writer: clientWriter})) } if err := dispatcher.DispatchLink(ctx, request.Destination(), &transport.Link{ From acfda31e597026c184c28f1694ff3be59dfa2cf7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 00:13:10 +0000 Subject: [PATCH 031/136] Bump actions/checkout from 5 to 6 (#5324) Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- .github/workflows/release-win7.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1fcee949..7601290f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -65,7 +65,7 @@ jobs: echo "LATEST=$LATEST" >>${GITHUB_ENV} - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/release-win7.yml b/.github/workflows/release-win7.yml index 294a5e1b..7062d262 100644 --- a/.github/workflows/release-win7.yml +++ b/.github/workflows/release-win7.yml @@ -63,7 +63,7 @@ jobs: CGO_ENABLED: 0 steps: - name: Checkout codebase - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Show workflow information run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b717f3e9..8353b245 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -153,7 +153,7 @@ jobs: CGO_ENABLED: 0 steps: - name: Checkout codebase - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up NDK if: matrix.goos == 'android' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d84fee1e..3ce9d1a7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: os: [windows-latest, ubuntu-latest, macos-latest] steps: - name: Checkout codebase - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: From e914183996a6d8c4742ee9adc7c97ad72beb5629 Mon Sep 17 00:00:00 2001 From: hax0r31337 <65506006+hax0r31337@users.noreply.github.com> Date: Fri, 21 Nov 2025 03:29:25 +0100 Subject: [PATCH 032/136] HTTP outbound: Read negotiated protocol from uTLS (#5251) --- proxy/http/client.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/proxy/http/client.go b/proxy/http/client.go index d5be2acb..9544ad37 100644 --- a/proxy/http/client.go +++ b/proxy/http/client.go @@ -308,6 +308,12 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u return nil, err } nextProto = tlsConn.ConnectionState().NegotiatedProtocol + } else if tlsConn, ok := iConn.(*tls.UConn); ok { + if err := tlsConn.HandshakeContext(ctx); err != nil { + rawConn.Close() + return nil, err + } + nextProto = tlsConn.ConnectionState().NegotiatedProtocol } switch nextProto { From 27ad487545737dfb527d4c781132dd4cb2153112 Mon Sep 17 00:00:00 2001 From: vanserox Date: Fri, 21 Nov 2025 10:31:34 +0800 Subject: [PATCH 033/136] DNS: Fix wrong protocol parse (#5232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 风扇滑翔翼 --- app/dns/nameserver_quic.go | 2 +- app/dns/nameserver_tcp.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/dns/nameserver_quic.go b/app/dns/nameserver_quic.go index 31af657e..75b9854e 100644 --- a/app/dns/nameserver_quic.go +++ b/app/dns/nameserver_quic.go @@ -146,7 +146,7 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- e noResponseErrCh <- err return } - var length int16 + var length uint16 err = binary.Read(bytes.NewReader(respBuf.Bytes()), binary.BigEndian, &length) if err != nil { errors.LogErrorInner(ctx, err, "failed to parse response length") diff --git a/app/dns/nameserver_tcp.go b/app/dns/nameserver_tcp.go index 1937c25c..a5e81ae0 100644 --- a/app/dns/nameserver_tcp.go +++ b/app/dns/nameserver_tcp.go @@ -173,7 +173,7 @@ func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er noResponseErrCh <- err return } - var length int16 + var length uint16 err = binary.Read(bytes.NewReader(respBuf.Bytes()), binary.BigEndian, &length) if err != nil { errors.LogErrorInner(ctx, err, "failed to parse response length") From b40bf56e4ed2312dde0991b733f60fe41b9e24a0 Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:45:09 +0800 Subject: [PATCH 034/136] refactor(dns): enhance cache safety, optimize performance, and refactor query logic (#5248) --- app/dns/cache_controller.go | 375 ++++++++++++++++++++++++----------- app/dns/dnscommon.go | 1 + app/dns/nameserver_cached.go | 149 ++++++++++++++ app/dns/nameserver_doh.go | 70 ++----- app/dns/nameserver_quic.go | 62 +----- app/dns/nameserver_tcp.go | 61 +----- app/dns/nameserver_udp.go | 61 +----- 7 files changed, 454 insertions(+), 325 deletions(-) create mode 100644 app/dns/nameserver_cached.go diff --git a/app/dns/cache_controller.go b/app/dns/cache_controller.go index f23c414d..85818039 100644 --- a/app/dns/cache_controller.go +++ b/app/dns/cache_controller.go @@ -3,24 +3,37 @@ package dns import ( "context" go_errors "errors" + "runtime" + "sync" + "time" + "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/signal/pubsub" "github.com/xtls/xray-core/common/task" dns_feature "github.com/xtls/xray-core/features/dns" + "golang.org/x/net/dns/dnsmessage" - "sync" - "time" + "golang.org/x/sync/singleflight" +) + +const ( + minSizeForEmptyRebuild = 512 + shrinkAbsoluteThreshold = 10240 + shrinkRatioThreshold = 0.65 + migrationBatchSize = 4096 ) type CacheController struct { sync.RWMutex - ips map[string]*record - pub *pubsub.Service - cacheCleanup *task.Periodic - name string - disableCache bool + ips map[string]*record + dirtyips map[string]*record + pub *pubsub.Service + cacheCleanup *task.Periodic + name string + disableCache bool + highWatermark int + requestGroup singleflight.Group } func NewCacheController(name string, disableCache bool) *CacheController { @@ -32,7 +45,7 @@ func NewCacheController(name string, disableCache bool) *CacheController { } c.cacheCleanup = &task.Periodic{ - Interval: time.Minute, + Interval: 300 * time.Second, Execute: c.CacheCleanup, } return c @@ -40,131 +53,253 @@ func NewCacheController(name string, disableCache bool) *CacheController { // CacheCleanup clears expired items from cache func (c *CacheController) CacheCleanup() error { - now := time.Now() - c.Lock() - defer c.Unlock() - - if len(c.ips) == 0 { - return errors.New("nothing to do. stopping...") + expiredKeys, err := c.collectExpiredKeys() + if err != nil { + return err } - - for domain, record := range c.ips { - if record.A != nil && record.A.Expire.Before(now) { - record.A = nil - } - if record.AAAA != nil && record.AAAA.Expire.Before(now) { - record.AAAA = nil - } - - if record.A == nil && record.AAAA == nil { - errors.LogDebug(context.Background(), c.name, "cache cleanup ", domain) - delete(c.ips, domain) - } else { - c.ips[domain] = record - } + if len(expiredKeys) == 0 { + return nil } - - if len(c.ips) == 0 { - c.ips = make(map[string]*record) - } - + c.writeAndShrink(expiredKeys) return nil } -func (c *CacheController) updateIP(req *dnsRequest, ipRec *IPRecord) { - elapsed := time.Since(req.start) +func (c *CacheController) collectExpiredKeys() ([]string, error) { + c.RLock() + defer c.RUnlock() + + if len(c.ips) == 0 { + return nil, errors.New("nothing to do. stopping...") + } + + // skip collection if a migration is in progress + if c.dirtyips != nil { + return nil, nil + } + + now := time.Now() + expiredKeys := make([]string, 0, len(c.ips)/4) // pre-allocate + + for domain, rec := range c.ips { + if (rec.A != nil && rec.A.Expire.Before(now)) || + (rec.AAAA != nil && rec.AAAA.Expire.Before(now)) { + expiredKeys = append(expiredKeys, domain) + } + } + + return expiredKeys, nil +} + +func (c *CacheController) writeAndShrink(expiredKeys []string) { + c.Lock() + defer c.Unlock() + + // double check to prevent upper call multiple cleanup tasks + if c.dirtyips != nil { + return + } + + lenBefore := len(c.ips) + if lenBefore > c.highWatermark { + c.highWatermark = lenBefore + } + + now := time.Now() + for _, domain := range expiredKeys { + rec := c.ips[domain] + if rec == nil { + continue + } + if rec.A != nil && rec.A.Expire.Before(now) { + rec.A = nil + } + if rec.AAAA != nil && rec.AAAA.Expire.Before(now) { + rec.AAAA = nil + } + if rec.A == nil && rec.AAAA == nil { + delete(c.ips, domain) + } + } + + lenAfter := len(c.ips) + + if lenAfter == 0 { + if c.highWatermark >= minSizeForEmptyRebuild { + errors.LogDebug(context.Background(), c.name, + " rebuilding empty cache map to reclaim memory.", + " size_before_cleanup=", lenBefore, + " peak_size_before_rebuild=", c.highWatermark, + ) + + c.ips = make(map[string]*record) + c.highWatermark = 0 + } + return + } + + if reductionFromPeak := c.highWatermark - lenAfter; reductionFromPeak > shrinkAbsoluteThreshold && + float64(reductionFromPeak) > float64(c.highWatermark)*shrinkRatioThreshold { + errors.LogDebug(context.Background(), c.name, + " shrinking cache map to reclaim memory.", + " new_size=", lenAfter, + " peak_size_before_shrink=", c.highWatermark, + " reduction_since_peak=", reductionFromPeak, + ) + + c.dirtyips = c.ips + c.ips = make(map[string]*record, int(float64(lenAfter)*1.1)) + c.highWatermark = lenAfter + go c.migrate() + } + +} + +type migrationEntry struct { + key string + value *record +} + +func (c *CacheController) migrate() { + defer func() { + if r := recover(); r != nil { + errors.LogError(context.Background(), c.name, " panic during cache migration: ", r) + c.Lock() + c.dirtyips = nil + // c.ips = make(map[string]*record) + // c.highWatermark = 0 + c.Unlock() + } + }() + + c.RLock() + dirtyips := c.dirtyips + c.RUnlock() + + // double check to prevent upper call multiple cleanup tasks + if dirtyips == nil { + return + } + + errors.LogDebug(context.Background(), c.name, " starting background cache migration for ", len(dirtyips), " items.") + + batch := make([]migrationEntry, 0, migrationBatchSize) + for domain, recD := range dirtyips { + batch = append(batch, migrationEntry{domain, recD}) + + if len(batch) >= migrationBatchSize { + c.flush(batch) + batch = batch[:0] + runtime.Gosched() + } + } + if len(batch) > 0 { + c.flush(batch) + } c.Lock() - rec, found := c.ips[req.domain] - if !found { - rec = &record{} - } - - switch req.reqType { - case dnsmessage.TypeA: - rec.A = ipRec - case dnsmessage.TypeAAAA: - rec.AAAA = ipRec - } - - errors.LogInfo(context.Background(), c.name, " got answer: ", req.domain, " ", req.reqType, " -> ", ipRec.IP, " ", elapsed) - c.ips[req.domain] = rec - - switch req.reqType { - case dnsmessage.TypeA: - c.pub.Publish(req.domain+"4", nil) - if !c.disableCache { - _, _, err := rec.AAAA.getIPs() - if !go_errors.Is(err, errRecordNotFound) { - c.pub.Publish(req.domain+"6", nil) - } - } - case dnsmessage.TypeAAAA: - c.pub.Publish(req.domain+"6", nil) - if !c.disableCache { - _, _, err := rec.A.getIPs() - if !go_errors.Is(err, errRecordNotFound) { - c.pub.Publish(req.domain+"4", nil) - } - } - } - + c.dirtyips = nil c.Unlock() + + errors.LogDebug(context.Background(), c.name, " cache migration completed.") +} + +func (c *CacheController) flush(batch []migrationEntry) { + c.Lock() + defer c.Unlock() + + for _, dirty := range batch { + if cur := c.ips[dirty.key]; cur != nil { + merge := &record{} + if cur.A == nil { + merge.A = dirty.value.A + } else { + merge.A = cur.A + } + if cur.AAAA == nil { + merge.AAAA = dirty.value.AAAA + } else { + merge.AAAA = cur.AAAA + } + c.ips[dirty.key] = merge + } else { + c.ips[dirty.key] = dirty.value + } + } +} + +func (c *CacheController) updateRecord(req *dnsRequest, rep *IPRecord) { + rtt := time.Since(req.start) + + switch req.reqType { + case dnsmessage.TypeA: + c.pub.Publish(req.domain+"4", rep) + case dnsmessage.TypeAAAA: + c.pub.Publish(req.domain+"6", rep) + } + + if c.disableCache { + errors.LogInfo(context.Background(), c.name, " got answer: ", req.domain, " ", req.reqType, " -> ", rep.IP, ", rtt: ", rtt) + return + } + + c.Lock() + lockWait := time.Since(req.start) - rtt + + newRec := &record{} + oldRec := c.ips[req.domain] + var dirtyRec *record + if c.dirtyips != nil { + dirtyRec = c.dirtyips[req.domain] + } + + var pubRecord *IPRecord + var pubSuffix string + + switch req.reqType { + case dnsmessage.TypeA: + newRec.A = rep + if oldRec != nil && oldRec.AAAA != nil { + newRec.AAAA = oldRec.AAAA + pubRecord = oldRec.AAAA + } else if dirtyRec != nil && dirtyRec.AAAA != nil { + pubRecord = dirtyRec.AAAA + } + pubSuffix = "6" + case dnsmessage.TypeAAAA: + newRec.AAAA = rep + if oldRec != nil && oldRec.A != nil { + newRec.A = oldRec.A + pubRecord = oldRec.A + } else if dirtyRec != nil && dirtyRec.A != nil { + pubRecord = dirtyRec.A + } + pubSuffix = "4" + } + + c.ips[req.domain] = newRec + c.Unlock() + + if pubRecord != nil { + _, _ /*ttl*/, err := pubRecord.getIPs() + if /*ttl >= 0 &&*/ !go_errors.Is(err, errRecordNotFound) { + c.pub.Publish(req.domain+pubSuffix, pubRecord) + } + } + + errors.LogInfo(context.Background(), c.name, " got answer: ", req.domain, " ", req.reqType, " -> ", rep.IP, ", rtt: ", rtt, ", lock: ", lockWait) + common.Must(c.cacheCleanup.Start()) } -func (c *CacheController) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { +func (c *CacheController) findRecords(domain string) *record { c.RLock() - record, found := c.ips[domain] - c.RUnlock() + defer c.RUnlock() - if !found { - return nil, 0, errRecordNotFound + rec := c.ips[domain] + if rec == nil && c.dirtyips != nil { + rec = c.dirtyips[domain] } - - var errs []error - var allIPs []net.IP - var rTTL uint32 = dns_feature.DefaultTTL - - mergeReq := option.IPv4Enable && option.IPv6Enable - - if option.IPv4Enable { - ips, ttl, err := record.A.getIPs() - if !mergeReq || go_errors.Is(err, errRecordNotFound) { - return ips, ttl, err - } - if ttl < rTTL { - rTTL = ttl - } - if len(ips) > 0 { - allIPs = append(allIPs, ips...) - } else { - errs = append(errs, err) - } - } - - if option.IPv6Enable { - ips, ttl, err := record.AAAA.getIPs() - if !mergeReq || go_errors.Is(err, errRecordNotFound) { - return ips, ttl, err - } - if ttl < rTTL { - rTTL = ttl - } - if len(ips) > 0 { - allIPs = append(allIPs, ips...) - } else { - errs = append(errs, err) - } - } - - if len(allIPs) > 0 { - return allIPs, rTTL, nil - } - if go_errors.Is(errs[0], errs[1]) { - return nil, rTTL, errs[0] - } - return nil, rTTL, errors.Combine(errs...) + return rec } func (c *CacheController) registerSubscribers(domain string, option dns_feature.IPOption) (sub4 *pubsub.Subscriber, sub6 *pubsub.Subscriber) { diff --git a/app/dns/dnscommon.go b/app/dns/dnscommon.go index 15ac2efe..e4ec8ae8 100644 --- a/app/dns/dnscommon.go +++ b/app/dns/dnscommon.go @@ -17,6 +17,7 @@ import ( ) // Fqdn normalizes domain make sure it ends with '.' +// case-sensitive func Fqdn(domain string) string { if len(domain) > 0 && strings.HasSuffix(domain, ".") { return domain diff --git a/app/dns/nameserver_cached.go b/app/dns/nameserver_cached.go new file mode 100644 index 00000000..d1872b0c --- /dev/null +++ b/app/dns/nameserver_cached.go @@ -0,0 +1,149 @@ +package dns + +import ( + "context" + go_errors "errors" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/signal/pubsub" + "github.com/xtls/xray-core/features/dns" +) + +type CachedNameserver interface { + getCacheController() *CacheController + + sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns.IPOption) +} + +// queryIP is called from dns.Server->queryIPTimeout +func queryIP(ctx context.Context, s CachedNameserver, domain string, option dns.IPOption) ([]net.IP, uint32, error) { + fqdn := Fqdn(domain) + + cache := s.getCacheController() + if !cache.disableCache { + if rec := cache.findRecords(fqdn); rec != nil { + ips, ttl, err := merge(option, rec.A, rec.AAAA) + if !go_errors.Is(err, errRecordNotFound) { + // errors.LogDebugInner(ctx, err, cache.name, " cache HIT ", fqdn, " -> ", ips) + log.Record(&log.DNSLog{Server: cache.name, Domain: fqdn, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) + return ips, ttl, err + } + } + } else { + errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", fqdn, " at ", cache.name) + } + + return fetch(ctx, s, fqdn, option) +} + +func fetch(ctx context.Context, s CachedNameserver, fqdn string, option dns.IPOption) ([]net.IP, uint32, error) { + key := fqdn + "f" + switch { + case option.IPv4Enable && option.IPv6Enable: + key = key + "46" + case option.IPv4Enable: + key = key + "4" + case option.IPv6Enable: + key = key + "6" + } + + v, _, _ := s.getCacheController().requestGroup.Do(key, func() (any, error) { + return doFetch(ctx, s, fqdn, option), nil + }) + ret := v.(result) + + return ret.ips, ret.ttl, ret.error +} + +type result struct { + ips []net.IP + ttl uint32 + error +} + +func doFetch(ctx context.Context, s CachedNameserver, fqdn string, option dns.IPOption) result { + sub4, sub6 := s.getCacheController().registerSubscribers(fqdn, option) + defer closeSubscribers(sub4, sub6) + + noResponseErrCh := make(chan error, 2) + onEvent := func(sub *pubsub.Subscriber) (*IPRecord, error) { + if sub == nil { + return nil, nil + } + select { + case <-ctx.Done(): + return nil, ctx.Err() + case err := <-noResponseErrCh: + return nil, err + case msg := <-sub.Wait(): + sub.Close() + return msg.(*IPRecord), nil // should panic + } + } + + start := time.Now() + s.sendQuery(ctx, noResponseErrCh, fqdn, option) + + rec4, err4 := onEvent(sub4) + rec6, err6 := onEvent(sub6) + + var errs []error + if err4 != nil { + errs = append(errs, err4) + } + if err6 != nil { + errs = append(errs, err6) + } + + ips, ttl, err := merge(option, rec4, rec6, errs...) + log.Record(&log.DNSLog{Server: s.getCacheController().name, Domain: fqdn, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err}) + return result{ips, ttl, err} +} + +func merge(option dns.IPOption, rec4 *IPRecord, rec6 *IPRecord, errs ...error) ([]net.IP, uint32, error) { + var allIPs []net.IP + var rTTL uint32 = dns.DefaultTTL + + mergeReq := option.IPv4Enable && option.IPv6Enable + + if option.IPv4Enable { + ips, ttl, err := rec4.getIPs() // it's safe + if !mergeReq || go_errors.Is(err, errRecordNotFound) { + return ips, ttl, err + } + if ttl < rTTL { + rTTL = ttl + } + if len(ips) > 0 { + allIPs = append(allIPs, ips...) + } else { + errs = append(errs, err) + } + } + + if option.IPv6Enable { + ips, ttl, err := rec6.getIPs() // it's safe + if !mergeReq || go_errors.Is(err, errRecordNotFound) { + return ips, ttl, err + } + if ttl < rTTL { + rTTL = ttl + } + if len(ips) > 0 { + allIPs = append(allIPs, ips...) + } else { + errs = append(errs, err) + } + } + + if len(allIPs) > 0 { + return allIPs, rTTL, nil + } + if len(errs) == 2 && go_errors.Is(errs[0], errs[1]) { + return nil, rTTL, errs[0] + } + return nil, rTTL, errors.Combine(errs...) +} diff --git a/app/dns/nameserver_doh.go b/app/dns/nameserver_doh.go index cba59423..ebdea6a2 100644 --- a/app/dns/nameserver_doh.go +++ b/app/dns/nameserver_doh.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "crypto/tls" - go_errors "errors" "fmt" "io" "net/http" @@ -121,10 +120,16 @@ func (s *DoHNameServer) newReqID() uint16 { return 0 } -func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, domain string, option dns_feature.IPOption) { - errors.LogInfo(ctx, s.Name(), " querying: ", domain) +// getCacheController implements CachedNameserver. +func (s *DoHNameServer) getCacheController() *CacheController { + return s.cacheController +} - if s.Name()+"." == "DOH//"+domain { +// sendQuery implements CachedNameserver. +func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { + errors.LogInfo(ctx, s.Name(), " querying: ", fqdn) + + if s.Name()+"." == "DOH//"+fqdn { errors.LogError(ctx, s.Name(), " tries to resolve itself! Use IP or set \"hosts\" instead.") noResponseErrCh <- errors.New("tries to resolve itself!", s.Name()) return @@ -132,7 +137,7 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er // As we don't want our traffic pattern looks like DoH, we use Random-Length Padding instead of Block-Length Padding recommended in RFC 8467 // Although DoH server like 1.1.1.1 will pad the response to Block-Length 468, at least it is better than no padding for response at all - reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP, int(crypto.RandBetween(100, 300)))) + reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, int(crypto.RandBetween(100, 300)))) var deadline time.Time if d, ok := ctx.Deadline(); ok { @@ -166,23 +171,23 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er b, err := dns.PackMessage(r.msg) if err != nil { - errors.LogErrorInner(ctx, err, "failed to pack dns query for ", domain) + errors.LogErrorInner(ctx, err, "failed to pack dns query for ", fqdn) noResponseErrCh <- err return } resp, err := s.dohHTTPSContext(dnsCtx, b.Bytes()) if err != nil { - errors.LogErrorInner(ctx, err, "failed to retrieve response for ", domain) + errors.LogErrorInner(ctx, err, "failed to retrieve response for ", fqdn) noResponseErrCh <- err return } rec, err := parseResponse(resp) if err != nil { - errors.LogErrorInner(ctx, err, "failed to handle DOH response for ", domain) + errors.LogErrorInner(ctx, err, "failed to handle DOH response for ", fqdn) noResponseErrCh <- err return } - s.cacheController.updateIP(r, rec) + s.cacheController.updateRecord(r, rec) }(req) } } @@ -216,49 +221,6 @@ func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte, } // QueryIP implements Server. -func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { // nolint: dupl - fqdn := Fqdn(domain) - sub4, sub6 := s.cacheController.registerSubscribers(fqdn, option) - defer closeSubscribers(sub4, sub6) - - if s.cacheController.disableCache { - errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.Name()) - } else { - ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option) - if !go_errors.Is(err, errRecordNotFound) { - errors.LogDebugInner(ctx, err, s.Name(), " cache HIT ", domain, " -> ", ips) - log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) - return ips, ttl, err - } - } - - noResponseErrCh := make(chan error, 2) - s.sendQuery(ctx, noResponseErrCh, fqdn, option) - start := time.Now() - - if sub4 != nil { - select { - case <-ctx.Done(): - return nil, 0, ctx.Err() - case err := <-noResponseErrCh: - return nil, 0, err - case <-sub4.Wait(): - sub4.Close() - } - } - if sub6 != nil { - select { - case <-ctx.Done(): - return nil, 0, ctx.Err() - case err := <-noResponseErrCh: - return nil, 0, err - case <-sub6.Wait(): - sub6.Close() - } - } - - ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option) - log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err}) - return ips, ttl, err - +func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { + return queryIP(ctx, s, domain, option) } diff --git a/app/dns/nameserver_quic.go b/app/dns/nameserver_quic.go index 75b9854e..c294ecda 100644 --- a/app/dns/nameserver_quic.go +++ b/app/dns/nameserver_quic.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/binary" - go_errors "errors" "net/url" "sync" "time" @@ -59,7 +58,7 @@ func NewQUICNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*QUICN return s, nil } -// Name returns client name +// Name implements Server. func (s *QUICNameServer) Name() string { return s.cacheController.name } @@ -68,10 +67,14 @@ func (s *QUICNameServer) newReqID() uint16 { return 0 } -func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, domain string, option dns_feature.IPOption) { - errors.LogInfo(ctx, s.Name(), " querying: ", domain) +// getCacheController implements CachedNameServer. +func (s *QUICNameServer) getCacheController() *CacheController { return s.cacheController } - reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) +// sendQuery implements CachedNameServer. +func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { + errors.LogInfo(ctx, s.Name(), " querying: ", fqdn) + + reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) var deadline time.Time if d, ok := ctx.Deadline(); ok { @@ -167,57 +170,14 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- e noResponseErrCh <- err return } - s.cacheController.updateIP(r, rec) + s.cacheController.updateRecord(r, rec) }(req) } } -// QueryIP is called from dns.Server->queryIPTimeout +// QueryIP implements Server. func (s *QUICNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { - fqdn := Fqdn(domain) - sub4, sub6 := s.cacheController.registerSubscribers(fqdn, option) - defer closeSubscribers(sub4, sub6) - - if s.cacheController.disableCache { - errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.Name()) - } else { - ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option) - if !go_errors.Is(err, errRecordNotFound) { - errors.LogDebugInner(ctx, err, s.Name(), " cache HIT ", domain, " -> ", ips) - log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) - return ips, ttl, err - } - } - - noResponseErrCh := make(chan error, 2) - s.sendQuery(ctx, noResponseErrCh, fqdn, option) - start := time.Now() - - if sub4 != nil { - select { - case <-ctx.Done(): - return nil, 0, ctx.Err() - case err := <-noResponseErrCh: - return nil, 0, err - case <-sub4.Wait(): - sub4.Close() - } - } - if sub6 != nil { - select { - case <-ctx.Done(): - return nil, 0, ctx.Err() - case err := <-noResponseErrCh: - return nil, 0, err - case <-sub6.Wait(): - sub6.Close() - } - } - - ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option) - log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err}) - return ips, ttl, err - + return queryIP(ctx, s, domain, option) } func isActive(s *quic.Conn) bool { diff --git a/app/dns/nameserver_tcp.go b/app/dns/nameserver_tcp.go index a5e81ae0..b9a2c2de 100644 --- a/app/dns/nameserver_tcp.go +++ b/app/dns/nameserver_tcp.go @@ -4,14 +4,12 @@ import ( "bytes" "context" "encoding/binary" - go_errors "errors" "net/url" "sync/atomic" "time" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net/cnc" "github.com/xtls/xray-core/common/protocol/dns" @@ -99,10 +97,16 @@ func (s *TCPNameServer) newReqID() uint16 { return uint16(atomic.AddUint32(&s.reqID, 1)) } -func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, domain string, option dns_feature.IPOption) { - errors.LogDebug(ctx, s.Name(), " querying DNS for: ", domain) +// getCacheController implements CachedNameserver. +func (s *TCPNameServer) getCacheController() *CacheController { + return s.cacheController +} - reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) +// sendQuery implements CachedNameserver. +func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { + errors.LogDebug(ctx, s.Name(), " querying DNS for: ", fqdn) + + reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) var deadline time.Time if d, ok := ctx.Deadline(); ok { @@ -195,55 +199,12 @@ func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er return } - s.cacheController.updateIP(r, rec) + s.cacheController.updateRecord(r, rec) }(req) } } // QueryIP implements Server. func (s *TCPNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { - fqdn := Fqdn(domain) - sub4, sub6 := s.cacheController.registerSubscribers(fqdn, option) - defer closeSubscribers(sub4, sub6) - - if s.cacheController.disableCache { - errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.Name()) - } else { - ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option) - if !go_errors.Is(err, errRecordNotFound) { - errors.LogDebugInner(ctx, err, s.Name(), " cache HIT ", domain, " -> ", ips) - log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) - return ips, ttl, err - } - } - - noResponseErrCh := make(chan error, 2) - s.sendQuery(ctx, noResponseErrCh, fqdn, option) - start := time.Now() - - if sub4 != nil { - select { - case <-ctx.Done(): - return nil, 0, ctx.Err() - case err := <-noResponseErrCh: - return nil, 0, err - case <-sub4.Wait(): - sub4.Close() - } - } - if sub6 != nil { - select { - case <-ctx.Done(): - return nil, 0, ctx.Err() - case err := <-noResponseErrCh: - return nil, 0, err - case <-sub6.Wait(): - sub6.Close() - } - } - - ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option) - log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err}) - return ips, ttl, err - + return queryIP(ctx, s, domain, option) } diff --git a/app/dns/nameserver_udp.go b/app/dns/nameserver_udp.go index e29f6e24..67a57b7f 100644 --- a/app/dns/nameserver_udp.go +++ b/app/dns/nameserver_udp.go @@ -2,7 +2,6 @@ package dns import ( "context" - go_errors "errors" "strings" "sync" "sync/atomic" @@ -10,7 +9,6 @@ import ( "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol/dns" udp_proto "github.com/xtls/xray-core/common/protocol/udp" @@ -134,7 +132,7 @@ func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_prot } } - s.cacheController.updateIP(&req.dnsRequest, ipRec) + s.cacheController.updateRecord(&req.dnsRequest, ipRec) } func (s *ClassicNameServer) newReqID() uint16 { @@ -150,10 +148,16 @@ func (s *ClassicNameServer) addPendingRequest(req *udpDnsRequest) { common.Must(s.requestsCleanup.Start()) } -func (s *ClassicNameServer) sendQuery(ctx context.Context, _ chan<- error, domain string, option dns_feature.IPOption) { - errors.LogDebug(ctx, s.Name(), " querying DNS for: ", domain) +// getCacheController implements CachedNameserver. +func (s *ClassicNameServer) getCacheController() *CacheController { + return s.cacheController +} - reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) +// sendQuery implements CachedNameserver. +func (s *ClassicNameServer) sendQuery(ctx context.Context, _ chan<- error, fqdn string, option dns_feature.IPOption) { + errors.LogDebug(ctx, s.Name(), " querying DNS for: ", fqdn) + + reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) for _, req := range reqs { udpReq := &udpDnsRequest{ @@ -170,48 +174,5 @@ func (s *ClassicNameServer) sendQuery(ctx context.Context, _ chan<- error, domai // QueryIP implements Server. func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { - fqdn := Fqdn(domain) - sub4, sub6 := s.cacheController.registerSubscribers(fqdn, option) - defer closeSubscribers(sub4, sub6) - - if s.cacheController.disableCache { - errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.Name()) - } else { - ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option) - if !go_errors.Is(err, errRecordNotFound) { - errors.LogDebugInner(ctx, err, s.Name(), " cache HIT ", domain, " -> ", ips) - log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) - return ips, ttl, err - } - } - - noResponseErrCh := make(chan error, 2) - s.sendQuery(ctx, noResponseErrCh, fqdn, option) - start := time.Now() - - if sub4 != nil { - select { - case <-ctx.Done(): - return nil, 0, ctx.Err() - case err := <-noResponseErrCh: - return nil, 0, err - case <-sub4.Wait(): - sub4.Close() - } - } - if sub6 != nil { - select { - case <-ctx.Done(): - return nil, 0, ctx.Err() - case err := <-noResponseErrCh: - return nil, 0, err - case <-sub6.Wait(): - sub6.Close() - } - } - - ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option) - log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err}) - return ips, ttl, err - + return queryIP(ctx, s, domain, option) } From fcfb0a302a2d167211153f1bac1d57efeea8ff9d Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:54:01 +0800 Subject: [PATCH 035/136] perf(GeoIPMatcher): faster heuristic matching with reduced memory usage (#5289) --- app/dns/nameserver.go | 38 +- app/router/condition.go | 69 +- app/router/condition_geoip.go | 1005 +++++++++++++++++++++++++--- app/router/condition_geoip_test.go | 71 +- app/router/condition_test.go | 2 +- app/router/config.go | 14 +- 6 files changed, 996 insertions(+), 203 deletions(-) diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go index cf1b665b..3f025d74 100644 --- a/app/dns/nameserver.go +++ b/app/dns/nameserver.go @@ -29,8 +29,8 @@ type Client struct { server Server skipFallback bool domains []string - expectedIPs []*router.GeoIPMatcher - unexpectedIPs []*router.GeoIPMatcher + expectedIPs router.GeoIPMatcher + unexpectedIPs router.GeoIPMatcher actPrior bool actUnprior bool tag string @@ -154,23 +154,21 @@ func NewClient( } // Establish expected IPs - var expectedMatchers []*router.GeoIPMatcher - for _, geoip := range ns.ExpectedGeoip { - matcher, err := router.GlobalGeoIPContainer.Add(geoip) + var expectedMatcher router.GeoIPMatcher + if len(ns.ExpectedGeoip) > 0 { + expectedMatcher, err = router.BuildOptimizedGeoIPMatcher(ns.ExpectedGeoip...) if err != nil { return errors.New("failed to create expected ip matcher").Base(err).AtWarning() } - expectedMatchers = append(expectedMatchers, matcher) } // Establish unexpected IPs - var unexpectedMatchers []*router.GeoIPMatcher - for _, geoip := range ns.UnexpectedGeoip { - matcher, err := router.GlobalGeoIPContainer.Add(geoip) + var unexpectedMatcher router.GeoIPMatcher + if len(ns.UnexpectedGeoip) > 0 { + unexpectedMatcher, err = router.BuildOptimizedGeoIPMatcher(ns.UnexpectedGeoip...) if err != nil { return errors.New("failed to create unexpected ip matcher").Base(err).AtWarning() } - unexpectedMatchers = append(unexpectedMatchers, matcher) } if len(clientIP) > 0 { @@ -192,8 +190,8 @@ func NewClient( client.server = server client.skipFallback = ns.SkipFallback client.domains = rules - client.expectedIPs = expectedMatchers - client.unexpectedIPs = unexpectedMatchers + client.expectedIPs = expectedMatcher + client.unexpectedIPs = unexpectedMatcher client.actPrior = ns.ActPrior client.actUnprior = ns.ActUnprior client.tag = tag @@ -243,32 +241,32 @@ func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption return nil, 0, dns.ErrEmptyResponse } - if len(c.expectedIPs) > 0 && !c.actPrior { - ips = router.MatchIPs(c.expectedIPs, ips, false) + if c.expectedIPs != nil && !c.actPrior { + ips, _ = c.expectedIPs.FilterIPs(ips) errors.LogDebug(context.Background(), "domain ", domain, " expectedIPs ", ips, " matched at server ", c.Name()) if len(ips) == 0 { return nil, 0, dns.ErrEmptyResponse } } - if len(c.unexpectedIPs) > 0 && !c.actUnprior { - ips = router.MatchIPs(c.unexpectedIPs, ips, true) + if c.unexpectedIPs != nil && !c.actUnprior { + _, ips = c.unexpectedIPs.FilterIPs(ips) errors.LogDebug(context.Background(), "domain ", domain, " unexpectedIPs ", ips, " matched at server ", c.Name()) if len(ips) == 0 { return nil, 0, dns.ErrEmptyResponse } } - if len(c.expectedIPs) > 0 && c.actPrior { - ipsNew := router.MatchIPs(c.expectedIPs, ips, false) + if c.expectedIPs != nil && c.actPrior { + ipsNew, _ := c.expectedIPs.FilterIPs(ips) if len(ipsNew) > 0 { ips = ipsNew errors.LogDebug(context.Background(), "domain ", domain, " priorIPs ", ips, " matched at server ", c.Name()) } } - if len(c.unexpectedIPs) > 0 && c.actUnprior { - ipsNew := router.MatchIPs(c.unexpectedIPs, ips, true) + if c.unexpectedIPs != nil && c.actUnprior { + _, ipsNew := c.unexpectedIPs.FilterIPs(ips) if len(ipsNew) > 0 { ips = ipsNew errors.LogDebug(context.Background(), "domain ", domain, " unpriorIPs ", ips, " matched at server ", c.Name()) diff --git a/app/router/condition.go b/app/router/condition.go index 4127ca47..c8cf4e8d 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -96,61 +96,53 @@ func (m *DomainMatcher) Apply(ctx routing.Context) bool { return m.ApplyDomain(domain) } -type MultiGeoIPMatcher struct { - matchers []*GeoIPMatcher - asType string // local, source, target +type MatcherAsType byte + +const ( + MatcherAsType_Local MatcherAsType = iota + MatcherAsType_Source + MatcherAsType_Target + MatcherAsType_VlessRoute // for port +) + +type IPMatcher struct { + matcher GeoIPMatcher + asType MatcherAsType } -func NewMultiGeoIPMatcher(geoips []*GeoIP, asType string) (*MultiGeoIPMatcher, error) { - var matchers []*GeoIPMatcher - for _, geoip := range geoips { - matcher, err := GlobalGeoIPContainer.Add(geoip) - if err != nil { - return nil, err - } - matchers = append(matchers, matcher) +func NewIPMatcher(geoips []*GeoIP, asType MatcherAsType) (*IPMatcher, error) { + matcher, err := BuildOptimizedGeoIPMatcher(geoips...) + if err != nil { + return nil, err } - - matcher := &MultiGeoIPMatcher{ - matchers: matchers, - asType: asType, - } - - return matcher, nil + return &IPMatcher{matcher: matcher, asType: asType}, nil } // Apply implements Condition. -func (m *MultiGeoIPMatcher) Apply(ctx routing.Context) bool { +func (m *IPMatcher) Apply(ctx routing.Context) bool { var ips []net.IP switch m.asType { - case "local": + case MatcherAsType_Local: ips = ctx.GetLocalIPs() - case "source": + case MatcherAsType_Source: ips = ctx.GetSourceIPs() - case "target": + case MatcherAsType_Target: ips = ctx.GetTargetIPs() default: - panic("unreachable, asType should be local or source or target") + panic("unk asType") } - for _, ip := range ips { - for _, matcher := range m.matchers { - if matcher.Match(ip) { - return true - } - } - } - return false + return m.matcher.AnyMatch(ips) } type PortMatcher struct { port net.MemoryPortList - asType string // local, source, target + asType MatcherAsType } // NewPortMatcher create a new port matcher that can match source or local or destination port -func NewPortMatcher(list *net.PortList, asType string) *PortMatcher { +func NewPortMatcher(list *net.PortList, asType MatcherAsType) *PortMatcher { return &PortMatcher{ port: net.PortListFromProto(list), asType: asType, @@ -160,18 +152,17 @@ func NewPortMatcher(list *net.PortList, asType string) *PortMatcher { // Apply implements Condition. func (v *PortMatcher) Apply(ctx routing.Context) bool { switch v.asType { - case "local": + case MatcherAsType_Local: return v.port.Contains(ctx.GetLocalPort()) - case "source": + case MatcherAsType_Source: return v.port.Contains(ctx.GetSourcePort()) - case "target": + case MatcherAsType_Target: return v.port.Contains(ctx.GetTargetPort()) - case "vlessRoute": + case MatcherAsType_VlessRoute: return v.port.Contains(ctx.GetVlessRoute()) default: - panic("unreachable, asType should be local or source or target") + panic("unk asType") } - } type NetworkMatcher struct { diff --git a/app/router/condition_geoip.go b/app/router/condition_geoip.go index 38f7f0ce..823548cf 100644 --- a/app/router/condition_geoip.go +++ b/app/router/condition_geoip.go @@ -1,144 +1,961 @@ package router import ( + "context" "net/netip" - "strconv" + "sort" + "strings" + "sync" + "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" + "go4.org/netipx" ) -type GeoIPMatcher struct { - countryCode string - reverseMatch bool - ip4 *netipx.IPSet - ip6 *netipx.IPSet +type GeoIPMatcher interface { + // TODO: (PERF) all net.IP -> netipx.Addr + + // Invalid IP always return false. + Match(ip net.IP) bool + + // Returns true if *any* IP is valid and match. + AnyMatch(ips []net.IP) bool + + // Returns true only if *all* IPs are valid and match. Any invalid IP, or non-matching valid IP, causes false. + Matches(ips []net.IP) bool + + // Filters IPs. Invalid IPs are silently dropped and not included in either result. + FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) + + ToggleReverse() + + SetReverse(reverse bool) } -func (m *GeoIPMatcher) Init(cidrs []*CIDR) error { - var builder4, builder6 netipx.IPSetBuilder +type GeoIPSet struct { + ipv4, ipv6 *netipx.IPSet + max4, max6 uint8 +} - for _, cidr := range cidrs { - ip := net.IP(cidr.GetIp()) - ipPrefixString := ip.String() + "/" + strconv.Itoa(int(cidr.GetPrefix())) - ipPrefix, err := netip.ParsePrefix(ipPrefixString) - if err != nil { - return err +type HeuristicGeoIPMatcher struct { + ipset *GeoIPSet + reverse bool +} + +type ipBucket struct { + rep netip.Addr + ips []net.IP +} + +// Match implements GeoIPMatcher. +func (m *HeuristicGeoIPMatcher) Match(ip net.IP) bool { + ipx, ok := netipx.FromStdIP(ip) + if !ok { + return false + } + return m.matchAddr(ipx) +} + +func (m *HeuristicGeoIPMatcher) matchAddr(ipx netip.Addr) bool { + if ipx.Is4() { + return m.ipset.ipv4.Contains(ipx) != m.reverse + } + if ipx.Is6() { + return m.ipset.ipv6.Contains(ipx) != m.reverse + } + return false +} + +// AnyMatch implements GeoIPMatcher. +func (m *HeuristicGeoIPMatcher) AnyMatch(ips []net.IP) bool { + n := len(ips) + if n == 0 { + return false + } + + if n == 1 { + return m.Match(ips[0]) + } + + heur4 := m.ipset.max4 <= 24 + heur6 := m.ipset.max6 <= 64 + if !heur4 && !heur6 { + for _, ip := range ips { + if ipx, ok := netipx.FromStdIP(ip); ok { + if m.matchAddr(ipx) { + return true + } + } + } + return false + } + + buckets := make(map[[9]byte]struct{}, n) + for _, ip := range ips { + key, ok := prefixKeyFromIP(ip) + if !ok { + continue + } + heur := (key[0] == 4 && heur4) || (key[0] == 6 && heur6) + if heur { + if _, exists := buckets[key]; exists { + continue + } + } + ipx, ok := netipx.FromStdIP(ip) + if !ok { + continue + } + if m.matchAddr(ipx) { + return true + } + if heur { + buckets[key] = struct{}{} + } + } + return false +} + +// Matches implements GeoIPMatcher. +func (m *HeuristicGeoIPMatcher) Matches(ips []net.IP) bool { + n := len(ips) + if n == 0 { + return false + } + + if n == 1 { + return m.Match(ips[0]) + } + + heur4 := m.ipset.max4 <= 24 + heur6 := m.ipset.max6 <= 64 + if !heur4 && !heur6 { + for _, ip := range ips { + ipx, ok := netipx.FromStdIP(ip) + if !ok { + return false + } + if !m.matchAddr(ipx) { + return false + } + } + return true + } + + buckets := make(map[[9]byte]netip.Addr, n) + precise := make([]netip.Addr, 0, n) + + for _, ip := range ips { + key, ok := prefixKeyFromIP(ip) + if !ok { + return false } - switch len(ip) { - case net.IPv4len: - builder4.AddPrefix(ipPrefix) - case net.IPv6len: - builder6.AddPrefix(ipPrefix) + if (key[0] == 4 && heur4) || (key[0] == 6 && heur6) { + if _, exists := buckets[key]; !exists { + ipx, ok := netipx.FromStdIP(ip) + if !ok { + return false + } + buckets[key] = ipx + } + } else { + ipx, ok := netipx.FromStdIP(ip) + if !ok { + return false + } + precise = append(precise, ipx) } } - if ip4, err := builder4.IPSet(); err != nil { - return err - } else { - m.ip4 = ip4 + for _, ipx := range buckets { + if !m.matchAddr(ipx) { + return false + } } - - if ip6, err := builder6.IPSet(); err != nil { - return err - } else { - m.ip6 = ip6 + for _, ipx := range precise { + if !m.matchAddr(ipx) { + return false + } } - - return nil + return true } -func (m *GeoIPMatcher) SetReverseMatch(isReverseMatch bool) { - m.reverseMatch = isReverseMatch +func prefixKeyFromIP(ip net.IP) (key [9]byte, ok bool) { + if ip4 := ip.To4(); ip4 != nil { + key[0] = 4 + key[1] = ip4[0] + key[2] = ip4[1] + key[3] = ip4[2] // /24 + return key, true + } + if ip16 := ip.To16(); ip16 != nil { + key[0] = 6 + key[1] = ip16[0] + key[2] = ip16[1] + key[3] = ip16[2] + key[4] = ip16[3] + key[5] = ip16[4] + key[6] = ip16[5] + key[7] = ip16[6] + key[8] = ip16[7] // /64 + return key, true + } + return key, false // illegal } -func (m *GeoIPMatcher) match4(ip net.IP) bool { - nip, ok := netipx.FromStdIP(ip) +// FilterIPs implements GeoIPMatcher. +func (m *HeuristicGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) { + n := len(ips) + if n == 0 { + return []net.IP{}, []net.IP{} + } + + if n == 1 { + ipx, ok := netipx.FromStdIP(ips[0]) + if !ok { + return []net.IP{}, []net.IP{} + } + if m.matchAddr(ipx) { + return ips, []net.IP{} + } + return []net.IP{}, ips + } + + heur4 := m.ipset.max4 <= 24 + heur6 := m.ipset.max6 <= 64 + if !heur4 && !heur6 { + matched = make([]net.IP, 0, n) + unmatched = make([]net.IP, 0, n) + for _, ip := range ips { + ipx, ok := netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + if m.matchAddr(ipx) { + matched = append(matched, ip) + } else { + unmatched = append(unmatched, ip) + } + } + return + } + + buckets := make(map[[9]byte]*ipBucket, n) + precise := make([]net.IP, 0, n) + + for _, ip := range ips { + key, ok := prefixKeyFromIP(ip) + if !ok { + continue // illegal ip, ignore + } + + if (key[0] == 4 && !heur4) || (key[0] == 6 && !heur6) { + precise = append(precise, ip) + continue + } + + b, exists := buckets[key] + if !exists { + // build bucket + ipx, ok := netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + b = &ipBucket{ + rep: ipx, + ips: make([]net.IP, 0, 4), // for dns answer + } + buckets[key] = b + } + b.ips = append(b.ips, ip) + } + + matched = make([]net.IP, 0, n) + unmatched = make([]net.IP, 0, n) + for _, b := range buckets { + if m.matchAddr(b.rep) { + matched = append(matched, b.ips...) + } else { + unmatched = append(unmatched, b.ips...) + } + } + for _, ip := range precise { + ipx, ok := netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + if m.matchAddr(ipx) { + matched = append(matched, ip) + } else { + unmatched = append(unmatched, ip) + } + } + return +} + +// ToggleReverse implements GeoIPMatcher. +func (m *HeuristicGeoIPMatcher) ToggleReverse() { + m.reverse = !m.reverse +} + +// SetReverse implements GeoIPMatcher. +func (m *HeuristicGeoIPMatcher) SetReverse(reverse bool) { + m.reverse = reverse +} + +type GeneralMultiGeoIPMatcher struct { + matchers []GeoIPMatcher +} + +// Match implements GeoIPMatcher. +func (mm *GeneralMultiGeoIPMatcher) Match(ip net.IP) bool { + for _, m := range mm.matchers { + if m.Match(ip) { + return true + } + } + return false +} + +// AnyMatch implements GeoIPMatcher. +func (mm *GeneralMultiGeoIPMatcher) AnyMatch(ips []net.IP) bool { + for _, m := range mm.matchers { + if m.AnyMatch(ips) { + return true + } + } + return false +} + +// Matches implements GeoIPMatcher. +func (mm *GeneralMultiGeoIPMatcher) Matches(ips []net.IP) bool { + for _, m := range mm.matchers { + if m.Matches(ips) { + return true + } + } + return false +} + +// FilterIPs implements GeoIPMatcher. +func (mm *GeneralMultiGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) { + matched = make([]net.IP, 0, len(ips)) + unmatched = ips + for _, m := range mm.matchers { + if len(unmatched) == 0 { + break + } + var mtch []net.IP + mtch, unmatched = m.FilterIPs(unmatched) + if len(mtch) > 0 { + matched = append(matched, mtch...) + } + } + return +} + +// ToggleReverse implements GeoIPMatcher. +func (mm *GeneralMultiGeoIPMatcher) ToggleReverse() { + for _, m := range mm.matchers { + m.ToggleReverse() + } +} + +// SetReverse implements GeoIPMatcher. +func (mm *GeneralMultiGeoIPMatcher) SetReverse(reverse bool) { + for _, m := range mm.matchers { + m.SetReverse(reverse) + } +} + +type HeuristicMultiGeoIPMatcher struct { + matchers []*HeuristicGeoIPMatcher +} + +// Match implements GeoIPMatcher. +func (mm *HeuristicMultiGeoIPMatcher) Match(ip net.IP) bool { + ipx, ok := netipx.FromStdIP(ip) if !ok { return false } - return m.ip4.Contains(nip) + for _, m := range mm.matchers { + if m.matchAddr(ipx) { + return true + } + } + return false } -func (m *GeoIPMatcher) match6(ip net.IP) bool { - nip, ok := netipx.FromStdIP(ip) - if !ok { +// AnyMatch implements GeoIPMatcher. +func (mm *HeuristicMultiGeoIPMatcher) AnyMatch(ips []net.IP) bool { + n := len(ips) + if n == 0 { return false } - return m.ip6.Contains(nip) -} - -// Match returns true if the given ip is included by the GeoIP. -func (m *GeoIPMatcher) Match(ip net.IP) bool { - isMatched := false - switch len(ip) { - case net.IPv4len: - isMatched = m.match4(ip) - case net.IPv6len: - isMatched = m.match6(ip) + if n == 1 { + return mm.Match(ips[0]) } - if m.reverseMatch { - return !isMatched + + buckets := make(map[[9]byte]struct{}, n) + for _, ip := range ips { + var ipx netip.Addr + state := uint8(0) // 0 = Not initialized, 1 = Initialized, 4 = IPv4 can be skipped, 6 = IPv6 can be skipped + for _, m := range mm.matchers { + heur4 := m.ipset.max4 <= 24 + heur6 := m.ipset.max6 <= 64 + + if state == 0 && (heur4 || heur6) { + key, ok := prefixKeyFromIP(ip) + if !ok { + break + } + if _, exists := buckets[key]; exists { + state = key[0] + } else { + buckets[key] = struct{}{} + state = 1 + } + } + if (heur4 && state == 4) || (heur6 && state == 6) { + continue + } + + if !ipx.IsValid() { + nipx, ok := netipx.FromStdIP(ip) + if !ok { + break + } + ipx = nipx + } + if m.matchAddr(ipx) { + return true + } + } } - return isMatched + return false } -// GeoIPMatcherContainer is a container for GeoIPMatchers. It keeps unique copies of GeoIPMatcher by country code. -type GeoIPMatcherContainer struct { - matchers []*GeoIPMatcher +// Matches implements GeoIPMatcher. +func (mm *HeuristicMultiGeoIPMatcher) Matches(ips []net.IP) bool { + n := len(ips) + if n == 0 { + return false + } + + if n == 1 { + return mm.Match(ips[0]) + } + + var views ipViews + for _, m := range mm.matchers { + if !views.ensureForMatcher(m, ips) { + return false + } + + matched := true + if m.ipset.max4 <= 24 { + for _, ipx := range views.buckets4 { + if !m.matchAddr(ipx) { + matched = false + break + } + } + } else { + for _, ipx := range views.precise4 { + if !m.matchAddr(ipx) { + matched = false + break + } + } + } + if !matched { + continue + } + + if m.ipset.max6 <= 64 { + for _, ipx := range views.buckets6 { + if !m.matchAddr(ipx) { + matched = false + break + } + } + } else { + for _, ipx := range views.precise6 { + if !m.matchAddr(ipx) { + matched = false + break + } + } + } + if matched { + return true + } + } + return false } -// Add adds a new GeoIP set into the container. -// If the country code of GeoIP is not empty, GeoIPMatcherContainer will try to find an existing one, instead of adding a new one. -func (c *GeoIPMatcherContainer) Add(geoip *GeoIP) (*GeoIPMatcher, error) { - if len(geoip.CountryCode) > 0 { - for _, m := range c.matchers { - if m.countryCode == geoip.CountryCode && m.reverseMatch == geoip.ReverseMatch { - return m, nil +type ipViews struct { + buckets4, buckets6 map[[9]byte]netip.Addr + precise4, precise6 []netip.Addr +} + +func (v *ipViews) ensureForMatcher(m *HeuristicGeoIPMatcher, ips []net.IP) bool { + needHeur4 := m.ipset.max4 <= 24 && v.buckets4 == nil + needHeur6 := m.ipset.max6 <= 64 && v.buckets6 == nil + needPrec4 := m.ipset.max4 > 24 && v.precise4 == nil + needPrec6 := m.ipset.max6 > 64 && v.precise6 == nil + + if !needHeur4 && !needHeur6 && !needPrec4 && !needPrec6 { + return true + } + + if needHeur4 { + v.buckets4 = make(map[[9]byte]netip.Addr, len(ips)) + } + if needHeur6 { + v.buckets6 = make(map[[9]byte]netip.Addr, len(ips)) + } + if needPrec4 { + v.precise4 = make([]netip.Addr, 0, len(ips)) + } + if needPrec6 { + v.precise6 = make([]netip.Addr, 0, len(ips)) + } + + for _, ip := range ips { + key, ok := prefixKeyFromIP(ip) + if !ok { + return false + } + + switch key[0] { + case 4: + var ipx netip.Addr + if needHeur4 { + if _, exists := v.buckets4[key]; !exists { + ipx, ok = netipx.FromStdIP(ip) + if !ok { + return false + } + v.buckets4[key] = ipx + } + } + if needPrec4 { + if !ipx.IsValid() { + ipx, ok = netipx.FromStdIP(ip) + if !ok { + return false + } + } + v.precise4 = append(v.precise4, ipx) + } + case 6: + var ipx netip.Addr + if needHeur6 { + if _, exists := v.buckets6[key]; !exists { + ipx, ok = netipx.FromStdIP(ip) + if !ok { + return false + } + v.buckets6[key] = ipx + } + } + if needPrec6 { + if !ipx.IsValid() { + ipx, ok = netipx.FromStdIP(ip) + if !ok { + return false + } + } + v.precise6 = append(v.precise6, ipx) + } + default: + return false + } + } + + return true +} + +// FilterIPs implements GeoIPMatcher. +func (mm *HeuristicMultiGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) { + n := len(ips) + if n == 0 { + return []net.IP{}, []net.IP{} + } + + if n == 1 { + ipx, ok := netipx.FromStdIP(ips[0]) + if !ok { + return []net.IP{}, []net.IP{} + } + for _, m := range mm.matchers { + if m.matchAddr(ipx) { + return ips, []net.IP{} + } + } + return []net.IP{}, ips + } + + var views ipBucketViews + + matched = make([]net.IP, 0, n) + for _, m := range mm.matchers { + views.ensureForMatcher(m, ips) + + if m.ipset.max4 <= 24 { + for key, b := range views.buckets4 { + if b == nil { + continue + } + if m.matchAddr(b.rep) { + views.buckets4[key] = nil + matched = append(matched, b.ips...) + } + } + } else { + for ipx, ip := range views.precise4 { + if ip == nil { + continue + } + if m.matchAddr(ipx) { + views.precise4[ipx] = nil + matched = append(matched, ip) + } + } + } + + if m.ipset.max6 <= 64 { + for key, b := range views.buckets6 { + if b == nil { + continue + } + if m.matchAddr(b.rep) { + views.buckets6[key] = nil + matched = append(matched, b.ips...) + } + } + } else { + for ipx, ip := range views.precise6 { + if ip == nil { + continue + } + if m.matchAddr(ipx) { + views.precise6[ipx] = nil + matched = append(matched, ip) + } } } } - m := &GeoIPMatcher{ - countryCode: geoip.CountryCode, - reverseMatch: geoip.ReverseMatch, + unmatched = make([]net.IP, 0, n-len(matched)) + if views.buckets4 != nil { + for _, b := range views.buckets4 { + if b == nil { + continue + } + unmatched = append(unmatched, b.ips...) + } } - if err := m.Init(geoip.Cidr); err != nil { + if views.precise4 != nil { + for _, ip := range views.precise4 { + if ip == nil { + continue + } + unmatched = append(unmatched, ip) + } + } + if views.buckets6 != nil { + for _, b := range views.buckets6 { + if b == nil { + continue + } + unmatched = append(unmatched, b.ips...) + } + } + if views.precise6 != nil { + for _, ip := range views.precise6 { + if ip == nil { + continue + } + unmatched = append(unmatched, ip) + } + } + + return +} + +type ipBucketViews struct { + buckets4, buckets6 map[[9]byte]*ipBucket + precise4, precise6 map[netip.Addr]net.IP +} + +func (v *ipBucketViews) ensureForMatcher(m *HeuristicGeoIPMatcher, ips []net.IP) { + needHeur4 := m.ipset.max4 <= 24 && v.buckets4 == nil + needHeur6 := m.ipset.max6 <= 64 && v.buckets6 == nil + needPrec4 := m.ipset.max4 > 24 && v.precise4 == nil + needPrec6 := m.ipset.max6 > 64 && v.precise6 == nil + + if !needHeur4 && !needHeur6 && !needPrec4 && !needPrec6 { + return + } + + if needHeur4 { + v.buckets4 = make(map[[9]byte]*ipBucket, len(ips)) + } + if needHeur6 { + v.buckets6 = make(map[[9]byte]*ipBucket, len(ips)) + } + if needPrec4 { + v.precise4 = make(map[netip.Addr]net.IP, len(ips)) + } + if needPrec6 { + v.precise6 = make(map[netip.Addr]net.IP, len(ips)) + } + + for _, ip := range ips { + key, ok := prefixKeyFromIP(ip) + if !ok { + continue // illegal ip, ignore + } + + switch key[0] { + case 4: + var ipx netip.Addr + if needHeur4 { + b, exists := v.buckets4[key] + if !exists { + // build bucket + ipx, ok = netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + b = &ipBucket{ + rep: ipx, + ips: make([]net.IP, 0, 4), // for dns answer + } + v.buckets4[key] = b + } + b.ips = append(b.ips, ip) + } + if needPrec4 { + if !ipx.IsValid() { + ipx, ok = netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + } + v.precise4[ipx] = ip + } + case 6: + var ipx netip.Addr + if needHeur6 { + b, exists := v.buckets6[key] + if !exists { + // build bucket + ipx, ok = netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + b = &ipBucket{ + rep: ipx, + ips: make([]net.IP, 0, 4), // for dns answer + } + v.buckets6[key] = b + } + b.ips = append(b.ips, ip) + } + if needPrec6 { + if !ipx.IsValid() { + ipx, ok = netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + } + v.precise6[ipx] = ip + } + } + } +} + +// ToggleReverse implements GeoIPMatcher. +func (mm *HeuristicMultiGeoIPMatcher) ToggleReverse() { + for _, m := range mm.matchers { + m.ToggleReverse() + } +} + +// SetReverse implements GeoIPMatcher. +func (mm *HeuristicMultiGeoIPMatcher) SetReverse(reverse bool) { + for _, m := range mm.matchers { + m.SetReverse(reverse) + } +} + +type GeoIPSetFactory struct { + sync.Mutex + shared map[string]*GeoIPSet // TODO: cleanup +} + +var ipsetFactory = GeoIPSetFactory{shared: make(map[string]*GeoIPSet)} + +func (f *GeoIPSetFactory) GetOrCreate(key string, cidrGroups [][]*CIDR) (*GeoIPSet, error) { + f.Lock() + defer f.Unlock() + + if ipset := f.shared[key]; ipset != nil { + return ipset, nil + } + + ipset, err := f.Create(cidrGroups...) + if err == nil { + f.shared[key] = ipset + } + return ipset, err +} + +func (f *GeoIPSetFactory) Create(cidrGroups ...[]*CIDR) (*GeoIPSet, error) { + var ipv4Builder, ipv6Builder netipx.IPSetBuilder + + for _, cidrGroup := range cidrGroups { + for _, cidrEntry := range cidrGroup { + ipBytes := cidrEntry.GetIp() + prefixLen := int(cidrEntry.GetPrefix()) + + addr, ok := netip.AddrFromSlice(ipBytes) + if !ok { + errors.LogError(context.Background(), "ignore invalid IP byte slice: ", ipBytes) + continue + } + + prefix := netip.PrefixFrom(addr, prefixLen) + if !prefix.IsValid() { + errors.LogError(context.Background(), "ignore created invalid prefix from addr ", addr, " and length ", prefixLen) + continue + } + + if addr.Is4() { + ipv4Builder.AddPrefix(prefix) + } else if addr.Is6() { + ipv6Builder.AddPrefix(prefix) + } + } + } + + ipv4, err := ipv4Builder.IPSet() + if err != nil { + return nil, errors.New("failed to build IPv4 set").Base(err) + } + ipv6, err := ipv6Builder.IPSet() + if err != nil { + return nil, errors.New("failed to build IPv6 set").Base(err) + } + + var max4, max6 int + + for _, p := range ipv4.Prefixes() { + if b := p.Bits(); b > max4 { + max4 = b + } + } + for _, p := range ipv6.Prefixes() { + if b := p.Bits(); b > max6 { + max6 = b + } + } + + if max4 == 0 { + max4 = 0xff + } + if max6 == 0 { + max6 = 0xff + } + + return &GeoIPSet{ipv4: ipv4, ipv6: ipv6, max4: uint8(max4), max6: uint8(max6)}, nil +} + +func BuildOptimizedGeoIPMatcher(geoips ...*GeoIP) (GeoIPMatcher, error) { + n := len(geoips) + if n == 0 { + return nil, errors.New("no geoip configs provided") + } + + var subs []*HeuristicGeoIPMatcher + pos := make([]*GeoIP, 0, n) + neg := make([]*GeoIP, 0, n/2) + + for _, geoip := range geoips { + if geoip == nil { + return nil, errors.New("geoip entry is nil") + } + if geoip.CountryCode == "" { + ipset, err := ipsetFactory.Create(geoip.Cidr) + if err != nil { + return nil, err + } + subs = append(subs, &HeuristicGeoIPMatcher{ipset: ipset, reverse: geoip.ReverseMatch}) + continue + } + if !geoip.ReverseMatch { + pos = append(pos, geoip) + } else { + neg = append(neg, geoip) + } + } + + buildIPSet := func(mergeables []*GeoIP) (*GeoIPSet, error) { + n := len(mergeables) + if n == 0 { + return nil, nil + } + + sort.Slice(mergeables, func(i, j int) bool { + gi, gj := mergeables[i], mergeables[j] + return gi.CountryCode < gj.CountryCode + }) + + var sb strings.Builder + sb.Grow(n * 3) // xx, + cidrGroups := make([][]*CIDR, 0, n) + var last *GeoIP + for i, geoip := range mergeables { + if i == 0 || (geoip.CountryCode != last.CountryCode) { + last = geoip + sb.WriteString(geoip.CountryCode) + sb.WriteString(",") + cidrGroups = append(cidrGroups, geoip.Cidr) + } + } + + return ipsetFactory.GetOrCreate(sb.String(), cidrGroups) + } + + ipset, err := buildIPSet(pos) + if err != nil { return nil, err } - if len(geoip.CountryCode) > 0 { - c.matchers = append(c.matchers, m) + if ipset != nil { + subs = append(subs, &HeuristicGeoIPMatcher{ipset: ipset, reverse: false}) } - return m, nil -} -var GlobalGeoIPContainer GeoIPMatcherContainer + ipset, err = buildIPSet(neg) + if err != nil { + return nil, err + } + if ipset != nil { + subs = append(subs, &HeuristicGeoIPMatcher{ipset: ipset, reverse: true}) + } -func MatchIPs(matchers []*GeoIPMatcher, ips []net.IP, reverse bool) []net.IP { - if len(matchers) == 0 { - panic("GeoIP matchers should not be empty to avoid ambiguity") + switch len(subs) { + case 0: + return nil, errors.New("no valid geoip matcher") + case 1: + return subs[0], nil + default: + return &HeuristicMultiGeoIPMatcher{matchers: subs}, nil } - newIPs := make([]net.IP, 0, len(ips)) - var isFound bool - for _, ip := range ips { - isFound = false - for _, matcher := range matchers { - if matcher.Match(ip) { - isFound = true - break - } - } - if isFound && !reverse { - newIPs = append(newIPs, ip) - continue - } - if !isFound && reverse { - newIPs = append(newIPs, ip) - continue - } - } - return newIPs } diff --git a/app/router/condition_geoip_test.go b/app/router/condition_geoip_test.go index 07f40b83..b712db9e 100644 --- a/app/router/condition_geoip_test.go +++ b/app/router/condition_geoip_test.go @@ -35,33 +35,6 @@ func getAssetPath(file string) (string, error) { return path, nil } -func TestGeoIPMatcherContainer(t *testing.T) { - container := &router.GeoIPMatcherContainer{} - - m1, err := container.Add(&router.GeoIP{ - CountryCode: "CN", - }) - common.Must(err) - - m2, err := container.Add(&router.GeoIP{ - CountryCode: "US", - }) - common.Must(err) - - m3, err := container.Add(&router.GeoIP{ - CountryCode: "CN", - }) - common.Must(err) - - if m1 != m3 { - t.Error("expect same matcher for same geoip, but not") - } - - if m1 == m2 { - t.Error("expect different matcher for different geoip, but actually same") - } -} - func TestGeoIPMatcher(t *testing.T) { cidrList := []*router.CIDR{ {Ip: []byte{0, 0, 0, 0}, Prefix: 8}, @@ -80,8 +53,10 @@ func TestGeoIPMatcher(t *testing.T) { {Ip: []byte{91, 108, 4, 0}, Prefix: 16}, } - matcher := &router.GeoIPMatcher{} - common.Must(matcher.Init(cidrList)) + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: cidrList, + }) + common.Must(err) testCases := []struct { Input string @@ -140,8 +115,10 @@ func TestGeoIPMatcherRegression(t *testing.T) { {Ip: []byte{98, 108, 20, 0}, Prefix: 23}, } - matcher := &router.GeoIPMatcher{} - common.Must(matcher.Init(cidrList)) + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: cidrList, + }) + common.Must(err) testCases := []struct { Input string @@ -171,9 +148,11 @@ func TestGeoIPReverseMatcher(t *testing.T) { {Ip: []byte{8, 8, 8, 8}, Prefix: 32}, {Ip: []byte{91, 108, 4, 0}, Prefix: 16}, } - matcher := &router.GeoIPMatcher{} - matcher.SetReverseMatch(true) // Reverse match - common.Must(matcher.Init(cidrList)) + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: cidrList, + }) + common.Must(err) + matcher.SetReverse(true) // Reverse match testCases := []struct { Input string @@ -206,8 +185,10 @@ func TestGeoIPMatcher4CN(t *testing.T) { ips, err := loadGeoIP("CN") common.Must(err) - matcher := &router.GeoIPMatcher{} - common.Must(matcher.Init(ips)) + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: ips, + }) + common.Must(err) if matcher.Match([]byte{8, 8, 8, 8}) { t.Error("expect CN geoip doesn't contain 8.8.8.8, but actually does") @@ -218,8 +199,10 @@ func TestGeoIPMatcher6US(t *testing.T) { ips, err := loadGeoIP("US") common.Must(err) - matcher := &router.GeoIPMatcher{} - common.Must(matcher.Init(ips)) + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: ips, + }) + common.Must(err) if !matcher.Match(net.ParseAddress("2001:4860:4860::8888").IP()) { t.Error("expect US geoip contain 2001:4860:4860::8888, but actually not") @@ -254,8 +237,10 @@ func BenchmarkGeoIPMatcher4CN(b *testing.B) { ips, err := loadGeoIP("CN") common.Must(err) - matcher := &router.GeoIPMatcher{} - common.Must(matcher.Init(ips)) + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: ips, + }) + common.Must(err) b.ResetTimer() @@ -268,8 +253,10 @@ func BenchmarkGeoIPMatcher6US(b *testing.B) { ips, err := loadGeoIP("US") common.Must(err) - matcher := &router.GeoIPMatcher{} - common.Must(matcher.Init(ips)) + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: ips, + }) + common.Must(err) b.ResetTimer() diff --git a/app/router/condition_test.go b/app/router/condition_test.go index 7e90351a..1272aef6 100644 --- a/app/router/condition_test.go +++ b/app/router/condition_test.go @@ -447,7 +447,7 @@ func BenchmarkMultiGeoIPMatcher(b *testing.B) { }) } - matcher, err := NewMultiGeoIPMatcher(geoips, "target") + matcher, err := NewIPMatcher(geoips, MatcherAsType_Target) common.Must(err) ctx := withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)}) diff --git a/app/router/config.go b/app/router/config.go index b7338e35..e9f0e02c 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -46,7 +46,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } if rr.VlessRouteList != nil { - conds.Add(NewPortMatcher(rr.VlessRouteList, "vlessRoute")) + conds.Add(NewPortMatcher(rr.VlessRouteList, MatcherAsType_VlessRoute)) } if len(rr.InboundTag) > 0 { @@ -54,15 +54,15 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } if rr.PortList != nil { - conds.Add(NewPortMatcher(rr.PortList, "target")) + conds.Add(NewPortMatcher(rr.PortList, MatcherAsType_Target)) } if rr.SourcePortList != nil { - conds.Add(NewPortMatcher(rr.SourcePortList, "source")) + conds.Add(NewPortMatcher(rr.SourcePortList, MatcherAsType_Source)) } if rr.LocalPortList != nil { - conds.Add(NewPortMatcher(rr.LocalPortList, "local")) + conds.Add(NewPortMatcher(rr.LocalPortList, MatcherAsType_Local)) } if len(rr.Networks) > 0 { @@ -70,7 +70,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } if len(rr.Geoip) > 0 { - cond, err := NewMultiGeoIPMatcher(rr.Geoip, "target") + cond, err := NewIPMatcher(rr.Geoip, MatcherAsType_Target) if err != nil { return nil, err } @@ -78,7 +78,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } if len(rr.SourceGeoip) > 0 { - cond, err := NewMultiGeoIPMatcher(rr.SourceGeoip, "source") + cond, err := NewIPMatcher(rr.SourceGeoip, MatcherAsType_Source) if err != nil { return nil, err } @@ -86,7 +86,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } if len(rr.LocalGeoip) > 0 { - cond, err := NewMultiGeoIPMatcher(rr.LocalGeoip, "local") + cond, err := NewIPMatcher(rr.LocalGeoip, MatcherAsType_Local) if err != nil { return nil, err } From 2185a730d2ead8265b26263e1d6c6ca187c0c28e Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:30:16 +0800 Subject: [PATCH 036/136] perf(router): adjust the order of rules to optimize performance (#5267) --- app/router/condition.go | 14 ----------- app/router/config.go | 56 ++++++++++++++++++++--------------------- 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/app/router/condition.go b/app/router/condition.go index c8cf4e8d..083cbfaf 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -47,20 +47,6 @@ var matcherTypeMap = map[Domain_Type]strmatcher.Type{ Domain_Full: strmatcher.Full, } -func domainToMatcher(domain *Domain) (strmatcher.Matcher, error) { - matcherType, f := matcherTypeMap[domain.Type] - if !f { - return nil, errors.New("unsupported domain type", domain.Type) - } - - matcher, err := matcherType.New(domain.Value) - if err != nil { - return nil, errors.New("failed to create domain matcher").Base(err) - } - - return matcher, nil -} - type DomainMatcher struct { matchers strmatcher.IndexMatcher } diff --git a/app/router/config.go b/app/router/config.go index e9f0e02c..9f6a4844 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -32,27 +32,18 @@ func (r *Rule) Apply(ctx routing.Context) bool { func (rr *RoutingRule) BuildCondition() (Condition, error) { conds := NewConditionChan() - if len(rr.Domain) > 0 { - matcher, err := NewMphMatcherGroup(rr.Domain) - if err != nil { - return nil, errors.New("failed to build domain condition with MphDomainMatcher").Base(err) - } - errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)") - conds.Add(matcher) - } - - if len(rr.UserEmail) > 0 { - conds.Add(NewUserMatcher(rr.UserEmail)) - } - - if rr.VlessRouteList != nil { - conds.Add(NewPortMatcher(rr.VlessRouteList, MatcherAsType_VlessRoute)) - } - if len(rr.InboundTag) > 0 { conds.Add(NewInboundTagMatcher(rr.InboundTag)) } + if len(rr.Networks) > 0 { + conds.Add(NewNetworkMatcher(rr.Networks)) + } + + if len(rr.Protocol) > 0 { + conds.Add(NewProtocolMatcher(rr.Protocol)) + } + if rr.PortList != nil { conds.Add(NewPortMatcher(rr.PortList, MatcherAsType_Target)) } @@ -65,8 +56,20 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { conds.Add(NewPortMatcher(rr.LocalPortList, MatcherAsType_Local)) } - if len(rr.Networks) > 0 { - conds.Add(NewNetworkMatcher(rr.Networks)) + if rr.VlessRouteList != nil { + conds.Add(NewPortMatcher(rr.VlessRouteList, MatcherAsType_VlessRoute)) + } + + if len(rr.UserEmail) > 0 { + conds.Add(NewUserMatcher(rr.UserEmail)) + } + + if len(rr.Attributes) > 0 { + configuredKeys := make(map[string]*regexp.Regexp) + for key, value := range rr.Attributes { + configuredKeys[strings.ToLower(key)] = regexp.MustCompile(value) + } + conds.Add(&AttributeMatcher{configuredKeys}) } if len(rr.Geoip) > 0 { @@ -94,16 +97,13 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { errors.LogWarning(context.Background(), "Due to some limitations, in UDP connections, localIP is always equal to listen interface IP, so \"localIP\" rule condition does not work properly on UDP inbound connections that listen on all interfaces") } - if len(rr.Protocol) > 0 { - conds.Add(NewProtocolMatcher(rr.Protocol)) - } - - if len(rr.Attributes) > 0 { - configuredKeys := make(map[string]*regexp.Regexp) - for key, value := range rr.Attributes { - configuredKeys[strings.ToLower(key)] = regexp.MustCompile(value) + if len(rr.Domain) > 0 { + matcher, err := NewMphMatcherGroup(rr.Domain) + if err != nil { + return nil, errors.New("failed to build domain condition with MphDomainMatcher").Base(err) } - conds.Add(&AttributeMatcher{configuredKeys}) + errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)") + conds.Add(matcher) } if conds.Len() == 0 { From 4956e6582465544e38c60e526a5bae3ab9b663ec Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:35:45 +0800 Subject: [PATCH 037/136] perf(dns): cache network capability check (#5244) --- app/dns/dns.go | 78 ++++++++++++++++++++++++++++++++++--------- app/dns/nameserver.go | 2 +- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/app/dns/dns.go b/app/dns/dns.go index 37183aba..a59fc398 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -5,9 +5,12 @@ import ( "context" go_errors "errors" "fmt" + "os" + "runtime" "sort" "strings" "sync" + "time" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" @@ -191,7 +194,7 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, er } if s.checkSystem { - supportIPv4, supportIPv6 := checkSystemNetwork() + supportIPv4, supportIPv6 := checkRoutes() option.IPv4Enable = option.IPv4Enable && supportIPv4 option.IPv6Enable = option.IPv6Enable && supportIPv6 } else { @@ -328,21 +331,66 @@ func init() { })) } -func checkSystemNetwork() (supportIPv4 bool, supportIPv6 bool) { - conn4, err4 := net.Dial("udp4", "192.33.4.12:53") - if err4 != nil { - supportIPv4 = false - } else { - supportIPv4 = true - conn4.Close() +func probeRoutes() (ipv4 bool, ipv6 bool) { + if conn, err := net.Dial("udp4", "192.33.4.12:53"); err == nil { + ipv4 = true + conn.Close() } - - conn6, err6 := net.Dial("udp6", "[2001:500:2::c]:53") - if err6 != nil { - supportIPv6 = false - } else { - supportIPv6 = true - conn6.Close() + if conn, err := net.Dial("udp6", "[2001:500:2::c]:53"); err == nil { + ipv6 = true + conn.Close() } return } + +var routeCache struct { + sync.Once + sync.RWMutex + expire time.Time + ipv4, ipv6 bool +} + +func checkRoutes() (bool, bool) { + if !isGUIPlatform { + routeCache.Once.Do(func() { + routeCache.ipv4, routeCache.ipv6 = probeRoutes() + }) + return routeCache.ipv4, routeCache.ipv6 + } + + routeCache.RWMutex.RLock() + now := time.Now() + if routeCache.expire.After(now) { + routeCache.RWMutex.RUnlock() + return routeCache.ipv4, routeCache.ipv6 + } + routeCache.RWMutex.RUnlock() + + routeCache.RWMutex.Lock() + defer routeCache.RWMutex.Unlock() + + now = time.Now() + if routeCache.expire.After(now) { // double-check + return routeCache.ipv4, routeCache.ipv6 + } + routeCache.ipv4, routeCache.ipv6 = probeRoutes() // ~2ms + routeCache.expire = now.Add(100 * time.Millisecond) // ttl + return routeCache.ipv4, routeCache.ipv6 +} + +var isGUIPlatform = detectGUIPlatform() + +func detectGUIPlatform() bool { + switch runtime.GOOS { + case "android", "ios", "windows", "darwin": + return true + case "linux", "freebsd", "openbsd": + if t := os.Getenv("XDG_SESSION_TYPE"); t == "wayland" || t == "x11" { + return true + } + if os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != "" { + return true + } + } + return false +} diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go index 3f025d74..4d288a25 100644 --- a/app/dns/nameserver.go +++ b/app/dns/nameserver.go @@ -216,7 +216,7 @@ func (c *Client) IsFinalQuery() bool { // QueryIP sends DNS query to the name server with the client's IP. func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) { if c.checkSystem { - supportIPv4, supportIPv6 := checkSystemNetwork() + supportIPv4, supportIPv6 := checkRoutes() option.IPv4Enable = option.IPv4Enable && supportIPv4 option.IPv6Enable = option.IPv6Enable && supportIPv6 } else { From cd51f57535194c6c7e112f322566399749578486 Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:38:06 +0800 Subject: [PATCH 038/136] feat(dns): add optimistic caching (#5237) --- app/dns/cache_controller.go | 40 ++++--- app/dns/config.pb.go | 180 ++++++++++++++++++++------------ app/dns/config.proto | 4 + app/dns/dns.go | 7 +- app/dns/dnscommon.go | 15 ++- app/dns/nameserver.go | 22 ++-- app/dns/nameserver_cached.go | 38 +++++-- app/dns/nameserver_doh.go | 20 ++-- app/dns/nameserver_doh_test.go | 8 +- app/dns/nameserver_quic.go | 40 +++++-- app/dns/nameserver_quic_test.go | 6 +- app/dns/nameserver_tcp.go | 48 ++++++--- app/dns/nameserver_tcp_test.go | 8 +- app/dns/nameserver_udp.go | 4 +- common/log/dns.go | 5 +- infra/conf/dns.go | 64 +++++++----- infra/conf/dns_test.go | 12 ++- 17 files changed, 338 insertions(+), 183 deletions(-) diff --git a/app/dns/cache_controller.go b/app/dns/cache_controller.go index 85818039..24ae3cb2 100644 --- a/app/dns/cache_controller.go +++ b/app/dns/cache_controller.go @@ -25,23 +25,29 @@ const ( ) type CacheController struct { + name string + disableCache bool + serveStale bool + serveExpiredTTL int32 + + ips map[string]*record + dirtyips map[string]*record + sync.RWMutex - ips map[string]*record - dirtyips map[string]*record pub *pubsub.Service cacheCleanup *task.Periodic - name string - disableCache bool highWatermark int requestGroup singleflight.Group } -func NewCacheController(name string, disableCache bool) *CacheController { +func NewCacheController(name string, disableCache bool, serveStale bool, serveExpiredTTL uint32) *CacheController { c := &CacheController{ - name: name, - disableCache: disableCache, - ips: make(map[string]*record), - pub: pubsub.NewService(), + name: name, + disableCache: disableCache, + serveStale: serveStale, + serveExpiredTTL: -int32(serveExpiredTTL), + ips: make(map[string]*record), + pub: pubsub.NewService(), } c.cacheCleanup = &task.Periodic{ @@ -78,6 +84,10 @@ func (c *CacheController) collectExpiredKeys() ([]string, error) { } now := time.Now() + if c.serveStale && c.serveExpiredTTL != 0 { + now = now.Add(time.Duration(c.serveExpiredTTL) * time.Second) + } + expiredKeys := make([]string, 0, len(c.ips)/4) // pre-allocate for domain, rec := range c.ips { @@ -105,6 +115,10 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) { } now := time.Now() + if c.serveStale && c.serveExpiredTTL != 0 { + now = now.Add(time.Duration(c.serveExpiredTTL) * time.Second) + } + for _, domain := range expiredKeys { rec := c.ips[domain] if rec == nil { @@ -280,15 +294,17 @@ func (c *CacheController) updateRecord(req *dnsRequest, rep *IPRecord) { c.Unlock() if pubRecord != nil { - _, _ /*ttl*/, err := pubRecord.getIPs() - if /*ttl >= 0 &&*/ !go_errors.Is(err, errRecordNotFound) { + _, ttl, err := pubRecord.getIPs() + if ttl > 0 && !go_errors.Is(err, errRecordNotFound) { c.pub.Publish(req.domain+pubSuffix, pubRecord) } } errors.LogInfo(context.Background(), c.name, " got answer: ", req.domain, " ", req.reqType, " -> ", rep.IP, ", rtt: ", rtt, ", lock: ", lockWait) - common.Must(c.cacheCleanup.Start()) + if !c.serveStale || c.serveExpiredTTL != 0 { + common.Must(c.cacheCleanup.Start()) + } } func (c *CacheController) findRecords(domain string) *record { diff --git a/app/dns/config.pb.go b/app/dns/config.pb.go index d54142ce..9cc63986 100644 --- a/app/dns/config.pb.go +++ b/app/dns/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.35.1 -// protoc v5.28.2 +// protoc v5.29.4 // source: app/dns/config.proto package dns @@ -142,6 +142,8 @@ type NameServer struct { Tag string `protobuf:"bytes,9,opt,name=tag,proto3" json:"tag,omitempty"` TimeoutMs uint64 `protobuf:"varint,10,opt,name=timeoutMs,proto3" json:"timeoutMs,omitempty"` DisableCache bool `protobuf:"varint,11,opt,name=disableCache,proto3" json:"disableCache,omitempty"` + ServeStale bool `protobuf:"varint,15,opt,name=serveStale,proto3" json:"serveStale,omitempty"` + ServeExpiredTTL *uint32 `protobuf:"varint,16,opt,name=serveExpiredTTL,proto3,oneof" json:"serveExpiredTTL,omitempty"` FinalQuery bool `protobuf:"varint,12,opt,name=finalQuery,proto3" json:"finalQuery,omitempty"` UnexpectedGeoip []*router.GeoIP `protobuf:"bytes,13,rep,name=unexpected_geoip,json=unexpectedGeoip,proto3" json:"unexpected_geoip,omitempty"` ActUnprior bool `protobuf:"varint,14,opt,name=actUnprior,proto3" json:"actUnprior,omitempty"` @@ -254,6 +256,20 @@ func (x *NameServer) GetDisableCache() bool { return false } +func (x *NameServer) GetServeStale() bool { + if x != nil { + return x.ServeStale + } + return false +} + +func (x *NameServer) GetServeExpiredTTL() uint32 { + if x != nil && x.ServeExpiredTTL != nil { + return *x.ServeExpiredTTL + } + return 0 +} + func (x *NameServer) GetFinalQuery() bool { if x != nil { return x.FinalQuery @@ -291,6 +307,8 @@ type Config struct { Tag string `protobuf:"bytes,6,opt,name=tag,proto3" json:"tag,omitempty"` // DisableCache disables DNS cache DisableCache bool `protobuf:"varint,8,opt,name=disableCache,proto3" json:"disableCache,omitempty"` + ServeStale bool `protobuf:"varint,12,opt,name=serveStale,proto3" json:"serveStale,omitempty"` + ServeExpiredTTL uint32 `protobuf:"varint,13,opt,name=serveExpiredTTL,proto3" json:"serveExpiredTTL,omitempty"` QueryStrategy QueryStrategy `protobuf:"varint,9,opt,name=query_strategy,json=queryStrategy,proto3,enum=xray.app.dns.QueryStrategy" json:"query_strategy,omitempty"` DisableFallback bool `protobuf:"varint,10,opt,name=disableFallback,proto3" json:"disableFallback,omitempty"` DisableFallbackIfMatch bool `protobuf:"varint,11,opt,name=disableFallbackIfMatch,proto3" json:"disableFallbackIfMatch,omitempty"` @@ -361,6 +379,20 @@ func (x *Config) GetDisableCache() bool { return false } +func (x *Config) GetServeStale() bool { + if x != nil { + return x.ServeStale + } + return false +} + +func (x *Config) GetServeExpiredTTL() uint32 { + if x != nil { + return x.ServeExpiredTTL + } + return 0 +} + func (x *Config) GetQueryStrategy() QueryStrategy { if x != nil { return x.QueryStrategy @@ -567,7 +599,7 @@ var file_app_dns_config_proto_rawDesc = []byte{ 0x2e, 0x64, 0x6e, 0x73, 0x1a, 0x1c, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74, 0x2f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb6, 0x06, 0x0a, 0x0a, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x99, 0x07, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x33, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x45, 0x6e, @@ -601,72 +633,83 @@ var file_app_dns_config_proto_rawDesc = []byte{ 0x75, 0x74, 0x4d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x6e, - 0x61, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, - 0x69, 0x6e, 0x61, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x41, 0x0a, 0x10, 0x75, 0x6e, 0x65, - 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x18, 0x0d, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x52, 0x0f, 0x75, 0x6e, 0x65, - 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x47, 0x65, 0x6f, 0x69, 0x70, 0x12, 0x1e, 0x0a, 0x0a, - 0x61, 0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0a, 0x61, 0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x1a, 0x5e, 0x0a, 0x0e, - 0x50, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, - 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, - 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x1a, 0x36, 0x0a, 0x0c, - 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x72, 0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, - 0x73, 0x69, 0x7a, 0x65, 0x22, 0x9c, 0x04, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x39, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, - 0x64, 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0a, - 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x69, - 0x63, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, - 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, - 0x0b, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, - 0x74, 0x61, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x22, - 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x18, 0x08, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, - 0x68, 0x65, 0x12, 0x42, 0x0a, 0x0e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x61, - 0x74, 0x65, 0x67, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61, - 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, - 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, - 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, - 0x12, 0x36, 0x0a, 0x16, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, - 0x61, 0x63, 0x6b, 0x49, 0x66, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x16, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, - 0x6b, 0x49, 0x66, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x1a, 0x92, 0x01, 0x0a, 0x0b, 0x48, 0x6f, 0x73, - 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, - 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, - 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, - 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65, - 0x64, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, - 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65, 0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4a, 0x04, 0x08, - 0x07, 0x10, 0x08, 0x2a, 0x45, 0x0a, 0x12, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, - 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c, - 0x6c, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x10, 0x02, 0x12, - 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x03, 0x2a, 0x42, 0x0a, 0x0d, 0x51, 0x75, - 0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x0a, 0x0a, 0x06, 0x55, - 0x53, 0x45, 0x5f, 0x49, 0x50, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, - 0x50, 0x34, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x36, 0x10, - 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x53, 0x59, 0x53, 0x10, 0x03, 0x42, 0x46, - 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, - 0x6e, 0x73, 0x50, 0x01, 0x5a, 0x21, 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, - 0x61, 0x70, 0x70, 0x2f, 0x64, 0x6e, 0x73, 0xaa, 0x02, 0x0c, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, - 0x70, 0x70, 0x2e, 0x44, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, 0x65, 0x12, 0x2d, 0x0a, 0x0f, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x18, 0x10, 0x20, 0x01, + 0x28, 0x0d, 0x48, 0x00, 0x52, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, + 0x65, 0x64, 0x54, 0x54, 0x4c, 0x88, 0x01, 0x01, 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x6e, 0x61, + 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69, + 0x6e, 0x61, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x41, 0x0a, 0x10, 0x75, 0x6e, 0x65, 0x78, + 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x18, 0x0d, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, + 0x75, 0x74, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x52, 0x0f, 0x75, 0x6e, 0x65, 0x78, + 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x47, 0x65, 0x6f, 0x69, 0x70, 0x12, 0x1e, 0x0a, 0x0a, 0x61, + 0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0a, 0x61, 0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x1a, 0x5e, 0x0a, 0x0e, 0x50, + 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x1a, 0x36, 0x0a, 0x0c, 0x4f, + 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, + 0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, + 0x69, 0x7a, 0x65, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, + 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x22, 0xe6, 0x04, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x39, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, + 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x1b, 0x0a, + 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x74, + 0x61, 0x74, 0x69, 0x63, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, + 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, + 0x67, 0x12, 0x22, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x53, 0x74, + 0x61, 0x6c, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x53, 0x74, 0x61, 0x6c, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, + 0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x12, + 0x42, 0x0a, 0x0e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, + 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, + 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, + 0x74, 0x65, 0x67, 0x79, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, + 0x65, 0x67, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, + 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x36, 0x0a, + 0x16, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, + 0x49, 0x66, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x64, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x49, 0x66, + 0x4d, 0x61, 0x74, 0x63, 0x68, 0x1a, 0x92, 0x01, 0x0a, 0x0b, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, + 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, + 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, + 0x02, 0x69, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65, 0x64, 0x5f, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x6f, + 0x78, 0x69, 0x65, 0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, + 0x2a, 0x45, 0x0a, 0x12, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, + 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x00, + 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x01, 0x12, + 0x0b, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, + 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x03, 0x2a, 0x42, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x53, 0x45, 0x5f, + 0x49, 0x50, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x34, 0x10, + 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x36, 0x10, 0x02, 0x12, 0x0b, + 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x53, 0x59, 0x53, 0x10, 0x03, 0x42, 0x46, 0x0a, 0x10, 0x63, + 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x50, + 0x01, 0x5a, 0x21, 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, 0x61, 0x70, 0x70, + 0x2f, 0x64, 0x6e, 0x73, 0xaa, 0x02, 0x0c, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, + 0x44, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -718,6 +761,7 @@ func file_app_dns_config_proto_init() { if File_app_dns_config_proto != nil { return } + file_app_dns_config_proto_msgTypes[0].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/app/dns/config.proto b/app/dns/config.proto index 4317d0d7..a87725a3 100644 --- a/app/dns/config.proto +++ b/app/dns/config.proto @@ -32,6 +32,8 @@ message NameServer { string tag = 9; uint64 timeoutMs = 10; bool disableCache = 11; + bool serveStale = 15; + optional uint32 serveExpiredTTL = 16; bool finalQuery = 12; repeated xray.app.router.GeoIP unexpected_geoip = 13; bool actUnprior = 14; @@ -80,6 +82,8 @@ message Config { // DisableCache disables DNS cache bool disableCache = 8; + bool serveStale = 12; + uint32 serveExpiredTTL = 13; QueryStrategy query_strategy = 9; diff --git a/app/dns/dns.go b/app/dns/dns.go index a59fc398..78e91389 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -121,6 +121,11 @@ func New(ctx context.Context, config *Config) (*DNS, error) { } disableCache := config.DisableCache || ns.DisableCache + serveStale := config.ServeStale || ns.ServeStale + serveExpiredTTL := config.ServeExpiredTTL + if ns.ServeExpiredTTL != nil { + serveExpiredTTL = *ns.ServeExpiredTTL + } var tag = defaultTag if len(ns.Tag) > 0 { @@ -131,7 +136,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) { return nil, errors.New("no QueryStrategy available for ", ns.Address) } - client, err := NewClient(ctx, ns, myClientIP, disableCache, tag, clientIPOption, &matcherInfos, updateDomain) + client, err := NewClient(ctx, ns, myClientIP, disableCache, serveStale, serveExpiredTTL, tag, clientIPOption, &matcherInfos, updateDomain) if err != nil { return nil, errors.New("failed to create client").Base(err) } diff --git a/app/dns/dnscommon.go b/app/dns/dnscommon.go index e4ec8ae8..2092a2fd 100644 --- a/app/dns/dnscommon.go +++ b/app/dns/dnscommon.go @@ -3,6 +3,7 @@ package dns import ( "context" "encoding/binary" + "math" "strings" "time" @@ -13,6 +14,7 @@ import ( "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/core" dns_feature "github.com/xtls/xray-core/features/dns" + "golang.org/x/net/dns/dnsmessage" ) @@ -39,19 +41,14 @@ type IPRecord struct { RawHeader *dnsmessage.Header } -func (r *IPRecord) getIPs() ([]net.IP, uint32, error) { +func (r *IPRecord) getIPs() ([]net.IP, int32, error) { if r == nil { return nil, 0, errRecordNotFound } - untilExpire := time.Until(r.Expire).Seconds() - if untilExpire <= 0 { - return nil, 0, errRecordNotFound - } - ttl := uint32(untilExpire) + 1 - if ttl == 1 { - r.Expire = time.Now().Add(time.Second) // To ensure that two consecutive requests get the same result - } + untilExpire := time.Until(r.Expire).Seconds() + ttl := int32(math.Ceil(untilExpire)) + if r.RCode != dnsmessage.RCodeSuccess { return nil, ttl, dns_feature.RCodeError(r.RCode) } diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go index 4d288a25..3f25833e 100644 --- a/app/dns/nameserver.go +++ b/app/dns/nameserver.go @@ -41,7 +41,7 @@ type Client struct { } // NewServer creates a name server object according to the network destination url. -func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dispatcher, disableCache bool, clientIP net.IP) (Server, error) { +func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dispatcher, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) (Server, error) { if address := dest.Address; address.Family().IsDomain() { u, err := url.Parse(address.Domain()) if err != nil { @@ -51,19 +51,19 @@ func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dis case strings.EqualFold(u.String(), "localhost"): return NewLocalNameServer(), nil case strings.EqualFold(u.Scheme, "https"): // DNS-over-HTTPS Remote mode - return NewDoHNameServer(u, dispatcher, false, disableCache, clientIP), nil + return NewDoHNameServer(u, dispatcher, false, disableCache, serveStale, serveExpiredTTL, clientIP), nil case strings.EqualFold(u.Scheme, "h2c"): // DNS-over-HTTPS h2c Remote mode - return NewDoHNameServer(u, dispatcher, true, disableCache, clientIP), nil + return NewDoHNameServer(u, dispatcher, true, disableCache, serveStale, serveExpiredTTL, clientIP), nil case strings.EqualFold(u.Scheme, "https+local"): // DNS-over-HTTPS Local mode - return NewDoHNameServer(u, nil, false, disableCache, clientIP), nil + return NewDoHNameServer(u, nil, false, disableCache, serveStale, serveExpiredTTL, clientIP), nil case strings.EqualFold(u.Scheme, "h2c+local"): // DNS-over-HTTPS h2c Local mode - return NewDoHNameServer(u, nil, true, disableCache, clientIP), nil + return NewDoHNameServer(u, nil, true, disableCache, serveStale, serveExpiredTTL, clientIP), nil case strings.EqualFold(u.Scheme, "quic+local"): // DNS-over-QUIC Local mode - return NewQUICNameServer(u, disableCache, clientIP) + return NewQUICNameServer(u, disableCache, serveStale, serveExpiredTTL, clientIP) case strings.EqualFold(u.Scheme, "tcp"): // DNS-over-TCP Remote mode - return NewTCPNameServer(u, dispatcher, disableCache, clientIP) + return NewTCPNameServer(u, dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP) case strings.EqualFold(u.Scheme, "tcp+local"): // DNS-over-TCP Local mode - return NewTCPLocalNameServer(u, disableCache, clientIP) + return NewTCPLocalNameServer(u, disableCache, serveStale, serveExpiredTTL, clientIP) case strings.EqualFold(u.String(), "fakedns"): var fd dns.FakeDNSEngine err = core.RequireFeatures(ctx, func(fdns dns.FakeDNSEngine) { @@ -79,7 +79,7 @@ func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dis dest.Network = net.Network_UDP } if dest.Network == net.Network_UDP { // UDP classic DNS mode - return NewClassicNameServer(dest, dispatcher, disableCache, clientIP), nil + return NewClassicNameServer(dest, dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP), nil } return nil, errors.New("No available name server could be created from ", dest).AtWarning() } @@ -89,7 +89,7 @@ func NewClient( ctx context.Context, ns *NameServer, clientIP net.IP, - disableCache bool, + disableCache bool, serveStale bool, serveExpiredTTL uint32, tag string, ipOption dns.IPOption, matcherInfos *[]*DomainMatcherInfo, @@ -99,7 +99,7 @@ func NewClient( err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error { // Create a new server for each client for now - server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, clientIP) + server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP) if err != nil { return errors.New("failed to create nameserver").Base(err).AtWarning() } diff --git a/app/dns/nameserver_cached.go b/app/dns/nameserver_cached.go index d1872b0c..e27dda49 100644 --- a/app/dns/nameserver_cached.go +++ b/app/dns/nameserver_cached.go @@ -27,9 +27,17 @@ func queryIP(ctx context.Context, s CachedNameserver, domain string, option dns. if rec := cache.findRecords(fqdn); rec != nil { ips, ttl, err := merge(option, rec.A, rec.AAAA) if !go_errors.Is(err, errRecordNotFound) { - // errors.LogDebugInner(ctx, err, cache.name, " cache HIT ", fqdn, " -> ", ips) - log.Record(&log.DNSLog{Server: cache.name, Domain: fqdn, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) - return ips, ttl, err + if ttl > 0 { + // errors.LogDebugInner(ctx, err, cache.name, " cache HIT ", fqdn, " -> ", ips) + log.Record(&log.DNSLog{Server: cache.name, Domain: fqdn, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) + return ips, uint32(ttl), err + } + if cache.serveStale && (cache.serveExpiredTTL == 0 || cache.serveExpiredTTL < ttl) { + // errors.LogDebugInner(ctx, err, cache.name, " cache OPTIMISTE ", fqdn, " -> ", ips) + log.Record(&log.DNSLog{Server: cache.name, Domain: fqdn, Result: ips, Status: log.DNSCacheOptimiste, Elapsed: 0, Error: err}) + go pull(ctx, s, fqdn, option) + return ips, 1, err + } } } } else { @@ -39,8 +47,15 @@ func queryIP(ctx context.Context, s CachedNameserver, domain string, option dns. return fetch(ctx, s, fqdn, option) } +func pull(ctx context.Context, s CachedNameserver, fqdn string, option dns.IPOption) { + nctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 8*time.Second) + defer cancel() + + fetch(nctx, s, fqdn, option) +} + func fetch(ctx context.Context, s CachedNameserver, fqdn string, option dns.IPOption) ([]net.IP, uint32, error) { - key := fqdn + "f" + key := fqdn switch { case option.IPv4Enable && option.IPv6Enable: key = key + "46" @@ -99,13 +114,22 @@ func doFetch(ctx context.Context, s CachedNameserver, fqdn string, option dns.IP } ips, ttl, err := merge(option, rec4, rec6, errs...) + var rTTL uint32 + if ttl > 0 { + rTTL = uint32(ttl) + } else if ttl == 0 && go_errors.Is(err, errRecordNotFound) { + rTTL = 0 + } else { // edge case: where a fast rep's ttl expires during the rtt of a slower, parallel query + rTTL = 1 + } + log.Record(&log.DNSLog{Server: s.getCacheController().name, Domain: fqdn, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err}) - return result{ips, ttl, err} + return result{ips, rTTL, err} } -func merge(option dns.IPOption, rec4 *IPRecord, rec6 *IPRecord, errs ...error) ([]net.IP, uint32, error) { +func merge(option dns.IPOption, rec4 *IPRecord, rec6 *IPRecord, errs ...error) ([]net.IP, int32, error) { var allIPs []net.IP - var rTTL uint32 = dns.DefaultTTL + var rTTL int32 = dns.DefaultTTL mergeReq := option.IPv4Enable && option.IPv6Enable diff --git a/app/dns/nameserver_doh.go b/app/dns/nameserver_doh.go index ebdea6a2..88198aed 100644 --- a/app/dns/nameserver_doh.go +++ b/app/dns/nameserver_doh.go @@ -37,7 +37,7 @@ type DoHNameServer struct { } // NewDoHNameServer creates DOH/DOHL client object for remote/local resolving. -func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, h2c bool, disableCache bool, clientIP net.IP) *DoHNameServer { +func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, h2c bool, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) *DoHNameServer { url.Scheme = "https" mode := "DOH" if dispatcher == nil { @@ -45,7 +45,7 @@ func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, h2c bool, dis } errors.LogInfo(context.Background(), "DNS: created ", mode, " client for ", url.String(), ", with h2c ", h2c) s := &DoHNameServer{ - cacheController: NewCacheController(mode+"//"+url.Host, disableCache), + cacheController: NewCacheController(mode+"//"+url.Host, disableCache, serveStale, serveExpiredTTL), dohURL: url.String(), clientIP: clientIP, } @@ -131,7 +131,9 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er if s.Name()+"." == "DOH//"+fqdn { errors.LogError(ctx, s.Name(), " tries to resolve itself! Use IP or set \"hosts\" instead.") - noResponseErrCh <- errors.New("tries to resolve itself!", s.Name()) + if noResponseErrCh != nil { + noResponseErrCh <- errors.New("tries to resolve itself!", s.Name()) + } return } @@ -172,19 +174,25 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er b, err := dns.PackMessage(r.msg) if err != nil { errors.LogErrorInner(ctx, err, "failed to pack dns query for ", fqdn) - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } resp, err := s.dohHTTPSContext(dnsCtx, b.Bytes()) if err != nil { errors.LogErrorInner(ctx, err, "failed to retrieve response for ", fqdn) - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } rec, err := parseResponse(resp) if err != nil { errors.LogErrorInner(ctx, err, "failed to handle DOH response for ", fqdn) - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } s.cacheController.updateRecord(r, rec) diff --git a/app/dns/nameserver_doh_test.go b/app/dns/nameserver_doh_test.go index 96412c22..52387661 100644 --- a/app/dns/nameserver_doh_test.go +++ b/app/dns/nameserver_doh_test.go @@ -17,7 +17,7 @@ func TestDOHNameServer(t *testing.T) { url, err := url.Parse("https+local://1.1.1.1/dns-query") common.Must(err) - s := NewDoHNameServer(url, nil, false, false, net.IP(nil)) + s := NewDoHNameServer(url, nil, false, false, false, 0, net.IP(nil)) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ IPv4Enable: true, @@ -34,7 +34,7 @@ func TestDOHNameServerWithCache(t *testing.T) { url, err := url.Parse("https+local://1.1.1.1/dns-query") common.Must(err) - s := NewDoHNameServer(url, nil, false, false, net.IP(nil)) + s := NewDoHNameServer(url, nil, false, false, false, 0, net.IP(nil)) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ IPv4Enable: true, @@ -62,7 +62,7 @@ func TestDOHNameServerWithIPv4Override(t *testing.T) { url, err := url.Parse("https+local://1.1.1.1/dns-query") common.Must(err) - s := NewDoHNameServer(url, nil, false, false, net.IP(nil)) + s := NewDoHNameServer(url, nil, false, false, false, 0, net.IP(nil)) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ IPv4Enable: true, @@ -85,7 +85,7 @@ func TestDOHNameServerWithIPv6Override(t *testing.T) { url, err := url.Parse("https+local://1.1.1.1/dns-query") common.Must(err) - s := NewDoHNameServer(url, nil, false, false, net.IP(nil)) + s := NewDoHNameServer(url, nil, false, false, false, 0, net.IP(nil)) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ IPv4Enable: false, diff --git a/app/dns/nameserver_quic.go b/app/dns/nameserver_quic.go index c294ecda..3d271d74 100644 --- a/app/dns/nameserver_quic.go +++ b/app/dns/nameserver_quic.go @@ -36,7 +36,7 @@ type QUICNameServer struct { } // NewQUICNameServer creates DNS-over-QUIC client object for local resolving -func NewQUICNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*QUICNameServer, error) { +func NewQUICNameServer(url *url.URL, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) (*QUICNameServer, error) { errors.LogInfo(context.Background(), "DNS: created Local DNS-over-QUIC client for ", url.String()) var err error @@ -50,7 +50,7 @@ func NewQUICNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*QUICN dest := net.UDPDestination(net.ParseAddress(url.Hostname()), port) s := &QUICNameServer{ - cacheController: NewCacheController(url.String(), disableCache), + cacheController: NewCacheController(url.String(), disableCache, serveStale, serveExpiredTTL), destination: &dest, clientIP: clientIP, } @@ -106,7 +106,9 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- e b, err := dns.PackMessage(r.msg) if err != nil { errors.LogErrorInner(ctx, err, "failed to pack dns query") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } @@ -114,13 +116,17 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- e err = binary.Write(dnsReqBuf, binary.BigEndian, uint16(b.Len())) if err != nil { errors.LogErrorInner(ctx, err, "binary write failed") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } _, err = dnsReqBuf.Write(b.Bytes()) if err != nil { errors.LogErrorInner(ctx, err, "buffer write failed") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } b.Release() @@ -128,14 +134,18 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- e conn, err := s.openStream(dnsCtx) if err != nil { errors.LogErrorInner(ctx, err, "failed to open quic connection") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } _, err = conn.Write(dnsReqBuf.Bytes()) if err != nil { errors.LogErrorInner(ctx, err, "failed to send query") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } @@ -146,28 +156,36 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- e n, err := respBuf.ReadFullFrom(conn, 2) if err != nil && n == 0 { errors.LogErrorInner(ctx, err, "failed to read response length") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } var length uint16 err = binary.Read(bytes.NewReader(respBuf.Bytes()), binary.BigEndian, &length) if err != nil { errors.LogErrorInner(ctx, err, "failed to parse response length") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } respBuf.Clear() n, err = respBuf.ReadFullFrom(conn, int32(length)) if err != nil && n == 0 { errors.LogErrorInner(ctx, err, "failed to read response length") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } rec, err := parseResponse(respBuf.Bytes()) if err != nil { errors.LogErrorInner(ctx, err, "failed to handle response") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } s.cacheController.updateRecord(r, rec) diff --git a/app/dns/nameserver_quic_test.go b/app/dns/nameserver_quic_test.go index fd11d2e6..83212dcb 100644 --- a/app/dns/nameserver_quic_test.go +++ b/app/dns/nameserver_quic_test.go @@ -16,7 +16,7 @@ import ( func TestQUICNameServer(t *testing.T) { url, err := url.Parse("quic://dns.adguard-dns.com") common.Must(err) - s, err := NewQUICNameServer(url, false, net.IP(nil)) + s, err := NewQUICNameServer(url, false, false, 0, net.IP(nil)) common.Must(err) ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{ @@ -43,7 +43,7 @@ func TestQUICNameServer(t *testing.T) { func TestQUICNameServerWithIPv4Override(t *testing.T) { url, err := url.Parse("quic://dns.adguard-dns.com") common.Must(err) - s, err := NewQUICNameServer(url, false, net.IP(nil)) + s, err := NewQUICNameServer(url, false, false, 0, net.IP(nil)) common.Must(err) ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{ @@ -66,7 +66,7 @@ func TestQUICNameServerWithIPv4Override(t *testing.T) { func TestQUICNameServerWithIPv6Override(t *testing.T) { url, err := url.Parse("quic://dns.adguard-dns.com") common.Must(err) - s, err := NewQUICNameServer(url, false, net.IP(nil)) + s, err := NewQUICNameServer(url, false, false, 0, net.IP(nil)) common.Must(err) ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{ diff --git a/app/dns/nameserver_tcp.go b/app/dns/nameserver_tcp.go index b9a2c2de..f549a161 100644 --- a/app/dns/nameserver_tcp.go +++ b/app/dns/nameserver_tcp.go @@ -32,10 +32,10 @@ type TCPNameServer struct { func NewTCPNameServer( url *url.URL, dispatcher routing.Dispatcher, - disableCache bool, + disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP, ) (*TCPNameServer, error) { - s, err := baseTCPNameServer(url, "TCP", disableCache, clientIP) + s, err := baseTCPNameServer(url, "TCP", disableCache, serveStale, serveExpiredTTL, clientIP) if err != nil { return nil, err } @@ -56,8 +56,8 @@ func NewTCPNameServer( } // NewTCPLocalNameServer creates DNS over TCP client object for local resolving -func NewTCPLocalNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*TCPNameServer, error) { - s, err := baseTCPNameServer(url, "TCPL", disableCache, clientIP) +func NewTCPLocalNameServer(url *url.URL, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) (*TCPNameServer, error) { + s, err := baseTCPNameServer(url, "TCPL", disableCache, serveStale, serveExpiredTTL, clientIP) if err != nil { return nil, err } @@ -69,7 +69,7 @@ func NewTCPLocalNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*T return s, nil } -func baseTCPNameServer(url *url.URL, prefix string, disableCache bool, clientIP net.IP) (*TCPNameServer, error) { +func baseTCPNameServer(url *url.URL, prefix string, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) (*TCPNameServer, error) { port := net.Port(53) if url.Port() != "" { var err error @@ -80,7 +80,7 @@ func baseTCPNameServer(url *url.URL, prefix string, disableCache bool, clientIP dest := net.TCPDestination(net.ParseAddress(url.Hostname()), port) s := &TCPNameServer{ - cacheController: NewCacheController(prefix+"//"+dest.NetAddr(), disableCache), + cacheController: NewCacheController(prefix+"//"+dest.NetAddr(), disableCache, serveStale, serveExpiredTTL), destination: &dest, clientIP: clientIP, } @@ -135,14 +135,18 @@ func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er b, err := dns.PackMessage(r.msg) if err != nil { errors.LogErrorInner(ctx, err, "failed to pack dns query") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } conn, err := s.dial(dnsCtx) if err != nil { errors.LogErrorInner(ctx, err, "failed to dial namesever") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } defer conn.Close() @@ -150,13 +154,17 @@ func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er err = binary.Write(dnsReqBuf, binary.BigEndian, uint16(b.Len())) if err != nil { errors.LogErrorInner(ctx, err, "binary write failed") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } _, err = dnsReqBuf.Write(b.Bytes()) if err != nil { errors.LogErrorInner(ctx, err, "buffer write failed") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } b.Release() @@ -164,7 +172,9 @@ func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er _, err = conn.Write(dnsReqBuf.Bytes()) if err != nil { errors.LogErrorInner(ctx, err, "failed to send query") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } dnsReqBuf.Release() @@ -174,28 +184,36 @@ func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er n, err := respBuf.ReadFullFrom(conn, 2) if err != nil && n == 0 { errors.LogErrorInner(ctx, err, "failed to read response length") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } var length uint16 err = binary.Read(bytes.NewReader(respBuf.Bytes()), binary.BigEndian, &length) if err != nil { errors.LogErrorInner(ctx, err, "failed to parse response length") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } respBuf.Clear() n, err = respBuf.ReadFullFrom(conn, int32(length)) if err != nil && n == 0 { errors.LogErrorInner(ctx, err, "failed to read response length") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } rec, err := parseResponse(respBuf.Bytes()) if err != nil { errors.LogErrorInner(ctx, err, "failed to parse DNS over TCP response") - noResponseErrCh <- err + if noResponseErrCh != nil { + noResponseErrCh <- err + } return } diff --git a/app/dns/nameserver_tcp_test.go b/app/dns/nameserver_tcp_test.go index 074896ca..048e9477 100644 --- a/app/dns/nameserver_tcp_test.go +++ b/app/dns/nameserver_tcp_test.go @@ -16,7 +16,7 @@ import ( func TestTCPLocalNameServer(t *testing.T) { url, err := url.Parse("tcp+local://8.8.8.8") common.Must(err) - s, err := NewTCPLocalNameServer(url, false, net.IP(nil)) + s, err := NewTCPLocalNameServer(url, false, false, 0, net.IP(nil)) common.Must(err) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ @@ -33,7 +33,7 @@ func TestTCPLocalNameServer(t *testing.T) { func TestTCPLocalNameServerWithCache(t *testing.T) { url, err := url.Parse("tcp+local://8.8.8.8") common.Must(err) - s, err := NewTCPLocalNameServer(url, false, net.IP(nil)) + s, err := NewTCPLocalNameServer(url, false, false, 0, net.IP(nil)) common.Must(err) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ @@ -61,7 +61,7 @@ func TestTCPLocalNameServerWithCache(t *testing.T) { func TestTCPLocalNameServerWithIPv4Override(t *testing.T) { url, err := url.Parse("tcp+local://8.8.8.8") common.Must(err) - s, err := NewTCPLocalNameServer(url, false, net.IP(nil)) + s, err := NewTCPLocalNameServer(url, false, false, 0, net.IP(nil)) common.Must(err) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ @@ -85,7 +85,7 @@ func TestTCPLocalNameServerWithIPv4Override(t *testing.T) { func TestTCPLocalNameServerWithIPv6Override(t *testing.T) { url, err := url.Parse("tcp+local://8.8.8.8") common.Must(err) - s, err := NewTCPLocalNameServer(url, false, net.IP(nil)) + s, err := NewTCPLocalNameServer(url, false, false, 0, net.IP(nil)) common.Must(err) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ diff --git a/app/dns/nameserver_udp.go b/app/dns/nameserver_udp.go index 67a57b7f..4449ac14 100644 --- a/app/dns/nameserver_udp.go +++ b/app/dns/nameserver_udp.go @@ -37,14 +37,14 @@ type udpDnsRequest struct { } // NewClassicNameServer creates udp server object for remote resolving. -func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher, disableCache bool, clientIP net.IP) *ClassicNameServer { +func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) *ClassicNameServer { // default to 53 if unspecific if address.Port == 0 { address.Port = net.Port(53) } s := &ClassicNameServer{ - cacheController: NewCacheController(strings.ToUpper(address.String()), disableCache), + cacheController: NewCacheController(strings.ToUpper(address.String()), disableCache, serveStale, serveExpiredTTL), address: &address, requests: make(map[uint16]*udpDnsRequest), clientIP: clientIP, diff --git a/common/log/dns.go b/common/log/dns.go index 76f5bbe3..91ecdb92 100644 --- a/common/log/dns.go +++ b/common/log/dns.go @@ -43,8 +43,9 @@ func (l *DNSLog) String() string { type dnsStatus string var ( - DNSQueried = dnsStatus("got answer:") - DNSCacheHit = dnsStatus("cache HIT:") + DNSQueried = dnsStatus("got answer:") + DNSCacheHit = dnsStatus("cache HIT:") + DNSCacheOptimiste = dnsStatus("cache OPTIMISTE:") ) func joinNetIP(ips []net.IP) string { diff --git a/infra/conf/dns.go b/infra/conf/dns.go index 8a60bc00..74b892c8 100644 --- a/infra/conf/dns.go +++ b/infra/conf/dns.go @@ -16,19 +16,21 @@ import ( ) type NameServerConfig struct { - Address *Address `json:"address"` - ClientIP *Address `json:"clientIp"` - Port uint16 `json:"port"` - SkipFallback bool `json:"skipFallback"` - Domains []string `json:"domains"` - ExpectedIPs StringList `json:"expectedIPs"` - ExpectIPs StringList `json:"expectIPs"` - QueryStrategy string `json:"queryStrategy"` - Tag string `json:"tag"` - TimeoutMs uint64 `json:"timeoutMs"` - DisableCache bool `json:"disableCache"` - FinalQuery bool `json:"finalQuery"` - UnexpectedIPs StringList `json:"unexpectedIPs"` + Address *Address `json:"address"` + ClientIP *Address `json:"clientIp"` + Port uint16 `json:"port"` + SkipFallback bool `json:"skipFallback"` + Domains []string `json:"domains"` + ExpectedIPs StringList `json:"expectedIPs"` + ExpectIPs StringList `json:"expectIPs"` + QueryStrategy string `json:"queryStrategy"` + Tag string `json:"tag"` + TimeoutMs uint64 `json:"timeoutMs"` + DisableCache bool `json:"disableCache"` + ServeStale bool `json:"serveStale"` + ServeExpiredTTL *uint32 `json:"serveExpiredTTL"` + FinalQuery bool `json:"finalQuery"` + UnexpectedIPs StringList `json:"unexpectedIPs"` } // UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON @@ -40,19 +42,21 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error { } var advanced struct { - Address *Address `json:"address"` - ClientIP *Address `json:"clientIp"` - Port uint16 `json:"port"` - SkipFallback bool `json:"skipFallback"` - Domains []string `json:"domains"` - ExpectedIPs StringList `json:"expectedIPs"` - ExpectIPs StringList `json:"expectIPs"` - QueryStrategy string `json:"queryStrategy"` - Tag string `json:"tag"` - TimeoutMs uint64 `json:"timeoutMs"` - DisableCache bool `json:"disableCache"` - FinalQuery bool `json:"finalQuery"` - UnexpectedIPs StringList `json:"unexpectedIPs"` + Address *Address `json:"address"` + ClientIP *Address `json:"clientIp"` + Port uint16 `json:"port"` + SkipFallback bool `json:"skipFallback"` + Domains []string `json:"domains"` + ExpectedIPs StringList `json:"expectedIPs"` + ExpectIPs StringList `json:"expectIPs"` + QueryStrategy string `json:"queryStrategy"` + Tag string `json:"tag"` + TimeoutMs uint64 `json:"timeoutMs"` + DisableCache bool `json:"disableCache"` + ServeStale bool `json:"serveStale"` + ServeExpiredTTL *uint32 `json:"serveExpiredTTL"` + FinalQuery bool `json:"finalQuery"` + UnexpectedIPs StringList `json:"unexpectedIPs"` } if err := json.Unmarshal(data, &advanced); err == nil { c.Address = advanced.Address @@ -66,6 +70,8 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error { c.Tag = advanced.Tag c.TimeoutMs = advanced.TimeoutMs c.DisableCache = advanced.DisableCache + c.ServeStale = advanced.ServeStale + c.ServeExpiredTTL = advanced.ServeExpiredTTL c.FinalQuery = advanced.FinalQuery c.UnexpectedIPs = advanced.UnexpectedIPs return nil @@ -173,6 +179,8 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) { Tag: c.Tag, TimeoutMs: c.TimeoutMs, DisableCache: c.DisableCache, + ServeStale: c.ServeStale, + ServeExpiredTTL: c.ServeExpiredTTL, FinalQuery: c.FinalQuery, UnexpectedGeoip: unexpectedGeoipList, ActUnprior: actUnprior, @@ -194,6 +202,8 @@ type DNSConfig struct { Tag string `json:"tag"` QueryStrategy string `json:"queryStrategy"` DisableCache bool `json:"disableCache"` + ServeStale bool `json:"serveStale"` + ServeExpiredTTL uint32 `json:"serveExpiredTTL"` DisableFallback bool `json:"disableFallback"` DisableFallbackIfMatch bool `json:"disableFallbackIfMatch"` UseSystemHosts bool `json:"useSystemHosts"` @@ -391,6 +401,8 @@ func (c *DNSConfig) Build() (*dns.Config, error) { config := &dns.Config{ Tag: c.Tag, DisableCache: c.DisableCache, + ServeStale: c.ServeStale, + ServeExpiredTTL: c.ServeExpiredTTL, DisableFallback: c.DisableFallback, DisableFallbackIfMatch: c.DisableFallbackIfMatch, QueryStrategy: resolveQueryStrategy(c.QueryStrategy), diff --git a/infra/conf/dns_test.go b/infra/conf/dns_test.go index 5d423884..3329f5a2 100644 --- a/infra/conf/dns_test.go +++ b/infra/conf/dns_test.go @@ -20,7 +20,7 @@ func TestDNSConfigParsing(t *testing.T) { return config.Build() } } - + expectedServeExpiredTTL := uint32(172800) runMultiTestCase(t, []TestCase{ { Input: `{ @@ -28,7 +28,9 @@ func TestDNSConfigParsing(t *testing.T) { "address": "8.8.8.8", "port": 5353, "skipFallback": true, - "domains": ["domain:example.com"] + "domains": ["domain:example.com"], + "serveStale": true, + "serveExpiredTTL": 172800 }], "hosts": { "domain:example.com": "google.com", @@ -40,6 +42,8 @@ func TestDNSConfigParsing(t *testing.T) { "clientIp": "10.0.0.1", "queryStrategy": "UseIPv4", "disableCache": true, + "serveStale": false, + "serveExpiredTTL": 86400, "disableFallback": true }`, Parser: parserCreator(), @@ -68,6 +72,8 @@ func TestDNSConfigParsing(t *testing.T) { Size: 1, }, }, + ServeStale: true, + ServeExpiredTTL: &expectedServeExpiredTTL, }, }, StaticHosts: []*dns.Config_HostMapping{ @@ -100,6 +106,8 @@ func TestDNSConfigParsing(t *testing.T) { ClientIp: []byte{10, 0, 0, 1}, QueryStrategy: dns.QueryStrategy_USE_IP4, DisableCache: true, + ServeStale: false, + ServeExpiredTTL: 86400, DisableFallback: true, }, }, From f14fd1cbee700d2f0127236e42edf79d2e23a2bc Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:45:42 +0800 Subject: [PATCH 039/136] feat(dns): add parallel query (#5239) --- app/dns/cache_controller.go | 4 +- app/dns/config.pb.go | 151 +++++++++++--------- app/dns/config.proto | 3 + app/dns/dns.go | 256 ++++++++++++++++++++++++++++------ app/dns/nameserver.go | 9 +- app/dns/nameserver_cached.go | 4 +- app/dns/nameserver_doh.go | 7 +- app/dns/nameserver_fakedns.go | 5 + app/dns/nameserver_local.go | 5 + app/dns/nameserver_quic.go | 8 +- app/dns/nameserver_tcp.go | 9 +- app/dns/nameserver_udp.go | 12 +- infra/conf/dns.go | 69 +++++++++ infra/conf/dns_test.go | 1 + 14 files changed, 422 insertions(+), 121 deletions(-) diff --git a/app/dns/cache_controller.go b/app/dns/cache_controller.go index 24ae3cb2..a303b264 100644 --- a/app/dns/cache_controller.go +++ b/app/dns/cache_controller.go @@ -194,7 +194,7 @@ func (c *CacheController) migrate() { return } - errors.LogDebug(context.Background(), c.name, " starting background cache migration for ", len(dirtyips), " items.") + errors.LogDebug(context.Background(), c.name, " starting background cache migration for ", len(dirtyips), " items") batch := make([]migrationEntry, 0, migrationBatchSize) for domain, recD := range dirtyips { @@ -214,7 +214,7 @@ func (c *CacheController) migrate() { c.dirtyips = nil c.Unlock() - errors.LogDebug(context.Background(), c.name, " cache migration completed.") + errors.LogDebug(context.Background(), c.name, " cache migration completed") } func (c *CacheController) flush(batch []migrationEntry) { diff --git a/app/dns/config.pb.go b/app/dns/config.pb.go index 9cc63986..d7ac3803 100644 --- a/app/dns/config.pb.go +++ b/app/dns/config.pb.go @@ -147,6 +147,7 @@ type NameServer struct { FinalQuery bool `protobuf:"varint,12,opt,name=finalQuery,proto3" json:"finalQuery,omitempty"` UnexpectedGeoip []*router.GeoIP `protobuf:"bytes,13,rep,name=unexpected_geoip,json=unexpectedGeoip,proto3" json:"unexpected_geoip,omitempty"` ActUnprior bool `protobuf:"varint,14,opt,name=actUnprior,proto3" json:"actUnprior,omitempty"` + PolicyID uint32 `protobuf:"varint,17,opt,name=policyID,proto3" json:"policyID,omitempty"` } func (x *NameServer) Reset() { @@ -291,6 +292,13 @@ func (x *NameServer) GetActUnprior() bool { return false } +func (x *NameServer) GetPolicyID() uint32 { + if x != nil { + return x.PolicyID + } + return 0 +} + type Config struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -312,6 +320,7 @@ type Config struct { QueryStrategy QueryStrategy `protobuf:"varint,9,opt,name=query_strategy,json=queryStrategy,proto3,enum=xray.app.dns.QueryStrategy" json:"query_strategy,omitempty"` DisableFallback bool `protobuf:"varint,10,opt,name=disableFallback,proto3" json:"disableFallback,omitempty"` DisableFallbackIfMatch bool `protobuf:"varint,11,opt,name=disableFallbackIfMatch,proto3" json:"disableFallbackIfMatch,omitempty"` + EnableParallelQuery bool `protobuf:"varint,14,opt,name=enableParallelQuery,proto3" json:"enableParallelQuery,omitempty"` } func (x *Config) Reset() { @@ -414,6 +423,13 @@ func (x *Config) GetDisableFallbackIfMatch() bool { return false } +func (x *Config) GetEnableParallelQuery() bool { + if x != nil { + return x.EnableParallelQuery + } + return false +} + type NameServer_PriorityDomain struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -599,7 +615,7 @@ var file_app_dns_config_proto_rawDesc = []byte{ 0x2e, 0x64, 0x6e, 0x73, 0x1a, 0x1c, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74, 0x2f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x99, 0x07, 0x0a, 0x0a, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb5, 0x07, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x33, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x45, 0x6e, @@ -646,70 +662,75 @@ var file_app_dns_config_proto_rawDesc = []byte{ 0x75, 0x74, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x52, 0x0f, 0x75, 0x6e, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x47, 0x65, 0x6f, 0x69, 0x70, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0a, 0x61, 0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x1a, 0x5e, 0x0a, 0x0e, 0x50, - 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, - 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x1a, 0x36, 0x0a, 0x0c, 0x4f, - 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, - 0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, - 0x69, 0x7a, 0x65, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, - 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x22, 0xe6, 0x04, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x39, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, - 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x1b, 0x0a, - 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x74, - 0x61, 0x74, 0x69, 0x63, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, - 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, - 0x67, 0x12, 0x22, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, - 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x53, 0x74, - 0x61, 0x6c, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x53, 0x74, 0x61, 0x6c, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, - 0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x12, - 0x42, 0x0a, 0x0e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, - 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, - 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, - 0x74, 0x65, 0x67, 0x79, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, - 0x65, 0x67, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, - 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69, - 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x36, 0x0a, - 0x16, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, - 0x49, 0x66, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x64, - 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x49, 0x66, - 0x4d, 0x61, 0x74, 0x63, 0x68, 0x1a, 0x92, 0x01, 0x0a, 0x0b, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, - 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, - 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, - 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, - 0x02, 0x69, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65, 0x64, 0x5f, 0x64, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x6f, - 0x78, 0x69, 0x65, 0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, - 0x2a, 0x45, 0x0a, 0x12, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, - 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x00, - 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x01, 0x12, - 0x0b, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, - 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x03, 0x2a, 0x42, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79, - 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x53, 0x45, 0x5f, - 0x49, 0x50, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x34, 0x10, - 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x36, 0x10, 0x02, 0x12, 0x0b, - 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x53, 0x59, 0x53, 0x10, 0x03, 0x42, 0x46, 0x0a, 0x10, 0x63, - 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x50, - 0x01, 0x5a, 0x21, 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, 0x61, 0x70, 0x70, - 0x2f, 0x64, 0x6e, 0x73, 0xaa, 0x02, 0x0c, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, - 0x44, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x0a, 0x61, 0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x1a, 0x5e, 0x0a, 0x0e, 0x50, 0x72, 0x69, 0x6f, 0x72, + 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, + 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, + 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x1a, 0x36, 0x0a, 0x0c, 0x4f, 0x72, 0x69, 0x67, 0x69, + 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, + 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x42, + 0x12, 0x0a, 0x10, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, + 0x54, 0x54, 0x4c, 0x22, 0x98, 0x05, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x39, + 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, + 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0a, 0x6e, + 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, + 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0b, + 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x74, + 0x61, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x22, 0x0a, + 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, + 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, 0x65, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, + 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, + 0x64, 0x54, 0x54, 0x4c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x12, 0x42, 0x0a, 0x0e, 0x71, + 0x75, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, + 0x6e, 0x73, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, + 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, + 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, + 0x63, 0x6b, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, + 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x36, 0x0a, 0x16, 0x64, 0x69, 0x73, + 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x49, 0x66, 0x4d, 0x61, + 0x74, 0x63, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x49, 0x66, 0x4d, 0x61, 0x74, 0x63, + 0x68, 0x12, 0x30, 0x0a, 0x13, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6c, + 0x6c, 0x65, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, + 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c, 0x51, 0x75, + 0x65, 0x72, 0x79, 0x1a, 0x92, 0x01, 0x0a, 0x0b, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, + 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, + 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x02, 0x69, + 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65, 0x64, 0x5f, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x69, + 0x65, 0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x2a, 0x45, + 0x0a, 0x12, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x00, 0x12, 0x0d, + 0x0a, 0x09, 0x53, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x01, 0x12, 0x0b, 0x0a, + 0x07, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, + 0x67, 0x65, 0x78, 0x10, 0x03, 0x2a, 0x42, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, + 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, + 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x34, 0x10, 0x01, 0x12, + 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x36, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, + 0x55, 0x53, 0x45, 0x5f, 0x53, 0x59, 0x53, 0x10, 0x03, 0x42, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6d, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x50, 0x01, 0x5a, + 0x21, 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, 0x61, 0x70, 0x70, 0x2f, 0x64, + 0x6e, 0x73, 0xaa, 0x02, 0x0c, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x44, 0x6e, + 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/app/dns/config.proto b/app/dns/config.proto index a87725a3..f1b47bc0 100644 --- a/app/dns/config.proto +++ b/app/dns/config.proto @@ -37,6 +37,7 @@ message NameServer { bool finalQuery = 12; repeated xray.app.router.GeoIP unexpected_geoip = 13; bool actUnprior = 14; + uint32 policyID = 17; } enum DomainMatchingType { @@ -89,4 +90,6 @@ message Config { bool disableFallback = 10; bool disableFallbackIfMatch = 11; + + bool enableParallelQuery = 14; } diff --git a/app/dns/dns.go b/app/dns/dns.go index 78e91389..9233d905 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -25,6 +25,7 @@ type DNS struct { sync.Mutex disableFallback bool disableFallbackIfMatch bool + enableParallelQuery bool ipOption *dns.IPOption hosts *StaticHosts clients []*Client @@ -157,6 +158,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) { matcherInfos: matcherInfos, disableFallback: config.DisableFallback, disableFallbackIfMatch: config.DisableFallbackIfMatch, + enableParallelQuery: config.EnableParallelQuery, checkSystem: checkSystem, }, nil } @@ -235,45 +237,11 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, er } // Name servers lookup - var errs []error - for _, client := range s.sortClients(domain) { - if !option.FakeEnable && strings.EqualFold(client.Name(), "FakeDNS") { - errors.LogDebug(s.ctx, "skip DNS resolution for domain ", domain, " at server ", client.Name()) - continue - } - - ips, ttl, err := client.QueryIP(s.ctx, domain, option) - - if len(ips) > 0 { - if ttl == 0 { - ttl = 1 - } - return ips, ttl, nil - } - - errors.LogInfoInner(s.ctx, err, "failed to lookup ip for domain ", domain, " at server ", client.Name()) - if err == nil { - err = dns.ErrEmptyResponse - } - errs = append(errs, err) - - if client.IsFinalQuery() { - break - } + if s.enableParallelQuery { + return s.parallelQuery(domain, option) + } else { + return s.serialQuery(domain, option) } - - if len(errs) > 0 { - allErrs := errors.Combine(errs...) - err0 := errs[0] - if errors.AllEqual(err0, allErrs) { - if go_errors.Is(err0, dns.ErrEmptyResponse) { - return nil, 0, dns.ErrEmptyResponse - } - return nil, 0, errors.New("returning nil for domain ", domain).Base(err0) - } - return nil, 0, errors.New("returning nil for domain ", domain).Base(allErrs) - } - return nil, 0, dns.ErrEmptyResponse } func (s *DNS) sortClients(domain string) []*Client { @@ -300,6 +268,9 @@ func (s *DNS) sortClients(domain string) []*Client { clients = append(clients, client) clientNames = append(clientNames, client.Name()) hasMatch = true + if client.finalQuery { + return clients + } } if !(s.disableFallback || s.disableFallbackIfMatch && hasMatch) { @@ -311,6 +282,9 @@ func (s *DNS) sortClients(domain string) []*Client { clientUsed[idx] = true clients = append(clients, client) clientNames = append(clientNames, client.Name()) + if client.finalQuery { + return clients + } } } @@ -322,14 +296,214 @@ func (s *DNS) sortClients(domain string) []*Client { } if len(clients) == 0 { - clients = append(clients, s.clients[0]) - clientNames = append(clientNames, s.clients[0].Name()) - errors.LogDebug(s.ctx, "domain ", domain, " will use the first DNS: ", clientNames) + if len(s.clients) > 0 { + clients = append(clients, s.clients[0]) + clientNames = append(clientNames, s.clients[0].Name()) + errors.LogWarning(s.ctx, "domain ", domain, " will use the first DNS: ", clientNames) + } else { + errors.LogError(s.ctx, "no DNS clients available for domain ", domain, " and no default clients configured") + } } return clients } +func mergeQueryErrors(domain string, errs []error) error { + if len(errs) == 0 { + return dns.ErrEmptyResponse + } + + var noRNF error + for _, err := range errs { + if go_errors.Is(err, errRecordNotFound) { + continue // server no response, ignore + } else if noRNF == nil { + noRNF = err + } else if !go_errors.Is(err, noRNF) { + return errors.New("returning nil for domain ", domain).Base(errors.Combine(errs...)) + } + } + if go_errors.Is(noRNF, dns.ErrEmptyResponse) { + return dns.ErrEmptyResponse + } + if noRNF == nil { + noRNF = errRecordNotFound + } + return errors.New("returning nil for domain ", domain).Base(noRNF) +} + +func (s *DNS) serialQuery(domain string, option dns.IPOption) ([]net.IP, uint32, error) { + var errs []error + for _, client := range s.sortClients(domain) { + if !option.FakeEnable && strings.EqualFold(client.Name(), "FakeDNS") { + errors.LogDebug(s.ctx, "skip DNS resolution for domain ", domain, " at server ", client.Name()) + continue + } + + ips, ttl, err := client.QueryIP(s.ctx, domain, option) + + if len(ips) > 0 { + return ips, ttl, nil + } + + errors.LogInfoInner(s.ctx, err, "failed to lookup ip for domain ", domain, " at server ", client.Name(), " in serial query mode") + if err == nil { + err = dns.ErrEmptyResponse + } + errs = append(errs, err) + } + return nil, 0, mergeQueryErrors(domain, errs) +} + +func (s *DNS) parallelQuery(domain string, option dns.IPOption) ([]net.IP, uint32, error) { + var errs []error + clients := s.sortClients(domain) + + resultsChan := asyncQueryAll(domain, option, clients, s.ctx) + + groups, groupOf := makeGroups( /*s.ctx,*/ clients) + results := make([]*queryResult, len(clients)) + pending := make([]int, len(groups)) + for gi, g := range groups { + pending[gi] = g.end - g.start + 1 + } + + nextGroup := 0 + for range clients { + result := <-resultsChan + results[result.index] = &result + + gi := groupOf[result.index] + pending[gi]-- + + for nextGroup < len(groups) { + g := groups[nextGroup] + + // group race, minimum rtt -> return + for j := g.start; j <= g.end; j++ { + r := results[j] + if r != nil && r.err == nil && len(r.ips) > 0 { + return r.ips, r.ttl, nil + } + } + + // current group is incomplete and no one success -> continue pending + if pending[nextGroup] > 0 { + break + } + + // all failed -> log and continue next group + for j := g.start; j <= g.end; j++ { + r := results[j] + e := r.err + if e == nil { + e = dns.ErrEmptyResponse + } + errors.LogInfoInner(s.ctx, e, "failed to lookup ip for domain ", domain, " at server ", clients[j].Name(), " in parallel query mode") + errs = append(errs, e) + } + nextGroup++ + } + } + + return nil, 0, mergeQueryErrors(domain, errs) +} + +type queryResult struct { + ips []net.IP + ttl uint32 + err error + index int +} + +func asyncQueryAll(domain string, option dns.IPOption, clients []*Client, ctx context.Context) chan queryResult { + if len(clients) == 0 { + ch := make(chan queryResult) + close(ch) + return ch + } + + ch := make(chan queryResult, len(clients)) + for i, client := range clients { + if !option.FakeEnable && strings.EqualFold(client.Name(), "FakeDNS") { + errors.LogDebug(ctx, "skip DNS resolution for domain ", domain, " at server ", client.Name()) + ch <- queryResult{err: dns.ErrEmptyResponse, index: i} + continue + } + + go func(i int, c *Client) { + qctx := ctx + if !c.server.IsDisableCache() { + nctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), c.timeoutMs*2) + qctx = nctx + defer cancel() + } + ips, ttl, err := c.QueryIP(qctx, domain, option) + ch <- queryResult{ips: ips, ttl: ttl, err: err, index: i} + }(i, client) + } + return ch +} + +type group struct{ start, end int } + +// merge only adjacent and rule-equivalent Client into a single group +func makeGroups( /*ctx context.Context,*/ clients []*Client) ([]group, []int) { + n := len(clients) + if n == 0 { + return nil, nil + } + groups := make([]group, 0, n) + groupOf := make([]int, n) + + s, e := 0, 0 + for i := 1; i < n; i++ { + if clients[i-1].policyID == clients[i].policyID { + e = i + } else { + for k := s; k <= e; k++ { + groupOf[k] = len(groups) + } + groups = append(groups, group{start: s, end: e}) + s, e = i, i + } + } + for k := s; k <= e; k++ { + groupOf[k] = len(groups) + } + groups = append(groups, group{start: s, end: e}) + + // var b strings.Builder + // b.WriteString("dns grouping: total clients=") + // b.WriteString(strconv.Itoa(n)) + // b.WriteString(", groups=") + // b.WriteString(strconv.Itoa(len(groups))) + + // for gi, g := range groups { + // b.WriteString("\n [") + // b.WriteString(strconv.Itoa(g.start)) + // b.WriteString("..") + // b.WriteString(strconv.Itoa(g.end)) + // b.WriteString("] gid=") + // b.WriteString(strconv.Itoa(gi)) + // b.WriteString(" pid=") + // b.WriteString(strconv.FormatUint(uint64(clients[g.start].policyID), 10)) + // b.WriteString(" members: ") + + // for i := g.start; i <= g.end; i++ { + // if i > g.start { + // b.WriteString(", ") + // } + // b.WriteString(strconv.Itoa(i)) + // b.WriteByte(':') + // b.WriteString(clients[i].Name()) + // } + // } + // errors.LogDebug(ctx, b.String()) + + return groups, groupOf +} + func init() { common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { return New(ctx, config.(*Config)) diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go index 3f25833e..e606d4b3 100644 --- a/app/dns/nameserver.go +++ b/app/dns/nameserver.go @@ -20,6 +20,9 @@ import ( type Server interface { // Name of the Client. Name() string + + IsDisableCache() bool + // QueryIP sends IP queries to its configured server. QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) } @@ -38,6 +41,7 @@ type Client struct { finalQuery bool ipOption *dns.IPOption checkSystem bool + policyID uint32 } // NewServer creates a name server object according to the network destination url. @@ -199,6 +203,7 @@ func NewClient( client.finalQuery = ns.FinalQuery client.ipOption = &ipOption client.checkSystem = checkSystem + client.policyID = ns.PolicyID return nil }) return client, err @@ -209,10 +214,6 @@ func (c *Client) Name() string { return c.server.Name() } -func (c *Client) IsFinalQuery() bool { - return c.finalQuery -} - // QueryIP sends DNS query to the name server with the client's IP. func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) { if c.checkSystem { diff --git a/app/dns/nameserver_cached.go b/app/dns/nameserver_cached.go index e27dda49..cbd2f031 100644 --- a/app/dns/nameserver_cached.go +++ b/app/dns/nameserver_cached.go @@ -28,12 +28,12 @@ func queryIP(ctx context.Context, s CachedNameserver, domain string, option dns. ips, ttl, err := merge(option, rec.A, rec.AAAA) if !go_errors.Is(err, errRecordNotFound) { if ttl > 0 { - // errors.LogDebugInner(ctx, err, cache.name, " cache HIT ", fqdn, " -> ", ips) + errors.LogDebugInner(ctx, err, cache.name, " cache HIT ", fqdn, " -> ", ips) log.Record(&log.DNSLog{Server: cache.name, Domain: fqdn, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) return ips, uint32(ttl), err } if cache.serveStale && (cache.serveExpiredTTL == 0 || cache.serveExpiredTTL < ttl) { - // errors.LogDebugInner(ctx, err, cache.name, " cache OPTIMISTE ", fqdn, " -> ", ips) + errors.LogDebugInner(ctx, err, cache.name, " cache OPTIMISTE ", fqdn, " -> ", ips) log.Record(&log.DNSLog{Server: cache.name, Domain: fqdn, Result: ips, Status: log.DNSCacheOptimiste, Elapsed: 0, Error: err}) go pull(ctx, s, fqdn, option) return ips, 1, err diff --git a/app/dns/nameserver_doh.go b/app/dns/nameserver_doh.go index 88198aed..9557c325 100644 --- a/app/dns/nameserver_doh.go +++ b/app/dns/nameserver_doh.go @@ -116,6 +116,11 @@ func (s *DoHNameServer) Name() string { return s.cacheController.name } +// IsDisableCache implements Server. +func (s *DoHNameServer) IsDisableCache() bool { + return s.cacheController.disableCache +} + func (s *DoHNameServer) newReqID() uint16 { return 0 } @@ -130,7 +135,7 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er errors.LogInfo(ctx, s.Name(), " querying: ", fqdn) if s.Name()+"." == "DOH//"+fqdn { - errors.LogError(ctx, s.Name(), " tries to resolve itself! Use IP or set \"hosts\" instead.") + errors.LogError(ctx, s.Name(), " tries to resolve itself! Use IP or set \"hosts\" instead") if noResponseErrCh != nil { noResponseErrCh <- errors.New("tries to resolve itself!", s.Name()) } diff --git a/app/dns/nameserver_fakedns.go b/app/dns/nameserver_fakedns.go index 8c598ac8..bed11bd6 100644 --- a/app/dns/nameserver_fakedns.go +++ b/app/dns/nameserver_fakedns.go @@ -20,6 +20,11 @@ func (FakeDNSServer) Name() string { return "FakeDNS" } +// IsDisableCache implements Server. +func (s *FakeDNSServer) IsDisableCache() bool { + return true +} + func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOption) ([]net.IP, uint32, error) { if f.fakeDNSEngine == nil { return nil, 0, errors.New("Unable to locate a fake DNS Engine").AtError() diff --git a/app/dns/nameserver_local.go b/app/dns/nameserver_local.go index 91b003e3..576c259b 100644 --- a/app/dns/nameserver_local.go +++ b/app/dns/nameserver_local.go @@ -35,6 +35,11 @@ func (s *LocalNameServer) Name() string { return "localhost" } +// IsDisableCache implements Server. +func (s *LocalNameServer) IsDisableCache() bool { + return true +} + // NewLocalNameServer creates localdns server object for directly lookup in system DNS. func NewLocalNameServer() *LocalNameServer { errors.LogInfo(context.Background(), "DNS: created localhost client") diff --git a/app/dns/nameserver_quic.go b/app/dns/nameserver_quic.go index 3d271d74..4c7ac032 100644 --- a/app/dns/nameserver_quic.go +++ b/app/dns/nameserver_quic.go @@ -37,8 +37,6 @@ type QUICNameServer struct { // NewQUICNameServer creates DNS-over-QUIC client object for local resolving func NewQUICNameServer(url *url.URL, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) (*QUICNameServer, error) { - errors.LogInfo(context.Background(), "DNS: created Local DNS-over-QUIC client for ", url.String()) - var err error port := net.Port(853) if url.Port() != "" { @@ -55,6 +53,7 @@ func NewQUICNameServer(url *url.URL, disableCache bool, serveStale bool, serveEx clientIP: clientIP, } + errors.LogInfo(context.Background(), "DNS: created Local DNS-over-QUIC client for ", url.String()) return s, nil } @@ -63,6 +62,11 @@ func (s *QUICNameServer) Name() string { return s.cacheController.name } +// IsDisableCache implements Server. +func (s *QUICNameServer) IsDisableCache() bool { + return s.cacheController.disableCache +} + func (s *QUICNameServer) newReqID() uint16 { return 0 } diff --git a/app/dns/nameserver_tcp.go b/app/dns/nameserver_tcp.go index f549a161..283a42a7 100644 --- a/app/dns/nameserver_tcp.go +++ b/app/dns/nameserver_tcp.go @@ -52,6 +52,7 @@ func NewTCPNameServer( ), nil } + errors.LogInfo(context.Background(), "DNS: created TCP client initialized for ", url.String()) return s, nil } @@ -66,6 +67,7 @@ func NewTCPLocalNameServer(url *url.URL, disableCache bool, serveStale bool, ser return internet.DialSystem(ctx, *s.destination, nil) } + errors.LogInfo(context.Background(), "DNS: created Local TCP client initialized for ", url.String()) return s, nil } @@ -93,6 +95,11 @@ func (s *TCPNameServer) Name() string { return s.cacheController.name } +// IsDisableCache implements Server. +func (s *TCPNameServer) IsDisableCache() bool { + return s.cacheController.disableCache +} + func (s *TCPNameServer) newReqID() uint16 { return uint16(atomic.AddUint32(&s.reqID, 1)) } @@ -104,7 +111,7 @@ func (s *TCPNameServer) getCacheController() *CacheController { // sendQuery implements CachedNameserver. func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { - errors.LogDebug(ctx, s.Name(), " querying DNS for: ", fqdn) + errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn) reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) diff --git a/app/dns/nameserver_udp.go b/app/dns/nameserver_udp.go index 4449ac14..f263a0f8 100644 --- a/app/dns/nameserver_udp.go +++ b/app/dns/nameserver_udp.go @@ -54,6 +54,7 @@ func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher Execute: s.RequestsCleanup, } s.udpServer = udp.NewDispatcher(dispatcher, s.HandleResponse) + errors.LogInfo(context.Background(), "DNS: created UDP client initialized for ", address.NetAddr()) return s } @@ -63,6 +64,11 @@ func (s *ClassicNameServer) Name() string { return s.cacheController.name } +// IsDisableCache implements Server. +func (s *ClassicNameServer) IsDisableCache() bool { + return s.cacheController.disableCache +} + // RequestsCleanup clears expired items from cache func (s *ClassicNameServer) RequestsCleanup() error { now := time.Now() @@ -92,7 +98,7 @@ func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_prot ipRec, err := parseResponse(payload.Bytes()) payload.Release() if err != nil { - errors.LogError(ctx, s.Name(), " fail to parse responded DNS udp") + errors.LogErrorInner(ctx, err, s.Name(), " fail to parse responded DNS udp") return } @@ -105,7 +111,7 @@ func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_prot } s.Unlock() if !ok { - errors.LogError(ctx, s.Name(), " cannot find the pending request") + errors.LogErrorInner(ctx, err, s.Name(), " cannot find the pending request") return } @@ -155,7 +161,7 @@ func (s *ClassicNameServer) getCacheController() *CacheController { // sendQuery implements CachedNameserver. func (s *ClassicNameServer) sendQuery(ctx context.Context, _ chan<- error, fqdn string, option dns_feature.IPOption) { - errors.LogDebug(ctx, s.Name(), " querying DNS for: ", fqdn) + errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn) reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) diff --git a/infra/conf/dns.go b/infra/conf/dns.go index 74b892c8..7c94a488 100644 --- a/infra/conf/dns.go +++ b/infra/conf/dns.go @@ -206,6 +206,7 @@ type DNSConfig struct { ServeExpiredTTL uint32 `json:"serveExpiredTTL"` DisableFallback bool `json:"disableFallback"` DisableFallbackIfMatch bool `json:"disableFallbackIfMatch"` + EnableParallelQuery bool `json:"enableParallelQuery"` UseSystemHosts bool `json:"useSystemHosts"` } @@ -405,6 +406,7 @@ func (c *DNSConfig) Build() (*dns.Config, error) { ServeExpiredTTL: c.ServeExpiredTTL, DisableFallback: c.DisableFallback, DisableFallbackIfMatch: c.DisableFallbackIfMatch, + EnableParallelQuery: c.EnableParallelQuery, QueryStrategy: resolveQueryStrategy(c.QueryStrategy), } @@ -415,11 +417,78 @@ func (c *DNSConfig) Build() (*dns.Config, error) { config.ClientIp = []byte(c.ClientIP.IP()) } + // Build PolicyID + policyMap := map[string]uint32{} + nextPolicyID := uint32(1) + buildPolicyID := func(nsc *NameServerConfig) uint32 { + var sb strings.Builder + + // ClientIP + if nsc.ClientIP != nil { + sb.WriteString("client=") + sb.WriteString(nsc.ClientIP.String()) + sb.WriteByte('|') + } else { + sb.WriteString("client=none|") + } + + // SkipFallback + if nsc.SkipFallback { + sb.WriteString("skip=1|") + } else { + sb.WriteString("skip=0|") + } + + // QueryStrategy + sb.WriteString("qs=") + sb.WriteString(strings.ToLower(strings.TrimSpace(nsc.QueryStrategy))) + sb.WriteByte('|') + + // Tag + sb.WriteString("tag=") + sb.WriteString(strings.ToLower(strings.TrimSpace(nsc.Tag))) + sb.WriteByte('|') + + // []string helper + writeList := func(tag string, lst []string) { + if len(lst) == 0 { + sb.WriteString(tag) + sb.WriteString("=[]|") + return + } + cp := make([]string, len(lst)) + for i, s := range lst { + cp[i] = strings.TrimSpace(strings.ToLower(s)) + } + sort.Strings(cp) + sb.WriteString(tag) + sb.WriteByte('=') + sb.WriteString(strings.Join(cp, ",")) + sb.WriteByte('|') + } + + writeList("domains", nsc.Domains) + writeList("expected", nsc.ExpectedIPs) + writeList("expect", nsc.ExpectIPs) + writeList("unexpected", nsc.UnexpectedIPs) + + key := sb.String() + + if id, ok := policyMap[key]; ok { + return id + } + id := nextPolicyID + nextPolicyID++ + policyMap[key] = id + return id + } + for _, server := range c.Servers { ns, err := server.Build() if err != nil { return nil, errors.New("failed to build nameserver").Base(err) } + ns.PolicyID = buildPolicyID(server) config.NameServer = append(config.NameServer, ns) } diff --git a/infra/conf/dns_test.go b/infra/conf/dns_test.go index 3329f5a2..af51b734 100644 --- a/infra/conf/dns_test.go +++ b/infra/conf/dns_test.go @@ -74,6 +74,7 @@ func TestDNSConfigParsing(t *testing.T) { }, ServeStale: true, ServeExpiredTTL: &expectedServeExpiredTTL, + PolicyID: 1, // Servers with certain identical fields share this ID, incrementing starting from 1. See: Build PolicyID }, }, StaticHosts: []*dns.Config_HostMapping{ From d41840132a3ca4f14eb0f49d9f67add51173c9bc Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:46:41 +0800 Subject: [PATCH 040/136] Router: Remove the deprecated UseIP option (#5323) --- app/router/config.pb.go | 29 ++++++++++++----------------- app/router/config.proto | 4 ++-- infra/conf/router.go | 2 -- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/app/router/config.pb.go b/app/router/config.pb.go index 046e6896..09cc77e6 100644 --- a/app/router/config.pb.go +++ b/app/router/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.35.1 -// protoc v5.28.2 +// protoc v5.29.4 // source: app/router/config.proto package router @@ -84,8 +84,6 @@ type Config_DomainStrategy int32 const ( // Use domain as is. Config_AsIs Config_DomainStrategy = 0 - // Always resolve IP for domains. - Config_UseIp Config_DomainStrategy = 1 // Resolve to IP if the domain doesn't match any rules. Config_IpIfNonMatch Config_DomainStrategy = 2 // Resolve to IP if any rule requires IP matching. @@ -96,13 +94,11 @@ const ( var ( Config_DomainStrategy_name = map[int32]string{ 0: "AsIs", - 1: "UseIp", 2: "IpIfNonMatch", 3: "IpOnDemand", } Config_DomainStrategy_value = map[string]int32{ "AsIs": 0, - "UseIp": 1, "IpIfNonMatch": 2, "IpOnDemand": 3, } @@ -1171,7 +1167,7 @@ var file_app_router_config_proto_rawDesc = []byte{ 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, 0x54, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, 0x54, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x6c, 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x02, 0x52, 0x09, 0x74, 0x6f, 0x6c, 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x22, 0x9b, 0x02, 0x0a, + 0x02, 0x52, 0x09, 0x74, 0x6f, 0x6c, 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x22, 0x90, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4f, 0x0a, 0x0f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, @@ -1185,17 +1181,16 @@ var file_app_router_config_proto_rawDesc = []byte{ 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, - 0x65, 0x22, 0x47, 0x0a, 0x0e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, - 0x65, 0x67, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x73, 0x49, 0x73, 0x10, 0x00, 0x12, 0x09, 0x0a, - 0x05, 0x55, 0x73, 0x65, 0x49, 0x70, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x70, 0x49, 0x66, - 0x4e, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x70, - 0x4f, 0x6e, 0x44, 0x65, 0x6d, 0x61, 0x6e, 0x64, 0x10, 0x03, 0x42, 0x4f, 0x0a, 0x13, 0x63, 0x6f, - 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x72, 0x50, 0x01, 0x5a, 0x24, 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, 0x61, - 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, - 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x65, 0x22, 0x3c, 0x0a, 0x0e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, + 0x65, 0x67, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x73, 0x49, 0x73, 0x10, 0x00, 0x12, 0x10, 0x0a, + 0x0c, 0x49, 0x70, 0x49, 0x66, 0x4e, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x10, 0x02, 0x12, + 0x0e, 0x0a, 0x0a, 0x49, 0x70, 0x4f, 0x6e, 0x44, 0x65, 0x6d, 0x61, 0x6e, 0x64, 0x10, 0x03, 0x42, + 0x4f, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, + 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x50, 0x01, 0x5a, 0x24, 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, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0xaa, 0x02, + 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/app/router/config.proto b/app/router/config.proto index 20dda4ba..f26bccb4 100644 --- a/app/router/config.proto +++ b/app/router/config.proto @@ -147,8 +147,8 @@ message Config { // Use domain as is. AsIs = 0; - // Always resolve IP for domains. - UseIp = 1; + // [Deprecated] Always resolve IP for domains. + // UseIp = 1; // Resolve to IP if the domain doesn't match any rules. IpIfNonMatch = 2; diff --git a/infra/conf/router.go b/infra/conf/router.go index 354d31f1..6e09038b 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -83,8 +83,6 @@ func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy { } switch strings.ToLower(ds) { - case "alwaysip": - return router.Config_UseIp case "ipifnonmatch": return router.Config_IpIfNonMatch case "ipondemand": From 2969a189e66464877b6661d84b9ee3094e95db2d Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Sun, 23 Nov 2025 01:09:49 +0000 Subject: [PATCH 041/136] Sockopt config: Add `trustedXForwardedFor` (for XHTTP, WS, HU inbounds) (#5331) Fixes https://github.com/XTLS/Xray-core/pull/5101#issuecomment-3404979909 --- app/dns/config.pb.go | 2 +- app/router/config.pb.go | 2 +- infra/conf/transport_internet.go | 2 + transport/internet/config.pb.go | 98 +++++++++++++++------------ transport/internet/config.proto | 2 + transport/internet/httpupgrade/hub.go | 14 +++- transport/internet/splithttp/hub.go | 40 +++++++---- transport/internet/websocket/hub.go | 26 +++++-- 8 files changed, 119 insertions(+), 67 deletions(-) diff --git a/app/dns/config.pb.go b/app/dns/config.pb.go index d7ac3803..f8f317b3 100644 --- a/app/dns/config.pb.go +++ b/app/dns/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.35.1 -// protoc v5.29.4 +// protoc v5.28.2 // source: app/dns/config.proto package dns diff --git a/app/router/config.pb.go b/app/router/config.pb.go index 09cc77e6..25f679b0 100644 --- a/app/router/config.pb.go +++ b/app/router/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.35.1 -// protoc v5.29.4 +// protoc v5.28.2 // source: app/router/config.proto package router diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 89dfa645..5a860b1c 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -810,6 +810,7 @@ type SocketConfig struct { CustomSockopt []*CustomSockoptConfig `json:"customSockopt"` AddressPortStrategy string `json:"addressPortStrategy"` HappyEyeballsSettings *HappyEyeballsConfig `json:"happyEyeballs"` + TrustedXForwardedFor []string `json:"trustedXForwardedFor"` } // Build implements Buildable. @@ -929,6 +930,7 @@ func (c *SocketConfig) Build() (*internet.SocketConfig, error) { CustomSockopt: customSockopts, AddressPortStrategy: addressPortStrategy, HappyEyeballs: happyEyeballs, + TrustedXForwardedFor: c.TrustedXForwardedFor, }, nil } diff --git a/transport/internet/config.pb.go b/transport/internet/config.pb.go index f974926e..41ce3294 100644 --- a/transport/internet/config.pb.go +++ b/transport/internet/config.pb.go @@ -530,6 +530,7 @@ type SocketConfig struct { 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"` + TrustedXForwardedFor []string `protobuf:"bytes,23,rep,name=trusted_x_forwarded_for,json=trustedXForwardedFor,proto3" json:"trusted_x_forwarded_for,omitempty"` } func (x *SocketConfig) Reset() { @@ -716,6 +717,13 @@ func (x *SocketConfig) GetHappyEyeballs() *HappyEyeballsConfig { return nil } +func (x *SocketConfig) GetTrustedXForwardedFor() []string { + if x != nil { + return x.TrustedXForwardedFor + } + return nil +} + type HappyEyeballsConfig struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -843,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, 0xd2, 0x08, 0x0a, 0x0c, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, + 0x74, 0x79, 0x70, 0x65, 0x22, 0x89, 0x09, 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, @@ -909,48 +917,52 @@ var file_transport_internet_config_proto_rawDesc = []byte{ 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, 0xad, 0x01, 0x0a, 0x13, 0x48, 0x61, - 0x70, 0x70, 0x79, 0x45, 0x79, 0x65, 0x62, 0x61, 0x6c, 0x6c, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x5f, - 0x69, 0x70, 0x76, 0x36, 0x18, 0x01, 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, 0x02, 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, 0x03, 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, 0x04, 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, + 0x45, 0x79, 0x65, 0x62, 0x61, 0x6c, 0x6c, 0x73, 0x12, 0x35, 0x0a, 0x17, 0x74, 0x72, 0x75, 0x73, + 0x74, 0x65, 0x64, 0x5f, 0x78, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x5f, + 0x66, 0x6f, 0x72, 0x18, 0x17, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x74, 0x72, 0x75, 0x73, 0x74, + 0x65, 0x64, 0x58, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x46, 0x6f, 0x72, 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, 0xad, 0x01, 0x0a, 0x13, 0x48, 0x61, 0x70, 0x70, 0x79, 0x45, 0x79, 0x65, 0x62, 0x61, 0x6c, + 0x6c, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x6f, + 0x72, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x18, 0x01, 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, + 0x02, 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, 0x03, 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, 0x04, 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 ( diff --git a/transport/internet/config.proto b/transport/internet/config.proto index a40f0938..e1655fdd 100644 --- a/transport/internet/config.proto +++ b/transport/internet/config.proto @@ -132,6 +132,8 @@ message SocketConfig { AddressPortStrategy address_port_strategy = 21; HappyEyeballsConfig happy_eyeballs = 22; + + repeated string trusted_x_forwarded_for = 23; } message HappyEyeballsConfig { diff --git a/transport/internet/httpupgrade/hub.go b/transport/internet/httpupgrade/hub.go index dc67c747..c3a51607 100644 --- a/transport/internet/httpupgrade/hub.go +++ b/transport/internet/httpupgrade/hub.go @@ -20,6 +20,7 @@ type server struct { config *Config addConn internet.ConnHandler innnerListener net.Listener + socketSettings *internet.SocketConfig } func (s *server) Close() error { @@ -70,7 +71,17 @@ func (s *server) Handle(conn net.Conn) (stat.Connection, error) { return nil, err } - forwardedAddrs := http_proto.ParseXForwardedFor(req.Header) + var forwardedAddrs []net.Address + if s.socketSettings != nil && len(s.socketSettings.TrustedXForwardedFor) > 0 { + for _, key := range s.socketSettings.TrustedXForwardedFor { + if len(req.Header.Values(key)) > 0 { + forwardedAddrs = http_proto.ParseXForwardedFor(req.Header) + break + } + } + } else { + forwardedAddrs = http_proto.ParseXForwardedFor(req.Header) + } remoteAddr := conn.RemoteAddr() if len(forwardedAddrs) > 0 && forwardedAddrs[0].Family().IsIP() { remoteAddr = &net.TCPAddr{ @@ -141,6 +152,7 @@ func ListenHTTPUpgrade(ctx context.Context, address net.Address, port net.Port, config: transportConfiguration, addConn: addConn, innnerListener: listener, + socketSettings: streamSettings.SocketSettings, } go serverInstance.keepAccepting() return serverInstance, nil diff --git a/transport/internet/splithttp/hub.go b/transport/internet/splithttp/hub.go index d161741a..5e9b9408 100644 --- a/transport/internet/splithttp/hub.go +++ b/transport/internet/splithttp/hub.go @@ -27,13 +27,14 @@ import ( ) type requestHandler struct { - config *Config - host string - path string - ln *Listener - sessionMu *sync.Mutex - sessions sync.Map - localAddr net.Addr + config *Config + host string + path string + ln *Listener + sessionMu *sync.Mutex + sessions sync.Map + localAddr net.Addr + socketSettings *internet.SocketConfig } type httpSession struct { @@ -139,7 +140,17 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req return } - forwardedAddrs := http_proto.ParseXForwardedFor(request.Header) + var forwardedAddrs []net.Address + if h.socketSettings != nil && len(h.socketSettings.TrustedXForwardedFor) > 0 { + for _, key := range h.socketSettings.TrustedXForwardedFor { + if len(request.Header.Values(key)) > 0 { + forwardedAddrs = http_proto.ParseXForwardedFor(request.Header) + break + } + } + } else { + forwardedAddrs = http_proto.ParseXForwardedFor(request.Header) + } var remoteAddr net.Addr var err error remoteAddr, err = net.ResolveTCPAddr("tcp", request.RemoteAddr) @@ -356,12 +367,13 @@ func ListenXH(ctx context.Context, address net.Address, port net.Port, streamSet } } handler := &requestHandler{ - config: l.config, - host: l.config.Host, - path: l.config.GetNormalizedPath(), - ln: l, - sessionMu: &sync.Mutex{}, - sessions: sync.Map{}, + config: l.config, + host: l.config.Host, + path: l.config.GetNormalizedPath(), + ln: l, + sessionMu: &sync.Mutex{}, + sessions: sync.Map{}, + socketSettings: streamSettings.SocketSettings, } tlsConfig := getTLSConfig(streamSettings) l.isH3 = len(tlsConfig.NextProtos) == 1 && tlsConfig.NextProtos[0] == "h3" diff --git a/transport/internet/websocket/hub.go b/transport/internet/websocket/hub.go index feefe2af..b47e7d58 100644 --- a/transport/internet/websocket/hub.go +++ b/transport/internet/websocket/hub.go @@ -21,9 +21,10 @@ import ( ) type requestHandler struct { - host string - path string - ln *Listener + host string + path string + ln *Listener + socketSettings *internet.SocketConfig } var replacer = strings.NewReplacer("+", "-", "/", "_", "=", "") @@ -64,7 +65,17 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req return } - forwardedAddrs := http_proto.ParseXForwardedFor(request.Header) + var forwardedAddrs []net.Address + if h.socketSettings != nil && len(h.socketSettings.TrustedXForwardedFor) > 0 { + for _, key := range h.socketSettings.TrustedXForwardedFor { + if len(request.Header.Values(key)) > 0 { + forwardedAddrs = http_proto.ParseXForwardedFor(request.Header) + break + } + } + } else { + forwardedAddrs = http_proto.ParseXForwardedFor(request.Header) + } remoteAddr := conn.RemoteAddr() if len(forwardedAddrs) > 0 && forwardedAddrs[0].Family().IsIP() { remoteAddr = &net.TCPAddr{ @@ -132,9 +143,10 @@ func ListenWS(ctx context.Context, address net.Address, port net.Port, streamSet l.server = http.Server{ Handler: &requestHandler{ - host: wsSettings.Host, - path: wsSettings.GetNormalizedPath(), - ln: l, + host: wsSettings.Host, + path: wsSettings.GetNormalizedPath(), + ln: l, + socketSettings: streamSettings.SocketSettings, }, ReadHeaderTimeout: time.Second * 4, MaxHeaderBytes: 8192, From a83253f3d7a4d423d2bfdf522205daacd8ee868e Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Sun, 23 Nov 2025 04:23:48 +0000 Subject: [PATCH 042/136] VLESS Reverse Proxy: Forbid reverse-proxy UUID using forward-proxy, enabled by default https://t.me/projectXtls/1070 https://github.com/XTLS/Xray-core/pull/5101#issuecomment-3567464144 --- proxy/vless/inbound/inbound.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 277ab068..89ed0e72 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -538,6 +538,10 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s account := request.User.Account.(*vless.MemoryAccount) + if account.Reverse != nil && request.Command != protocol.RequestCommandRvs { + return errors.New("for safety reasons, user " + account.ID.String() + " is not allowed to use forward proxy") + } + responseAddons := &encoding.Addons{ // Flow: requestAddons.Flow, } From d60ef656cc11fed9497a371f6cc129e5ff25ec16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 00:55:50 +0000 Subject: [PATCH 043/136] Bump github.com/quic-go/quic-go from 0.56.0 to 0.57.0 (#5335) Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.56.0 to 0.57.0. - [Release notes](https://github.com/quic-go/quic-go/releases) - [Commits](https://github.com/quic-go/quic-go/compare/v0.56.0...v0.57.0) --- updated-dependencies: - dependency-name: github.com/quic-go/quic-go dependency-version: 0.57.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index e51632ae..9165e78c 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/miekg/dns v1.1.68 github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 - github.com/quic-go/quic-go v0.56.0 + github.com/quic-go/quic-go v0.57.0 github.com/refraction-networking/utls v1.8.1 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 @@ -43,7 +43,7 @@ require ( github.com/klauspost/cpuid/v2 v2.0.12 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/vishvananda/netns v0.0.5 // indirect golang.org/x/mod v0.29.0 // indirect diff --git a/go.sum b/go.sum index 77ee8b99..a8395f1e 100644 --- a/go.sum +++ b/go.sum @@ -48,10 +48,10 @@ github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaAS github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY= -github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c= +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/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= +github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= From 9d3401b6f09be4f4cb32ce69779572bb11458439 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 02:11:33 +0000 Subject: [PATCH 044/136] Bump github.com/quic-go/quic-go from 0.57.0 to 0.57.1 (#5352) Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.57.0 to 0.57.1. - [Release notes](https://github.com/quic-go/quic-go/releases) - [Commits](https://github.com/quic-go/quic-go/compare/v0.57.0...v0.57.1) --- updated-dependencies: - dependency-name: github.com/quic-go/quic-go dependency-version: 0.57.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9165e78c..69816eb4 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/miekg/dns v1.1.68 github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 - github.com/quic-go/quic-go v0.57.0 + github.com/quic-go/quic-go v0.57.1 github.com/refraction-networking/utls v1.8.1 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 diff --git a/go.sum b/go.sum index a8395f1e..62af7643 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= -github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= +github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= From ed5f7e7af5a1996cc26885b64f5efea6832adbad Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:12:37 +0800 Subject: [PATCH 045/136] fix(dns): inheritance issue with disableCache (#5351) --- app/dns/config.pb.go | 177 +++++++++++++++++++++-------------------- app/dns/config.proto | 4 +- app/dns/dns.go | 12 ++- infra/conf/dns.go | 8 +- infra/conf/dns_test.go | 3 +- 5 files changed, 108 insertions(+), 96 deletions(-) diff --git a/app/dns/config.pb.go b/app/dns/config.pb.go index f8f317b3..69eb4c3b 100644 --- a/app/dns/config.pb.go +++ b/app/dns/config.pb.go @@ -141,8 +141,8 @@ type NameServer struct { ActPrior bool `protobuf:"varint,8,opt,name=actPrior,proto3" json:"actPrior,omitempty"` Tag string `protobuf:"bytes,9,opt,name=tag,proto3" json:"tag,omitempty"` TimeoutMs uint64 `protobuf:"varint,10,opt,name=timeoutMs,proto3" json:"timeoutMs,omitempty"` - DisableCache bool `protobuf:"varint,11,opt,name=disableCache,proto3" json:"disableCache,omitempty"` - ServeStale bool `protobuf:"varint,15,opt,name=serveStale,proto3" json:"serveStale,omitempty"` + DisableCache *bool `protobuf:"varint,11,opt,name=disableCache,proto3,oneof" json:"disableCache,omitempty"` + ServeStale *bool `protobuf:"varint,15,opt,name=serveStale,proto3,oneof" json:"serveStale,omitempty"` ServeExpiredTTL *uint32 `protobuf:"varint,16,opt,name=serveExpiredTTL,proto3,oneof" json:"serveExpiredTTL,omitempty"` FinalQuery bool `protobuf:"varint,12,opt,name=finalQuery,proto3" json:"finalQuery,omitempty"` UnexpectedGeoip []*router.GeoIP `protobuf:"bytes,13,rep,name=unexpected_geoip,json=unexpectedGeoip,proto3" json:"unexpected_geoip,omitempty"` @@ -251,15 +251,15 @@ func (x *NameServer) GetTimeoutMs() uint64 { } func (x *NameServer) GetDisableCache() bool { - if x != nil { - return x.DisableCache + if x != nil && x.DisableCache != nil { + return *x.DisableCache } return false } func (x *NameServer) GetServeStale() bool { - if x != nil { - return x.ServeStale + if x != nil && x.ServeStale != nil { + return *x.ServeStale } return false } @@ -615,7 +615,7 @@ var file_app_dns_config_proto_rawDesc = []byte{ 0x2e, 0x64, 0x6e, 0x73, 0x1a, 0x1c, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74, 0x2f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb5, 0x07, 0x0a, 0x0a, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xdf, 0x07, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x33, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x45, 0x6e, @@ -647,90 +647,93 @@ var file_app_dns_config_proto_rawDesc = []byte{ 0x50, 0x72, 0x69, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, - 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x43, 0x61, 0x63, 0x68, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, - 0x65, 0x72, 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, 0x65, 0x12, 0x2d, 0x0a, 0x0f, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x18, 0x10, 0x20, 0x01, - 0x28, 0x0d, 0x48, 0x00, 0x52, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, - 0x65, 0x64, 0x54, 0x54, 0x4c, 0x88, 0x01, 0x01, 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x6e, 0x61, - 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69, - 0x6e, 0x61, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x41, 0x0a, 0x10, 0x75, 0x6e, 0x65, 0x78, - 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x18, 0x0d, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x52, 0x0f, 0x75, 0x6e, 0x65, 0x78, - 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x47, 0x65, 0x6f, 0x69, 0x70, 0x12, 0x1e, 0x0a, 0x0a, 0x61, - 0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0a, 0x61, 0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x1a, 0x5e, 0x0a, 0x0e, 0x50, 0x72, 0x69, 0x6f, 0x72, - 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, - 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, - 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, - 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x1a, 0x36, 0x0a, 0x0c, 0x4f, 0x72, 0x69, 0x67, 0x69, - 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, - 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x42, - 0x12, 0x0a, 0x10, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, - 0x54, 0x54, 0x4c, 0x22, 0x98, 0x05, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x39, - 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, - 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0a, 0x6e, - 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x63, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, - 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, - 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0b, - 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x74, - 0x61, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x22, 0x0a, - 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, - 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, 0x65, 0x18, - 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, - 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, - 0x64, 0x54, 0x54, 0x4c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x73, 0x65, 0x72, 0x76, - 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x12, 0x42, 0x0a, 0x0e, 0x71, - 0x75, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, - 0x6e, 0x73, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, - 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, - 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, - 0x63, 0x6b, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x36, 0x0a, 0x16, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x49, 0x66, 0x4d, 0x61, - 0x74, 0x63, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x49, 0x66, 0x4d, 0x61, 0x74, 0x63, - 0x68, 0x12, 0x30, 0x0a, 0x13, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6c, - 0x6c, 0x65, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, - 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c, 0x51, 0x75, - 0x65, 0x72, 0x79, 0x1a, 0x92, 0x01, 0x0a, 0x0b, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x12, 0x27, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x43, 0x61, 0x63, 0x68, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c, 0x64, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x88, 0x01, 0x01, 0x12, 0x23, + 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, 0x65, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x08, 0x48, 0x01, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, 0x65, + 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, + 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52, 0x0f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x88, + 0x01, 0x01, 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x12, 0x41, 0x0a, 0x10, 0x75, 0x6e, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x5f, 0x67, 0x65, 0x6f, 0x69, 0x70, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x47, + 0x65, 0x6f, 0x49, 0x50, 0x52, 0x0f, 0x75, 0x6e, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x47, 0x65, 0x6f, 0x69, 0x70, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x55, 0x6e, 0x70, 0x72, + 0x69, 0x6f, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x63, 0x74, 0x55, 0x6e, + 0x70, 0x72, 0x69, 0x6f, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, + 0x44, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, + 0x44, 0x1a, 0x5e, 0x0a, 0x0e, 0x50, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x02, 0x69, - 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65, 0x64, 0x5f, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x69, - 0x65, 0x64, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x2a, 0x45, - 0x0a, 0x12, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x00, 0x12, 0x0d, - 0x0a, 0x09, 0x53, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x01, 0x12, 0x0b, 0x0a, - 0x07, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, - 0x67, 0x65, 0x78, 0x10, 0x03, 0x2a, 0x42, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, - 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, - 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x34, 0x10, 0x01, 0x12, - 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x36, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, - 0x55, 0x53, 0x45, 0x5f, 0x53, 0x59, 0x53, 0x10, 0x03, 0x42, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6d, - 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x50, 0x01, 0x5a, - 0x21, 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, 0x61, 0x70, 0x70, 0x2f, 0x64, - 0x6e, 0x73, 0xaa, 0x02, 0x0c, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x44, 0x6e, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x1a, 0x36, 0x0a, 0x0c, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6c, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x64, 0x69, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, 0x65, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x22, 0x98, 0x05, + 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x39, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, + 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x4e, 0x61, 0x6d, + 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x70, + 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, + 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x48, 0x6f, 0x73, + 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, + 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x22, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x64, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x53, 0x74, 0x61, 0x6c, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x54, 0x54, 0x4c, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, + 0x65, 0x64, 0x54, 0x54, 0x4c, 0x12, 0x42, 0x0a, 0x0e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, + 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, + 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, 0x62, + 0x61, 0x63, 0x6b, 0x12, 0x36, 0x0a, 0x16, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, + 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x49, 0x66, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x16, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x6c, 0x6c, + 0x62, 0x61, 0x63, 0x6b, 0x49, 0x66, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x30, 0x0a, 0x13, 0x65, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x50, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x1a, 0x92, 0x01, + 0x0a, 0x0b, 0x48, 0x6f, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, + 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x70, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x70, + 0x72, 0x6f, 0x78, 0x69, 0x65, 0x64, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x69, 0x65, 0x64, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x2a, 0x45, 0x0a, 0x12, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, + 0x0a, 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x77, 0x6f, + 0x72, 0x64, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x03, 0x2a, + 0x42, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, + 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, + 0x55, 0x53, 0x45, 0x5f, 0x49, 0x50, 0x34, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, + 0x5f, 0x49, 0x50, 0x36, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x53, 0x45, 0x5f, 0x53, 0x59, + 0x53, 0x10, 0x03, 0x42, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x50, 0x01, 0x5a, 0x21, 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, 0x61, 0x70, 0x70, 0x2f, 0x64, 0x6e, 0x73, 0xaa, 0x02, 0x0c, 0x58, + 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x44, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/app/dns/config.proto b/app/dns/config.proto index f1b47bc0..3ce312bc 100644 --- a/app/dns/config.proto +++ b/app/dns/config.proto @@ -31,8 +31,8 @@ message NameServer { bool actPrior = 8; string tag = 9; uint64 timeoutMs = 10; - bool disableCache = 11; - bool serveStale = 15; + optional bool disableCache = 11; + optional bool serveStale = 15; optional uint32 serveExpiredTTL = 16; bool finalQuery = 12; repeated xray.app.router.GeoIP unexpected_geoip = 13; diff --git a/app/dns/dns.go b/app/dns/dns.go index 9233d905..79f9e8e1 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -121,8 +121,16 @@ func New(ctx context.Context, config *Config) (*DNS, error) { myClientIP = net.IP(ns.ClientIp) } - disableCache := config.DisableCache || ns.DisableCache - serveStale := config.ServeStale || ns.ServeStale + disableCache := config.DisableCache + if ns.DisableCache != nil { + disableCache = *ns.DisableCache + } + + serveStale := config.ServeStale + if ns.ServeStale != nil { + serveStale = *ns.ServeStale + } + serveExpiredTTL := config.ServeExpiredTTL if ns.ServeExpiredTTL != nil { serveExpiredTTL = *ns.ServeExpiredTTL diff --git a/infra/conf/dns.go b/infra/conf/dns.go index 7c94a488..a65f0ee8 100644 --- a/infra/conf/dns.go +++ b/infra/conf/dns.go @@ -26,8 +26,8 @@ type NameServerConfig struct { QueryStrategy string `json:"queryStrategy"` Tag string `json:"tag"` TimeoutMs uint64 `json:"timeoutMs"` - DisableCache bool `json:"disableCache"` - ServeStale bool `json:"serveStale"` + DisableCache *bool `json:"disableCache"` + ServeStale *bool `json:"serveStale"` ServeExpiredTTL *uint32 `json:"serveExpiredTTL"` FinalQuery bool `json:"finalQuery"` UnexpectedIPs StringList `json:"unexpectedIPs"` @@ -52,8 +52,8 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error { QueryStrategy string `json:"queryStrategy"` Tag string `json:"tag"` TimeoutMs uint64 `json:"timeoutMs"` - DisableCache bool `json:"disableCache"` - ServeStale bool `json:"serveStale"` + DisableCache *bool `json:"disableCache"` + ServeStale *bool `json:"serveStale"` ServeExpiredTTL *uint32 `json:"serveExpiredTTL"` FinalQuery bool `json:"finalQuery"` UnexpectedIPs StringList `json:"unexpectedIPs"` diff --git a/infra/conf/dns_test.go b/infra/conf/dns_test.go index af51b734..a9739668 100644 --- a/infra/conf/dns_test.go +++ b/infra/conf/dns_test.go @@ -20,6 +20,7 @@ func TestDNSConfigParsing(t *testing.T) { return config.Build() } } + expectedServeStale := true expectedServeExpiredTTL := uint32(172800) runMultiTestCase(t, []TestCase{ { @@ -72,7 +73,7 @@ func TestDNSConfigParsing(t *testing.T) { Size: 1, }, }, - ServeStale: true, + ServeStale: &expectedServeStale, ServeExpiredTTL: &expectedServeExpiredTTL, PolicyID: 1, // Servers with certain identical fields share this ID, incrementing starting from 1. See: Build PolicyID }, From c6afcd5fb676e97f26b07ccd3f84ef3eac197cf4 Mon Sep 17 00:00:00 2001 From: yuhan6665 <1588741+yuhan6665@users.noreply.github.com> Date: Sun, 30 Nov 2025 05:28:01 -0500 Subject: [PATCH 046/136] XTLS Vision: Check TLS record isComplete (#5179) Fixes https://github.com/XTLS/Xray-core/discussions/5169#discussioncomment-14482684 --- proxy/proxy.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/proxy/proxy.go b/proxy/proxy.go index f759fdc0..9f965e3e 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -350,10 +350,11 @@ func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { mb[0] = XtlsPadding(nil, CommandPaddingContinue, &w.writeOnceUserUUID, true, w.ctx) // we do a long padding to hide vless header return w.Writer.WriteMultiBuffer(mb) } + isComplete := IsCompleteRecord(mb) mb = ReshapeMultiBuffer(w.ctx, mb) longPadding := w.trafficState.IsTLS for i, b := range mb { - if w.trafficState.IsTLS && b.Len() >= 6 && bytes.Equal(TlsApplicationDataStart, b.BytesTo(3)) { + if w.trafficState.IsTLS && b.Len() >= 6 && bytes.Equal(TlsApplicationDataStart, b.BytesTo(3)) && isComplete { if w.trafficState.EnableXtls { *switchToDirectCopy = true } @@ -386,6 +387,71 @@ func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { return w.Writer.WriteMultiBuffer(mb) } +// IsCompleteRecord Is complete tls data record +func IsCompleteRecord(buffer buf.MultiBuffer) bool { + mb2 := make(buf.MultiBuffer, 0, len(buffer)) + for _, buffer1 := range buffer { + buffer2 := buf.New() + buffer2.Write(buffer1.Bytes()) + mb2 = append(mb2, buffer2) + } + isComplete := true + var headerLen int32 = 5 + var recordLen int32 + for _, buffer2 := range mb2 { + for buffer2.Len() > 0 { + if headerLen > 0 { + data, _ := buffer2.ReadByte() + switch headerLen { + case 5: + if data != 0x17 { + isComplete = false + break + } + case 4: + if data != 0x03 { + isComplete = false + break + } + case 3: + if data != 0x03 { + isComplete = false + break + } + case 2: + recordLen = int32(data) << 8 + case 1: + recordLen = recordLen | int32(data) + } + headerLen-- + } else if recordLen > 0 { + var len = recordLen + if buffer2.Len() < recordLen{ + len = buffer2.Len() + } + buffer2.Advance(len) + recordLen -= len + if recordLen == 0 { + headerLen = 5 + } + } else { + isComplete = false + } + } + if !isComplete { + break + } + } + for _, buffer2 := range mb2 { + buffer2.Release() + buffer2 = nil + } + if headerLen == 5 && recordLen == 0 && isComplete { + return true + } + return false +} + // ReshapeMultiBuffer prepare multi buffer for padding structure (max 21 bytes) func ReshapeMultiBuffer(ctx context.Context, buffer buf.MultiBuffer) buf.MultiBuffer { needReshape := 0 From cadcb4707439eff9358ac50bc43645caf6b07c9c Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:10:54 +0000 Subject: [PATCH 047/136] XTLS Vision: Add `testpre` (outbound pre-connect) and `testseed` (outbound & inbound) (#5270) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://t.me/projectXtls/1034 --------- Co-authored-by: 风扇滑翔翼 --- app/proxyman/outbound/handler.go | 8 +++-- infra/conf/vless.go | 9 ++++++ proxy/proxy.go | 26 +++++++++------ proxy/vless/account.go | 7 ++++ proxy/vless/account.pb.go | 35 +++++++++++++++----- proxy/vless/account.proto | 3 ++ proxy/vless/encoding/addons.go | 2 +- proxy/vless/outbound/outbound.go | 55 +++++++++++++++++++++++++++----- 8 files changed, 116 insertions(+), 29 deletions(-) diff --git a/app/proxyman/outbound/handler.go b/app/proxyman/outbound/handler.go index 3c3c6918..eaa1b0b2 100644 --- a/app/proxyman/outbound/handler.go +++ b/app/proxyman/outbound/handler.go @@ -317,8 +317,12 @@ func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connecti conn, err := internet.Dial(ctx, dest, h.streamSettings) conn = h.getStatCouterConnection(conn) outbounds := session.OutboundsFromContext(ctx) - ob := outbounds[len(outbounds)-1] - ob.Conn = conn + if outbounds != nil { + ob := outbounds[len(outbounds)-1] + ob.Conn = conn + } else { + // for Vision's pre-connect + } return conn, err } diff --git a/infra/conf/vless.go b/infra/conf/vless.go index efb480a0..f482c4d8 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -34,6 +34,7 @@ type VLessInboundConfig struct { Decryption string `json:"decryption"` Fallbacks []*VLessInboundFallback `json:"fallbacks"` Flow string `json:"flow"` + Testseed []uint32 `json:"testseed"` } // Build implements Buildable @@ -73,6 +74,10 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { return nil, errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`) } + if len(account.Testseed) < 4 { + account.Testseed = c.Testseed + } + if account.Encryption != "" { return nil, errors.New(`VLESS clients: "encryption" should not be in inbound settings`) } @@ -212,6 +217,8 @@ type VLessOutboundConfig struct { Seed string `json:"seed"` Encryption string `json:"encryption"` Reverse *vless.Reverse `json:"reverse"` + Testpre uint32 `json:"testpre"` + Testseed []uint32 `json:"testseed"` Vnext []*VLessOutboundVnext `json:"vnext"` } @@ -258,6 +265,8 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { //account.Seed = c.Seed account.Encryption = c.Encryption account.Reverse = c.Reverse + account.Testpre = c.Testpre + account.Testseed = c.Testseed } else { if err := json.Unmarshal(rawUser, account); err != nil { return nil, errors.New(`VLESS users: invalid user`).Base(err) diff --git a/proxy/proxy.go b/proxy/proxy.go index 9f965e3e..aa57adae 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -296,11 +296,16 @@ type VisionWriter struct { // internal writeOnceUserUUID []byte directWriteCounter stats.Counter + + testseed []uint32 } -func NewVisionWriter(writer buf.Writer, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, ob *session.Outbound) *VisionWriter { +func NewVisionWriter(writer buf.Writer, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, ob *session.Outbound, testseed []uint32) *VisionWriter { w := make([]byte, len(trafficState.UserUUID)) copy(w, trafficState.UserUUID) + if len(testseed) < 4 { + testseed = []uint32{900, 500, 900, 256} + } return &VisionWriter{ Writer: writer, trafficState: trafficState, @@ -309,6 +314,7 @@ func NewVisionWriter(writer buf.Writer, trafficState *TrafficState, isUplink boo isUplink: isUplink, conn: conn, ob: ob, + testseed: testseed, } } @@ -347,7 +353,7 @@ func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { if *isPadding { if len(mb) == 1 && mb[0] == nil { - mb[0] = XtlsPadding(nil, CommandPaddingContinue, &w.writeOnceUserUUID, true, w.ctx) // we do a long padding to hide vless header + mb[0] = XtlsPadding(nil, CommandPaddingContinue, &w.writeOnceUserUUID, true, w.ctx, w.testseed) // we do a long padding to hide vless header return w.Writer.WriteMultiBuffer(mb) } isComplete := IsCompleteRecord(mb) @@ -365,13 +371,13 @@ func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { command = CommandPaddingDirect } } - mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, true, w.ctx) + mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, true, w.ctx, w.testseed) *isPadding = false // padding going to end longPadding = false continue } else if !w.trafficState.IsTLS12orAbove && w.trafficState.NumberOfPacketToFilter <= 1 { // For compatibility with earlier vision receiver, we finish padding 1 packet early *isPadding = false - mb[i] = XtlsPadding(b, CommandPaddingEnd, &w.writeOnceUserUUID, longPadding, w.ctx) + mb[i] = XtlsPadding(b, CommandPaddingEnd, &w.writeOnceUserUUID, longPadding, w.ctx, w.testseed) break } var command byte = CommandPaddingContinue @@ -381,7 +387,7 @@ func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { command = CommandPaddingDirect } } - mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, longPadding, w.ctx) + mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, longPadding, w.ctx, w.testseed) } } return w.Writer.WriteMultiBuffer(mb) @@ -488,20 +494,20 @@ func ReshapeMultiBuffer(ctx context.Context, buffer buf.MultiBuffer) buf.MultiBu } // XtlsPadding add padding to eliminate length signature during tls handshake -func XtlsPadding(b *buf.Buffer, command byte, userUUID *[]byte, longPadding bool, ctx context.Context) *buf.Buffer { +func XtlsPadding(b *buf.Buffer, command byte, userUUID *[]byte, longPadding bool, ctx context.Context, testseed []uint32) *buf.Buffer { var contentLen int32 = 0 var paddingLen int32 = 0 if b != nil { contentLen = b.Len() } - if contentLen < 900 && longPadding { - l, err := rand.Int(rand.Reader, big.NewInt(500)) + if contentLen < int32(testseed[0]) && longPadding { + l, err := rand.Int(rand.Reader, big.NewInt(int64(testseed[1]))) if err != nil { errors.LogDebugInner(ctx, err, "failed to generate padding") } - paddingLen = int32(l.Int64()) + 900 - contentLen + paddingLen = int32(l.Int64()) + int32(testseed[2]) - contentLen } else { - l, err := rand.Int(rand.Reader, big.NewInt(256)) + l, err := rand.Int(rand.Reader, big.NewInt(int64(testseed[3]))) if err != nil { errors.LogDebugInner(ctx, err, "failed to generate padding") } diff --git a/proxy/vless/account.go b/proxy/vless/account.go index ac00ea53..2f617149 100644 --- a/proxy/vless/account.go +++ b/proxy/vless/account.go @@ -22,6 +22,8 @@ func (a *Account) AsAccount() (protocol.Account, error) { Seconds: a.Seconds, Padding: a.Padding, Reverse: a.Reverse, + Testpre: a.Testpre, + Testseed: a.Testseed, }, nil } @@ -38,6 +40,9 @@ type MemoryAccount struct { Padding string Reverse *Reverse + + Testpre uint32 + Testseed []uint32 } // Equals implements protocol.Account.Equals(). @@ -58,5 +63,7 @@ func (a *MemoryAccount) ToProto() proto.Message { Seconds: a.Seconds, Padding: a.Padding, Reverse: a.Reverse, + Testpre: a.Testpre, + Testseed: a.Testseed, } } diff --git a/proxy/vless/account.pb.go b/proxy/vless/account.pb.go index 5822f512..ce01ecae 100644 --- a/proxy/vless/account.pb.go +++ b/proxy/vless/account.pb.go @@ -79,6 +79,8 @@ type Account struct { Seconds uint32 `protobuf:"varint,5,opt,name=seconds,proto3" json:"seconds,omitempty"` Padding string `protobuf:"bytes,6,opt,name=padding,proto3" json:"padding,omitempty"` Reverse *Reverse `protobuf:"bytes,7,opt,name=reverse,proto3" json:"reverse,omitempty"` + Testpre uint32 `protobuf:"varint,8,opt,name=testpre,proto3" json:"testpre,omitempty"` + Testseed []uint32 `protobuf:"varint,9,rep,packed,name=testseed,proto3" json:"testseed,omitempty"` } func (x *Account) Reset() { @@ -160,6 +162,20 @@ func (x *Account) GetReverse() *Reverse { return nil } +func (x *Account) GetTestpre() uint32 { + if x != nil { + return x.Testpre + } + return 0 +} + +func (x *Account) GetTestseed() []uint32 { + if x != nil { + return x.Testseed + } + return nil +} + var File_proxy_vless_account_proto protoreflect.FileDescriptor var file_proxy_vless_account_proto_rawDesc = []byte{ @@ -167,7 +183,7 @@ var file_proxy_vless_account_proto_rawDesc = []byte{ 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x1b, 0x0a, 0x07, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x22, 0xd0, 0x01, 0x0a, 0x07, 0x41, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x22, 0x86, 0x02, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x6e, @@ -180,13 +196,16 @@ var file_proxy_vless_account_proto_rawDesc = []byte{ 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x33, 0x0a, 0x07, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x2e, 0x52, 0x65, 0x76, - 0x65, 0x72, 0x73, 0x65, 0x52, 0x07, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x42, 0x52, 0x0a, - 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, - 0x76, 0x6c, 0x65, 0x73, 0x73, 0x50, 0x01, 0x5a, 0x25, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, 0x6c, 0x65, 0x73, 0x73, 0xaa, 0x02, - 0x10, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x72, 0x73, 0x65, 0x52, 0x07, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, + 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x73, 0x74, 0x73, + 0x65, 0x65, 0x64, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x08, 0x74, 0x65, 0x73, 0x74, 0x73, + 0x65, 0x65, 0x64, 0x42, 0x52, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, + 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x50, 0x01, 0x5a, 0x25, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x76, + 0x6c, 0x65, 0x73, 0x73, 0xaa, 0x02, 0x10, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, + 0x79, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proxy/vless/account.proto b/proxy/vless/account.proto index 047311dd..d0f1ee22 100644 --- a/proxy/vless/account.proto +++ b/proxy/vless/account.proto @@ -22,4 +22,7 @@ message Account { string padding = 6; Reverse reverse = 7; + + uint32 testpre = 8; + repeated uint32 testseed = 9; } diff --git a/proxy/vless/encoding/addons.go b/proxy/vless/encoding/addons.go index 77b09861..724de777 100644 --- a/proxy/vless/encoding/addons.go +++ b/proxy/vless/encoding/addons.go @@ -68,7 +68,7 @@ func EncodeBodyAddons(writer buf.Writer, request *protocol.RequestHeader, reques return NewMultiLengthPacketWriter(writer) } if requestAddons.Flow == vless.XRV { - return proxy.NewVisionWriter(writer, state, isUplink, context, conn, ob) + return proxy.NewVisionWriter(writer, state, isUplink, context, conn, ob, request.User.Account.(*vless.MemoryAccount).Testseed) } return writer } diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index c425151f..504b2126 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "reflect" "strings" + "sync" "time" "unsafe" @@ -15,6 +16,7 @@ import ( "github.com/xtls/xray-core/app/reverse" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" + xctx "github.com/xtls/xray-core/common/ctx" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/mux" "github.com/xtls/xray-core/common/net" @@ -52,6 +54,10 @@ type Handler struct { cone bool encryption *encryption.ClientInstance reverse *Reverse + + testpre uint32 + initpre sync.Once + preConns chan stat.Connection } // New creates a new VLess outbound handler. @@ -105,11 +111,16 @@ func New(ctx context.Context, config *Config) (*Handler, error) { }() } + handler.testpre = a.Testpre + return handler, nil } // Close implements common.Closable.Close(). func (h *Handler) Close() error { + if h.preConns != nil { + close(h.preConns) + } if h.reverse != nil { return h.reverse.Close() } @@ -128,18 +139,46 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte rec := h.server var conn stat.Connection - if err := retry.ExponentialBackoff(5, 200).On(func() error { - var err error - conn, err = dialer.Dial(ctx, rec.Destination) - if err != nil { - return err + if h.testpre > 0 && h.reverse == nil { + h.initpre.Do(func() { + h.preConns = make(chan stat.Connection) + for range h.testpre { // TODO: randomize + go func() { + defer func() { recover() }() + ctx := xctx.ContextWithID(context.Background(), session.NewID()) + for { + time.Sleep(time.Millisecond * 200) // TODO: randomize + conn, err := dialer.Dial(ctx, rec.Destination) + if err != nil { + errors.LogWarningInner(ctx, err, "pre-connect failed") + continue + } + h.preConns <- conn + } + }() + } + }) + if conn = <-h.preConns; conn == nil { + return errors.New("closed handler").AtWarning() + } + } + + if conn == nil { + if err := retry.ExponentialBackoff(5, 200).On(func() error { + var err error + conn, err = dialer.Dial(ctx, rec.Destination) + if err != nil { + return err + } + return nil + }); err != nil { + return errors.New("failed to find an available destination").Base(err).AtWarning() } - return nil - }); err != nil { - return errors.New("failed to find an available destination").Base(err).AtWarning() } defer conn.Close() + ob.Conn = conn // for Vision's pre-connect + iConn := conn if statConn, ok := iConn.(*stat.CounterConnection); ok { iConn = statConn.Connection From 36cb0f00bd06212feeb73c3f3e47c3806849ef63 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:15:01 +0000 Subject: [PATCH 048/136] v25.12.1 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 --- core/core.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/core.go b/core/core.go index 42197697..1d40a5d2 100644 --- a/core/core.go +++ b/core/core.go @@ -18,8 +18,8 @@ import ( var ( Version_x byte = 25 - Version_y byte = 10 - Version_z byte = 15 + Version_y byte = 12 + Version_z byte = 1 ) var ( From 93312d29e58c4c676906495c4402e98894e08a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Tue, 2 Dec 2025 21:01:44 +0800 Subject: [PATCH 049/136] XTLS Vision: Fix IsCompleteRecord() (#5365) Fixes https://github.com/XTLS/Xray-core/pull/5179 --- proxy/proxy.go | 93 ++++++++++++++++++++++---------------------------- 1 file changed, 41 insertions(+), 52 deletions(-) diff --git a/proxy/proxy.go b/proxy/proxy.go index aa57adae..7bb3f79e 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -395,65 +395,54 @@ func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { // IsCompleteRecord Is complete tls data record func IsCompleteRecord(buffer buf.MultiBuffer) bool { - mb2 := make(buf.MultiBuffer, 0, len(buffer)) - for _, buffer1 := range buffer { - buffer2 := buf.New() - buffer2.Write(buffer1.Bytes()) - mb2 = append(mb2, buffer2) + b := make([]byte, buffer.Len()) + if buffer.Copy(b) != int(buffer.Len()) { + panic("impossible bytes allocation") } - isComplete := true - var headerLen int32 = 5 - var recordLen int32 - for _, buffer2 := range mb2 { - for buffer2.Len() > 0 { - if headerLen > 0 { - data, _ := buffer2.ReadByte() - switch headerLen { - case 5: - if data != 0x17 { - isComplete = false - break - } - case 4: - if data != 0x03 { - isComplete = false - break - } - case 3: - if data != 0x03 { - isComplete = false - break - } - case 2: - recordLen = int32(data) << 8 - case 1: - recordLen = recordLen | int32(data) + var headerLen int = 5 + var recordLen int + + totalLen := len(b) + i := 0 + for i < totalLen { + // record header: 0x17 0x3 0x3 + 2 bytes length + if headerLen > 0 { + data := b[i] + i++ + switch headerLen { + case 5: + if data != 0x17 { + return false } - headerLen-- - } else if recordLen > 0 { - var len = recordLen - if buffer2.Len() < recordLen{ - len = buffer2.Len() + case 4: + if data != 0x03 { + return false } - buffer2.Advance(len) - recordLen -= len - if recordLen == 0 { - headerLen = 5 + case 3: + if data != 0x03 { + return false } - } else { - isComplete = false + case 2: + recordLen = int(data) << 8 + case 1: + recordLen = recordLen | int(data) } - } - if !isComplete { - break + headerLen-- + } else if recordLen > 0 { + remaining := totalLen - i + if remaining < recordLen { + return false + } else { + i += recordLen + recordLen = 0 + headerLen = 5 + } + } else { + return false } } - for _, buffer2 := range mb2 { - buffer2.Release() - buffer2 = nil - } - if headerLen == 5 && recordLen == 0 && isComplete { - return true + if headerLen == 5 && recordLen == 0 { + return true } return false } From c123f163c20d5f54cb92fc1421c844955cddbaaf Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:28:00 +0000 Subject: [PATCH 050/136] XTLS Vision: Discard expired pre-connect conn automatically https://t.me/projectXray/4538408 https://github.com/XTLS/Xray-core/pull/5270#issuecomment-3602122299 --- proxy/vless/outbound/outbound.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 504b2126..17e82210 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -57,7 +57,12 @@ type Handler struct { testpre uint32 initpre sync.Once - preConns chan stat.Connection + preConns chan *ConnExpire +} + +type ConnExpire struct { + Conn stat.Connection + Expire time.Time } // New creates a new VLess outbound handler. @@ -141,25 +146,33 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte if h.testpre > 0 && h.reverse == nil { h.initpre.Do(func() { - h.preConns = make(chan stat.Connection) + h.preConns = make(chan *ConnExpire) for range h.testpre { // TODO: randomize go func() { defer func() { recover() }() ctx := xctx.ContextWithID(context.Background(), session.NewID()) for { - time.Sleep(time.Millisecond * 200) // TODO: randomize conn, err := dialer.Dial(ctx, rec.Destination) if err != nil { errors.LogWarningInner(ctx, err, "pre-connect failed") continue } - h.preConns <- conn + h.preConns <- &ConnExpire{Conn: conn, Expire: time.Now().Add(time.Minute * 2)} // TODO: customize & randomize + time.Sleep(time.Millisecond * 200) // TODO: customize & randomize } }() } }) - if conn = <-h.preConns; conn == nil { - return errors.New("closed handler").AtWarning() + for { + connTime := <-h.preConns + if connTime == nil { + return errors.New("closed handler").AtWarning() + } + if time.Now().Before(connTime.Expire) { + conn = connTime.Conn + break + } + connTime.Conn.Close() } } From e403abe360ffd5bd6e44e0bb362fb4d1a84d7c0b Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:53:08 +0000 Subject: [PATCH 051/136] v25.12.2 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 --- core/core.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/core.go b/core/core.go index 1d40a5d2..6adc82bb 100644 --- a/core/core.go +++ b/core/core.go @@ -19,7 +19,7 @@ import ( var ( Version_x byte = 25 Version_y byte = 12 - Version_z byte = 1 + Version_z byte = 2 ) var ( From 903214a0f034911c5fedc2c50a9b52d7747818c4 Mon Sep 17 00:00:00 2001 From: yuhan6665 <1588741+yuhan6665@users.noreply.github.com> Date: Mon, 8 Dec 2025 08:13:43 -0500 Subject: [PATCH 052/136] XTLS Vision: Fix enabled uplink splice flag by mistake (#5391) Fixes https://github.com/XTLS/Xray-core/issues/5379 --- proxy/proxy.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/proxy/proxy.go b/proxy/proxy.go index 7bb3f79e..6f942cde 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -269,9 +269,9 @@ func (w *VisionReader) ReadMultiBuffer() (buf.MultiBuffer, error) { w.rawInput = nil if inbound := session.InboundFromContext(w.ctx); inbound != nil && inbound.Conn != nil { - if w.isUplink && inbound.CanSpliceCopy == 2 { - inbound.CanSpliceCopy = 1 - } + // if w.isUplink && inbound.CanSpliceCopy == 2 { // TODO: enable uplink splice + // inbound.CanSpliceCopy = 1 + // } if !w.isUplink && w.ob != nil && w.ob.CanSpliceCopy == 2 { // ob need to be passed in due to context can have more than one ob w.ob.CanSpliceCopy = 1 } @@ -334,9 +334,9 @@ func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { if !w.isUplink && inbound.CanSpliceCopy == 2 { inbound.CanSpliceCopy = 1 } - if w.isUplink && w.ob != nil && w.ob.CanSpliceCopy == 2 { - w.ob.CanSpliceCopy = 1 - } + // if w.isUplink && w.ob != nil && w.ob.CanSpliceCopy == 2 { // TODO: enable uplink splice + // w.ob.CanSpliceCopy = 1 + // } } rawConn, _, writerCounter := UnwrapRawConn(w.conn) w.Writer = buf.NewWriter(rawConn) From bd7503d506b07252e9c6ae48ef084be7b95fb1c3 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:19:59 +0000 Subject: [PATCH 053/136] XTLS Vision: LogInfo() -> LogDebug() https://t.me/projectXray/4543105 --- proxy/proxy.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/proxy/proxy.go b/proxy/proxy.go index 6f942cde..d6a797cb 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -248,7 +248,7 @@ func (w *VisionReader) ReadMultiBuffer() (buf.MultiBuffer, error) { *withinPaddingBuffers = false *switchToDirectCopy = true } else { - errors.LogInfo(w.ctx, "XtlsRead unknown command ", *currentCommand, buffer.Len()) + errors.LogDebug(w.ctx, "XtlsRead unknown command ", *currentCommand, buffer.Len()) } } if w.trafficState.NumberOfPacketToFilter > 0 { @@ -478,7 +478,7 @@ func ReshapeMultiBuffer(ctx context.Context, buffer buf.MultiBuffer) buf.MultiBu buffer[i] = nil } buffer = buffer[:0] - errors.LogInfo(ctx, "ReshapeMultiBuffer ", toPrint) + errors.LogDebug(ctx, "ReshapeMultiBuffer ", toPrint) return mb2 } @@ -517,7 +517,7 @@ func XtlsPadding(b *buf.Buffer, command byte, userUUID *[]byte, longPadding bool b = nil } newbuffer.Extend(paddingLen) - errors.LogInfo(ctx, "XtlsPadding ", contentLen, " ", paddingLen, " ", command) + errors.LogDebug(ctx, "XtlsPadding ", contentLen, " ", paddingLen, " ", command) return newbuffer } @@ -564,7 +564,7 @@ func XtlsUnpadding(b *buf.Buffer, s *TrafficState, isUplink bool, ctx context.Co *remainingPadding = int32(data) << 8 case 1: *remainingPadding = *remainingPadding | int32(data) - errors.LogInfo(ctx, "Xtls Unpadding new block, content ", *remainingContent, " padding ", *remainingPadding, " command ", *currentCommand) + errors.LogDebug(ctx, "Xtls Unpadding new block, content ", *remainingContent, " padding ", *remainingPadding, " command ", *currentCommand) } *remainingCommand-- } else if *remainingContent > 0 { @@ -623,11 +623,11 @@ func XtlsFilterTls(buffer buf.MultiBuffer, trafficState *TrafficState, ctx conte cipherSuite := b.BytesRange(43+sessionIdLen+1, 43+sessionIdLen+3) trafficState.Cipher = uint16(cipherSuite[0])<<8 | uint16(cipherSuite[1]) } else { - errors.LogInfo(ctx, "XtlsFilterTls short server hello, tls 1.2 or older? ", b.Len(), " ", trafficState.RemainingServerHello) + errors.LogDebug(ctx, "XtlsFilterTls short server hello, tls 1.2 or older? ", b.Len(), " ", trafficState.RemainingServerHello) } } else if bytes.Equal(TlsClientHandShakeStart, startsBytes[:2]) && startsBytes[5] == TlsHandshakeTypeClientHello { trafficState.IsTLS = true - errors.LogInfo(ctx, "XtlsFilterTls found tls client hello! ", buffer.Len()) + errors.LogDebug(ctx, "XtlsFilterTls found tls client hello! ", buffer.Len()) } } if trafficState.RemainingServerHello > 0 { @@ -643,18 +643,18 @@ func XtlsFilterTls(buffer buf.MultiBuffer, trafficState *TrafficState, ctx conte } else if v != "TLS_AES_128_CCM_8_SHA256" { trafficState.EnableXtls = true } - errors.LogInfo(ctx, "XtlsFilterTls found tls 1.3! ", b.Len(), " ", v) + errors.LogDebug(ctx, "XtlsFilterTls found tls 1.3! ", b.Len(), " ", v) trafficState.NumberOfPacketToFilter = 0 return } else if trafficState.RemainingServerHello <= 0 { - errors.LogInfo(ctx, "XtlsFilterTls found tls 1.2! ", b.Len()) + errors.LogDebug(ctx, "XtlsFilterTls found tls 1.2! ", b.Len()) trafficState.NumberOfPacketToFilter = 0 return } - errors.LogInfo(ctx, "XtlsFilterTls inconclusive server hello ", b.Len(), " ", trafficState.RemainingServerHello) + errors.LogDebug(ctx, "XtlsFilterTls inconclusive server hello ", b.Len(), " ", trafficState.RemainingServerHello) } if trafficState.NumberOfPacketToFilter <= 0 { - errors.LogInfo(ctx, "XtlsFilterTls stop filtering", buffer.Len()) + errors.LogDebug(ctx, "XtlsFilterTls stop filtering", buffer.Len()) } } } @@ -736,7 +736,7 @@ func CopyRawConnIfExist(ctx context.Context, readerConn net.Conn, writerConn net } } if splice { - errors.LogInfo(ctx, "CopyRawConn splice") + errors.LogDebug(ctx, "CopyRawConn splice") statWriter, _ := writer.(*dispatcher.SizeStatWriter) //runtime.Gosched() // necessary time.Sleep(time.Millisecond) // without this, there will be a rare ssl error for freedom splice @@ -779,7 +779,7 @@ func CopyRawConnIfExist(ctx context.Context, readerConn net.Conn, writerConn net } func readV(ctx context.Context, reader buf.Reader, writer buf.Writer, timer signal.ActivityUpdater, readCounter stats.Counter) error { - errors.LogInfo(ctx, "CopyRawConn (maybe) readv") + errors.LogDebug(ctx, "CopyRawConn (maybe) readv") if err := buf.Copy(reader, writer, buf.UpdateActivity(timer), buf.AddToStatCounter(readCounter)); err != nil { return errors.New("failed to process response").Base(err) } From aea123842b37b3bba0d4b130f4f8cbb6410a40b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=90=B2=93=F0=90=B3=9B=F0=90=B3=AA=F0=90=B3=82?= =?UTF-8?q?=F0=90=B3=90=20=F0=90=B2=80=F0=90=B3=A2=F0=90=B3=A6=F0=90=B3=AB?= =?UTF-8?q?=F0=90=B3=A2=20=F0=90=B2=A5=F0=90=B3=94=F0=90=B3=9B=F0=90=B3=AA?= =?UTF-8?q?=F0=90=B3=8C=F0=90=B3=91=F0=90=B3=96=F0=90=B3=87?= <26771058+KobeArthurScofield@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:27:22 +0800 Subject: [PATCH 054/136] Chore: Remove ctlcmd and leftover envvar (#5392) https://github.com/v2fly/v2ray-core/issues/360 --- common/platform/ctlcmd/attr_other.go | 10 ------ common/platform/ctlcmd/attr_windows.go | 12 ------- common/platform/ctlcmd/ctlcmd.go | 50 -------------------------- common/platform/others.go | 9 ----- common/platform/platform.go | 13 ------- common/platform/windows.go | 10 ------ core/config.go | 6 ++-- infra/conf/xray.go | 4 --- main/commands/all/convert/protobuf.go | 11 +----- main/confloader/confloader.go | 12 ------- main/confloader/external/external.go | 11 ------ 11 files changed, 4 insertions(+), 144 deletions(-) delete mode 100644 common/platform/ctlcmd/attr_other.go delete mode 100644 common/platform/ctlcmd/attr_windows.go delete mode 100644 common/platform/ctlcmd/ctlcmd.go diff --git a/common/platform/ctlcmd/attr_other.go b/common/platform/ctlcmd/attr_other.go deleted file mode 100644 index 3e1bc265..00000000 --- a/common/platform/ctlcmd/attr_other.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !windows -// +build !windows - -package ctlcmd - -import "syscall" - -func getSysProcAttr() *syscall.SysProcAttr { - return nil -} diff --git a/common/platform/ctlcmd/attr_windows.go b/common/platform/ctlcmd/attr_windows.go deleted file mode 100644 index ab8ac064..00000000 --- a/common/platform/ctlcmd/attr_windows.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build windows -// +build windows - -package ctlcmd - -import "syscall" - -func getSysProcAttr() *syscall.SysProcAttr { - return &syscall.SysProcAttr{ - HideWindow: true, - } -} diff --git a/common/platform/ctlcmd/ctlcmd.go b/common/platform/ctlcmd/ctlcmd.go deleted file mode 100644 index bc28ace1..00000000 --- a/common/platform/ctlcmd/ctlcmd.go +++ /dev/null @@ -1,50 +0,0 @@ -package ctlcmd - -import ( - "context" - "io" - "os" - "os/exec" - "strings" - - "github.com/xtls/xray-core/common/buf" - "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/platform" -) - -func Run(args []string, input io.Reader) (buf.MultiBuffer, error) { - xctl := platform.GetToolLocation("xctl") - if _, err := os.Stat(xctl); err != nil { - return nil, errors.New("xctl doesn't exist").Base(err) - } - - var errBuffer buf.MultiBufferContainer - var outBuffer buf.MultiBufferContainer - - cmd := exec.Command(xctl, args...) - cmd.Stderr = &errBuffer - cmd.Stdout = &outBuffer - cmd.SysProcAttr = getSysProcAttr() - if input != nil { - cmd.Stdin = input - } - - if err := cmd.Start(); err != nil { - return nil, errors.New("failed to start xctl").Base(err) - } - - if err := cmd.Wait(); err != nil { - msg := "failed to execute xctl" - if errBuffer.Len() > 0 { - msg += ": \n" + strings.TrimSpace(errBuffer.MultiBuffer.String()) - } - return nil, errors.New(msg).Base(err) - } - - // log stderr, info message - if !errBuffer.IsEmpty() { - errors.LogInfo(context.Background(), " \n", strings.TrimSpace(errBuffer.MultiBuffer.String())) - } - - return outBuffer.MultiBuffer, nil -} diff --git a/common/platform/others.go b/common/platform/others.go index a405ac48..be86b6fa 100644 --- a/common/platform/others.go +++ b/common/platform/others.go @@ -8,19 +8,10 @@ import ( "path/filepath" ) -func ExpandEnv(s string) string { - return os.ExpandEnv(s) -} - func LineSeparator() string { return "\n" } -func GetToolLocation(file string) string { - toolPath := NewEnvFlag(ToolLocation).GetValue(getExecutableDir) - return filepath.Join(toolPath, file) -} - // GetAssetLocation searches for `file` in the env dir, the executable dir, and certain locations func GetAssetLocation(file string) string { assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) diff --git a/common/platform/platform.go b/common/platform/platform.go index b865dc0d..bc7baf65 100644 --- a/common/platform/platform.go +++ b/common/platform/platform.go @@ -8,10 +8,8 @@ import ( ) const ( - PluginLocation = "xray.location.plugin" ConfigLocation = "xray.location.config" ConfdirLocation = "xray.location.confdir" - ToolLocation = "xray.location.tool" AssetLocation = "xray.location.asset" CertLocation = "xray.location.cert" @@ -79,17 +77,6 @@ func getExecutableDir() string { return filepath.Dir(exec) } -func getExecutableSubDir(dir string) func() string { - return func() string { - return filepath.Join(getExecutableDir(), dir) - } -} - -func GetPluginDirectory() string { - pluginDir := NewEnvFlag(PluginLocation).GetValue(getExecutableSubDir("plugins")) - return pluginDir -} - func GetConfigurationPath() string { configPath := NewEnvFlag(ConfigLocation).GetValue(getExecutableDir) return filepath.Join(configPath, "config.json") diff --git a/common/platform/windows.go b/common/platform/windows.go index cb25a1ad..684ddc9c 100644 --- a/common/platform/windows.go +++ b/common/platform/windows.go @@ -5,20 +5,10 @@ package platform import "path/filepath" -func ExpandEnv(s string) string { - // TODO - return s -} - func LineSeparator() string { return "\r\n" } -func GetToolLocation(file string) string { - toolPath := NewEnvFlag(ToolLocation).GetValue(getExecutableDir) - return filepath.Join(toolPath, file+".exe") -} - // GetAssetLocation searches for `file` in the env dir and the executable dir func GetAssetLocation(file string) string { assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) diff --git a/core/config.go b/core/config.go index ec9e5aa4..8f2018e6 100644 --- a/core/config.go +++ b/core/config.go @@ -64,7 +64,7 @@ func GetMergedConfig(args cmdarg.Arg) (string, error) { var files []*ConfigSource supported := []string{"json", "yaml", "toml"} for _, file := range args { - format := getFormat(file) + format := GetFormat(file) if slices.Contains(supported, format) { files = append(files, &ConfigSource{ Name: file, @@ -98,7 +98,7 @@ func getExtension(filename string) string { return filename[idx+1:] } -func getFormat(filename string) string { +func GetFormat(filename string) string { return GetFormatByExtension(getExtension(filename)) } @@ -112,7 +112,7 @@ func LoadConfig(formatName string, input interface{}) (*Config, error) { if formatName == "auto" { if file != "stdin:" { - f = getFormat(file) + f = GetFormat(file) } else { f = "json" } diff --git a/infra/conf/xray.go b/infra/conf/xray.go index e9cacc87..0dfb2a98 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -3,8 +3,6 @@ package conf import ( "context" "encoding/json" - "log" - "os" "path/filepath" "strings" @@ -47,8 +45,6 @@ var ( "dns": func() interface{} { return new(DNSOutboundConfig) }, "wireguard": func() interface{} { return &WireGuardConfig{IsClient: true} }, }, "protocol", "settings") - - ctllog = log.New(os.Stderr, "xctl> ", 0) ) type SniffingConfig struct { diff --git a/main/commands/all/convert/protobuf.go b/main/commands/all/convert/protobuf.go index 74272c57..2f4296e1 100644 --- a/main/commands/all/convert/protobuf.go +++ b/main/commands/all/convert/protobuf.go @@ -3,7 +3,6 @@ package convert import ( "fmt" "os" - "strings" "github.com/xtls/xray-core/common/cmdarg" creflect "github.com/xtls/xray-core/common/reflect" @@ -61,7 +60,7 @@ func executeConvertConfigsToProtobuf(cmd *base.Command, args []string) { } if len(optFile) > 0 { - switch core.GetFormatByExtension(getFileExtension(optFile)){ + switch core.GetFormat(optFile){ case "protobuf", "": fmt.Println("Output ProtoBuf file is ", optFile) default: @@ -106,11 +105,3 @@ func executeConvertConfigsToProtobuf(cmd *base.Command, args []string) { } } } - -func getFileExtension(filename string) string { - idx := strings.LastIndexByte(filename, '.') - if idx == -1 { - return "" - } - return filename[idx+1:] -} diff --git a/main/confloader/confloader.go b/main/confloader/confloader.go index 315c500e..b9652a67 100644 --- a/main/confloader/confloader.go +++ b/main/confloader/confloader.go @@ -10,12 +10,10 @@ import ( type ( configFileLoader func(string) (io.Reader, error) - extconfigLoader func([]string, io.Reader) (io.Reader, error) ) var ( EffectiveConfigFileLoader configFileLoader - EffectiveExtConfigLoader extconfigLoader ) // LoadConfig reads from a path/url/stdin @@ -27,13 +25,3 @@ func LoadConfig(file string) (io.Reader, error) { } return EffectiveConfigFileLoader(file) } - -// LoadExtConfig calls xctl to handle multiple config -// the actual work also in external module -func LoadExtConfig(files []string, reader io.Reader) (io.Reader, error) { - if EffectiveExtConfigLoader == nil { - return nil, errors.New("external config module not loaded").AtError() - } - - return EffectiveExtConfigLoader(files, reader) -} diff --git a/main/confloader/external/external.go b/main/confloader/external/external.go index 787de985..110b483d 100644 --- a/main/confloader/external/external.go +++ b/main/confloader/external/external.go @@ -13,7 +13,6 @@ import ( "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/platform/ctlcmd" "github.com/xtls/xray-core/main/confloader" ) @@ -129,16 +128,6 @@ func FetchUnixSocketHTTPContent(target string) ([]byte, error) { return content, nil } -func ExtConfigLoader(files []string, reader io.Reader) (io.Reader, error) { - buf, err := ctlcmd.Run(append([]string{"convert"}, files...), reader) - if err != nil { - return nil, err - } - - return strings.NewReader(buf.String()), nil -} - func init() { confloader.EffectiveConfigFileLoader = ConfigLoader - confloader.EffectiveExtConfigLoader = ExtConfigLoader } From 81f8f398c7b2b845853b1e85087c6122acc6db0b Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:35:16 +0000 Subject: [PATCH 055/136] v25.12.8 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 --- core/core.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/core.go b/core/core.go index 6adc82bb..96624d1d 100644 --- a/core/core.go +++ b/core/core.go @@ -19,7 +19,7 @@ import ( var ( Version_x byte = 25 Version_y byte = 12 - Version_z byte = 2 + Version_z byte = 8 ) var ( From b451f8929d1c73b5c952789005ff6f323d8b4bd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 07:16:37 +0000 Subject: [PATCH 056/136] Bump golang.org/x/net from 0.47.0 to 0.48.0 (#5397) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.47.0 to 0.48.0. - [Commits](https://github.com/golang/net/compare/v0.47.0...v0.48.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.48.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 69816eb4..1014ca74 100644 --- a/go.mod +++ b/go.mod @@ -21,10 +21,10 @@ require ( github.com/vishvananda/netlink v1.3.1 github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.44.0 - golang.org/x/net v0.47.0 - golang.org/x/sync v0.18.0 - golang.org/x/sys v0.38.0 + golang.org/x/crypto v0.46.0 + golang.org/x/net v0.48.0 + golang.org/x/sync v0.19.0 + golang.org/x/sys v0.39.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 @@ -46,10 +46,10 @@ require ( github.com/quic-go/qpack v0.6.0 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/vishvananda/netns v0.0.5 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/tools v0.39.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 62af7643..4938d051 100644 --- a/go.sum +++ b/go.sum @@ -95,20 +95,20 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= 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-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -116,21 +116,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From a610a4c89ac723520339dc02043c4c8b08ef2d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Wed, 10 Dec 2025 15:17:29 +0800 Subject: [PATCH 057/136] Chore: Remove all double gonet import (#5402) --- app/dns/fakedns/fake.go | 7 +++---- app/dns/fakedns/fakedns_test.go | 5 ++--- app/proxyman/inbound/worker.go | 5 ++--- app/proxyman/outbound/handler.go | 5 ++--- common/net/system.go | 18 ++++++++++++++++++ proxy/wireguard/bind.go | 19 +++++++++---------- proxy/wireguard/tun.go | 9 ++++----- transport/internet/dialer.go | 5 ++--- transport/internet/grpc/dial.go | 5 ++--- transport/internet/grpc/encoding/hunkconn.go | 5 ++--- transport/internet/sockopt_darwin.go | 5 ++--- .../internet/splithttp/browser_client.go | 6 +++--- transport/internet/splithttp/client.go | 3 +-- transport/internet/system_dialer.go | 3 +-- transport/internet/system_listener.go | 3 +-- transport/internet/websocket/dialer.go | 3 +-- 16 files changed, 55 insertions(+), 51 deletions(-) diff --git a/app/dns/fakedns/fake.go b/app/dns/fakedns/fake.go index 20e4bffa..33bf63cc 100644 --- a/app/dns/fakedns/fake.go +++ b/app/dns/fakedns/fake.go @@ -4,7 +4,6 @@ import ( "context" "math" "math/big" - gonet "net" "sync" "time" @@ -17,7 +16,7 @@ import ( type Holder struct { domainToIP cache.Lru - ipRange *gonet.IPNet + ipRange *net.IPNet mu *sync.Mutex config *FakeDnsPool @@ -79,10 +78,10 @@ func (fkdns *Holder) initializeFromConfig() error { } func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error { - var ipRange *gonet.IPNet + var ipRange *net.IPNet var err error - if _, ipRange, err = gonet.ParseCIDR(ipPoolCidr); err != nil { + if _, ipRange, err = net.ParseCIDR(ipPoolCidr); err != nil { return errors.New("Unable to parse CIDR for Fake DNS IP assignment").Base(err).AtError() } diff --git a/app/dns/fakedns/fakedns_test.go b/app/dns/fakedns/fakedns_test.go index 76ccbca6..f9a8449c 100644 --- a/app/dns/fakedns/fakedns_test.go +++ b/app/dns/fakedns/fakedns_test.go @@ -1,7 +1,6 @@ package fakedns import ( - gonet "net" "strconv" "testing" @@ -155,7 +154,7 @@ func TestFakeDNSMulti(t *testing.T) { assert.True(t, inPool) }) t.Run("ipv6", func(t *testing.T) { - ip, err := gonet.ResolveIPAddr("ip", "fddd:c5b4:ff5f:f4f0::5") + ip, err := net.ResolveIPAddr("ip", "fddd:c5b4:ff5f:f4f0::5") assert.Nil(t, err) inPool := fakeMulti.IsIPInIPPool(net.IPAddress(ip.IP)) assert.True(t, inPool) @@ -165,7 +164,7 @@ func TestFakeDNSMulti(t *testing.T) { assert.False(t, inPool) }) t.Run("ipv6_inverse", func(t *testing.T) { - ip, err := gonet.ResolveIPAddr("ip", "fcdd:c5b4:ff5f:f4f0::5") + ip, err := net.ResolveIPAddr("ip", "fcdd:c5b4:ff5f:f4f0::5") assert.Nil(t, err) inPool := fakeMulti.IsIPInIPPool(net.IPAddress(ip.IP)) assert.False(t, inPool) diff --git a/app/proxyman/inbound/worker.go b/app/proxyman/inbound/worker.go index 42b5c3e9..ef454d21 100644 --- a/app/proxyman/inbound/worker.go +++ b/app/proxyman/inbound/worker.go @@ -2,7 +2,6 @@ package inbound import ( "context" - gonet "net" "sync" "sync/atomic" "time" @@ -565,12 +564,12 @@ func (w *dsWorker) Close() error { } func IsLocal(ip net.IP) bool { - addrs, err := gonet.InterfaceAddrs() + addrs, err := net.InterfaceAddrs() if err != nil { return false } for _, addr := range addrs { - if ipnet, ok := addr.(*gonet.IPNet); ok { + if ipnet, ok := addr.(*net.IPNet); ok { if ipnet.IP.Equal(ip) { return true } diff --git a/app/proxyman/outbound/handler.go b/app/proxyman/outbound/handler.go index eaa1b0b2..62902c60 100644 --- a/app/proxyman/outbound/handler.go +++ b/app/proxyman/outbound/handler.go @@ -6,7 +6,6 @@ import ( goerrors "errors" "io" "math/big" - gonet "net" "os" "github.com/xtls/xray-core/common/dice" @@ -398,7 +397,7 @@ func (h *Handler) ProxySettings() *serial.TypedMessage { func ParseRandomIP(addr net.Address, prefix string) net.Address { - _, ipnet, _ := gonet.ParseCIDR(addr.IP().String() + "/" + prefix) + _, ipnet, _ := net.ParseCIDR(addr.IP().String() + "/" + prefix) ones, bits := ipnet.Mask.Size() subnetSize := new(big.Int).Lsh(big.NewInt(1), uint(bits-ones)) @@ -412,5 +411,5 @@ func ParseRandomIP(addr net.Address, prefix string) net.Address { padded := make([]byte, len(ipnet.IP)) copy(padded[len(padded)-len(rndBytes):], rndBytes) - return net.ParseAddress(gonet.IP(padded).String()) + return net.ParseAddress(net.IP(padded).String()) } diff --git a/common/net/system.go b/common/net/system.go index 7e1c4b01..136da0c1 100644 --- a/common/net/system.go +++ b/common/net/system.go @@ -12,6 +12,8 @@ var ( type ListenConfig = net.ListenConfig +type KeepAliveConfig = net.KeepAliveConfig + var ( Listen = net.Listen ListenTCP = net.ListenTCP @@ -26,6 +28,12 @@ var FileConn = net.FileConn // ParseIP is an alias of net.ParseIP var ParseIP = net.ParseIP +var ParseCIDR = net.ParseCIDR + +var ResolveIPAddr = net.ResolveIPAddr + +var InterfaceByName = net.InterfaceByName + var SplitHostPort = net.SplitHostPort var CIDRMask = net.CIDRMask @@ -51,6 +59,8 @@ type ( UnixConn = net.UnixConn ) +type IPAddr = net.IPAddr + // IP is an alias for net.IP. type ( IP = net.IP @@ -82,3 +92,11 @@ var ( ) type Resolver = net.Resolver + +var DefaultResolver = net.DefaultResolver + +var JoinHostPort = net.JoinHostPort + +var InterfaceAddrs = net.InterfaceAddrs + +var Interfaces = net.Interfaces diff --git a/proxy/wireguard/bind.go b/proxy/wireguard/bind.go index 07ec85ef..515afaa5 100644 --- a/proxy/wireguard/bind.go +++ b/proxy/wireguard/bind.go @@ -3,14 +3,13 @@ package wireguard import ( "context" "errors" - "net" "net/netip" "strconv" "sync" "golang.zx2c4.com/wireguard/conn" - xnet "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/transport/internet" ) @@ -51,21 +50,21 @@ func (n *netBind) ParseEndpoint(s string) (conn.Endpoint, error) { return nil, err } - addr := xnet.ParseAddress(ipStr) - if addr.Family() == xnet.AddressFamilyDomain { + addr := net.ParseAddress(ipStr) + if addr.Family() == net.AddressFamilyDomain { ips, _, err := n.dns.LookupIP(addr.Domain(), n.dnsOption) if err != nil { return nil, err } else if len(ips) == 0 { return nil, dns.ErrEmptyResponse } - addr = xnet.IPAddress(ips[0]) + addr = net.IPAddress(ips[0]) } - dst := xnet.Destination{ + dst := net.Destination{ Address: addr, - Port: xnet.Port(portNum), - Network: xnet.Network_UDP, + Port: net.Port(portNum), + Network: net.Network_UDP, } return &netEndpoint{ @@ -214,7 +213,7 @@ func (bind *netBindServer) Send(buff [][]byte, endpoint conn.Endpoint) error { } type netEndpoint struct { - dst xnet.Destination + dst net.Destination conn net.Conn } @@ -247,7 +246,7 @@ func (e netEndpoint) SrcToString() string { return "" } -func toNetIpAddr(addr xnet.Address) netip.Addr { +func toNetIpAddr(addr net.Address) netip.Addr { if addr.Family().IsIPv4() { ip := addr.IP() return netip.AddrFrom4([4]byte{ip[0], ip[1], ip[2], ip[3]}) diff --git a/proxy/wireguard/tun.go b/proxy/wireguard/tun.go index 74a3b71d..bd20fab2 100644 --- a/proxy/wireguard/tun.go +++ b/proxy/wireguard/tun.go @@ -3,7 +3,6 @@ package wireguard import ( "context" "fmt" - "net" "net/netip" "runtime" "strconv" @@ -13,7 +12,7 @@ import ( "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/log" - xnet "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/proxy/wireguard/gvisortun" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" @@ -28,7 +27,7 @@ import ( type tunCreator func(localAddresses []netip.Addr, mtu int, handler promiscuousModeHandler) (Tunnel, error) -type promiscuousModeHandler func(dest xnet.Destination, conn net.Conn) +type promiscuousModeHandler func(dest net.Destination, conn net.Conn) type Tunnel interface { BuildDevice(ipc string, bind conn.Bind) error @@ -169,7 +168,7 @@ func createGVisorTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo ep.SocketOptions().SetKeepAlive(true) // local address is actually destination - handler(xnet.TCPDestination(xnet.IPAddress(id.LocalAddress.AsSlice()), xnet.Port(id.LocalPort)), gonet.NewTCPConn(&wq, ep)) + handler(net.TCPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)), gonet.NewTCPConn(&wq, ep)) }(r) }) stack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) @@ -194,7 +193,7 @@ func createGVisorTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo Timeout: 15 * time.Second, }) - handler(xnet.UDPDestination(xnet.IPAddress(id.LocalAddress.AsSlice()), xnet.Port(id.LocalPort)), gonet.NewUDPConn(&wq, ep)) + handler(net.UDPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)), gonet.NewUDPConn(&wq, ep)) }(r) }) stack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) diff --git a/transport/internet/dialer.go b/transport/internet/dialer.go index e0da30c2..9342f26f 100644 --- a/transport/internet/dialer.go +++ b/transport/internet/dialer.go @@ -3,7 +3,6 @@ package internet import ( "context" "fmt" - gonet "net" "strings" "github.com/xtls/xray-core/common" @@ -183,7 +182,7 @@ func checkAddressPortStrategy(ctx context.Context, dest net.Destination, sockopt if len(parts) != 3 { return nil, errors.New("invalid address format", dest.Address.String()) } - _, srvRecords, err := gonet.DefaultResolver.LookupSRV(context.Background(), parts[0][1:], parts[1][1:], parts[2]) + _, srvRecords, err := net.DefaultResolver.LookupSRV(context.Background(), parts[0][1:], parts[1][1:], parts[2]) if err != nil { return nil, errors.New("failed to lookup SRV record").Base(err) } @@ -198,7 +197,7 @@ func checkAddressPortStrategy(ctx context.Context, dest net.Destination, sockopt } if OverrideBy == "txt" { errors.LogDebug(ctx, "query TXT record for "+dest.Address.String()) - txtRecords, err := gonet.DefaultResolver.LookupTXT(ctx, dest.Address.String()) + txtRecords, err := net.DefaultResolver.LookupTXT(ctx, dest.Address.String()) if err != nil { errors.LogError(ctx, "failed to lookup SRV record: "+err.Error()) return nil, errors.New("failed to lookup SRV record").Base(err) diff --git a/transport/internet/grpc/dial.go b/transport/internet/grpc/dial.go index b8740dae..454b3280 100644 --- a/transport/internet/grpc/dial.go +++ b/transport/internet/grpc/dial.go @@ -2,7 +2,6 @@ package grpc import ( "context" - gonet "net" "sync" "time" @@ -99,7 +98,7 @@ func getGrpcClient(ctx context.Context, dest net.Destination, streamSettings *in }, MinConnectTimeout: 5 * time.Second, }), - grpc.WithContextDialer(func(gctx context.Context, s string) (gonet.Conn, error) { + grpc.WithContextDialer(func(gctx context.Context, s string) (net.Conn, error) { select { case <-gctx.Done(): return nil, gctx.Err() @@ -180,7 +179,7 @@ func getGrpcClient(ctx context.Context, dest net.Destination, streamSettings *in } conn, err := grpc.Dial( - gonet.JoinHostPort(grpcDestHost, dest.Port.String()), + net.JoinHostPort(grpcDestHost, dest.Port.String()), dialOptions..., ) globalDialerMap[dialerConf{dest, streamSettings}] = conn diff --git a/transport/internet/grpc/encoding/hunkconn.go b/transport/internet/grpc/encoding/hunkconn.go index f295f33a..f1155de8 100644 --- a/transport/internet/grpc/encoding/hunkconn.go +++ b/transport/internet/grpc/encoding/hunkconn.go @@ -3,11 +3,10 @@ package encoding import ( "context" "io" - "net" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" - xnet "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/signal/done" "google.golang.org/grpc/metadata" @@ -55,7 +54,7 @@ func NewHunkConn(hc HunkConn, cancel context.CancelFunc) net.Conn { if ok { header := md.Get("x-real-ip") if len(header) > 0 { - realip := xnet.ParseAddress(header[0]) + realip := net.ParseAddress(header[0]) if realip.Family().IsIP() { rAddr = &net.TCPAddr{ IP: realip.IP(), diff --git a/transport/internet/sockopt_darwin.go b/transport/internet/sockopt_darwin.go index 2c827214..bcae2f3f 100644 --- a/transport/internet/sockopt_darwin.go +++ b/transport/internet/sockopt_darwin.go @@ -2,7 +2,6 @@ package internet import ( "context" - gonet "net" "os" "runtime" "strconv" @@ -135,7 +134,7 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf } if config.Interface != "" { - iface, err := gonet.InterfaceByName(config.Interface) + iface, err := net.InterfaceByName(config.Interface) if err != nil { return errors.New("failed to get interface ", config.Interface).Base(err) @@ -226,7 +225,7 @@ func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) } if config.Interface != "" { - iface, err := gonet.InterfaceByName(config.Interface) + iface, err := net.InterfaceByName(config.Interface) if err != nil { return errors.New("failed to get interface ", config.Interface).Base(err) diff --git a/transport/internet/splithttp/browser_client.go b/transport/internet/splithttp/browser_client.go index e4317aa2..6e0a5302 100644 --- a/transport/internet/splithttp/browser_client.go +++ b/transport/internet/splithttp/browser_client.go @@ -3,9 +3,9 @@ package splithttp import ( "context" "io" - gonet "net" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/transport/internet/browser_dialer" "github.com/xtls/xray-core/transport/internet/websocket" ) @@ -19,13 +19,13 @@ func (c *BrowserDialerClient) IsClosed() bool { panic("not implemented yet") } -func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, body io.Reader, uploadOnly bool) (io.ReadCloser, gonet.Addr, gonet.Addr, error) { +func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, body io.Reader, uploadOnly bool) (io.ReadCloser, net.Addr, net.Addr, error) { if body != nil { return nil, nil, nil, errors.New("bidirectional streaming for browser dialer not implemented yet") } conn, err := browser_dialer.DialGet(url, c.transportConfig.GetRequestHeader(url)) - dummyAddr := &gonet.IPAddr{} + dummyAddr := &net.IPAddr{} if err != nil { return nil, dummyAddr, dummyAddr, err } diff --git a/transport/internet/splithttp/client.go b/transport/internet/splithttp/client.go index 0f809838..3eeab6eb 100644 --- a/transport/internet/splithttp/client.go +++ b/transport/internet/splithttp/client.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "io" - gonet "net" "net/http" "net/http/httptrace" "sync" @@ -42,7 +41,7 @@ func (c *DefaultDialerClient) IsClosed() bool { return c.closed } -func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr gonet.Addr, err error) { +func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) { // this is done when the TCP/UDP connection to the server was established, // and we can unblock the Dial function and print correct net addresses in // logs diff --git a/transport/internet/system_dialer.go b/transport/internet/system_dialer.go index ceb6ebc9..27f3c9a9 100644 --- a/transport/internet/system_dialer.go +++ b/transport/internet/system_dialer.go @@ -3,7 +3,6 @@ package internet import ( "context" "math/rand" - gonet "net" "syscall" "time" @@ -89,7 +88,7 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne }, nil } // Chrome defaults - keepAliveConfig := gonet.KeepAliveConfig{ + keepAliveConfig := net.KeepAliveConfig{ Enable: true, Idle: 45 * time.Second, Interval: 45 * time.Second, diff --git a/transport/internet/system_listener.go b/transport/internet/system_listener.go index 0cb6cd06..2ac28eda 100644 --- a/transport/internet/system_listener.go +++ b/transport/internet/system_listener.go @@ -2,7 +2,6 @@ package internet import ( "context" - gonet "net" "os" "runtime" "strconv" @@ -95,7 +94,7 @@ func (dl *DefaultListener) Listen(ctx context.Context, addr net.Addr, sockopt *S if sockopt.TcpKeepAliveIdle*sockopt.TcpKeepAliveInterval < 0 { return nil, errors.New("invalid TcpKeepAliveIdle or TcpKeepAliveInterval value: ", sockopt.TcpKeepAliveIdle, " ", sockopt.TcpKeepAliveInterval) } - lc.KeepAliveConfig = gonet.KeepAliveConfig{ + lc.KeepAliveConfig = net.KeepAliveConfig{ Enable: false, Idle: -1, Interval: -1, diff --git a/transport/internet/websocket/dialer.go b/transport/internet/websocket/dialer.go index 60330fd7..5e413893 100644 --- a/transport/internet/websocket/dialer.go +++ b/transport/internet/websocket/dialer.go @@ -5,7 +5,6 @@ import ( _ "embed" "encoding/base64" "io" - gonet "net" "time" "github.com/gorilla/websocket" @@ -64,7 +63,7 @@ func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *in tlsConfig := tConfig.GetTLSConfig(tls.WithDestination(dest), tls.WithNextProto("http/1.1")) dialer.TLSClientConfig = tlsConfig if fingerprint := tls.GetFingerprint(tConfig.Fingerprint); fingerprint != nil { - dialer.NetDialTLSContext = func(_ context.Context, _, addr string) (gonet.Conn, error) { + dialer.NetDialTLSContext = func(_ context.Context, _, addr string) (net.Conn, error) { // Like the NetDial in the dialer pconn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) if err != nil { From a903e80356f71caccf2771b6872664c7c4db8129 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 06:01:24 +0000 Subject: [PATCH 058/136] Bump actions/cache from 4 to 5 (#5411) Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release-win7.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/scheduled-assets-update.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-win7.yml b/.github/workflows/release-win7.yml index 7062d262..144e706c 100644 --- a/.github/workflows/release-win7.yml +++ b/.github/workflows/release-win7.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Restore Geodat Cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: resources key: xray-geodat- @@ -101,7 +101,7 @@ jobs: # go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main - name: Restore Geodat Cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: resources key: xray-geodat- diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8353b245..80ed94d3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Restore Geodat Cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: resources key: xray-geodat- @@ -207,7 +207,7 @@ jobs: fi - name: Restore Geodat Cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: resources key: xray-geodat- diff --git a/.github/workflows/scheduled-assets-update.yml b/.github/workflows/scheduled-assets-update.yml index 089c3b33..24183528 100644 --- a/.github/workflows/scheduled-assets-update.yml +++ b/.github/workflows/scheduled-assets-update.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Restore Geodat Cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: resources key: xray-geodat- @@ -58,7 +58,7 @@ jobs: done - name: Save Geodat Cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: ${{ steps.update.outputs.unhit }} with: path: resources diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ce9d1a7..036b7b9c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Restore Geodat Cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: resources key: xray-geodat- @@ -52,7 +52,7 @@ jobs: go-version-file: go.mod check-latest: true - name: Restore Geodat Cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: resources key: xray-geodat- From 9cf22114a144fd643879512f6d5f2be274b5ad05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 06:01:44 +0000 Subject: [PATCH 059/136] Bump github.com/miekg/dns from 1.1.68 to 1.1.69 (#5410) Bumps [github.com/miekg/dns](https://github.com/miekg/dns) from 1.1.68 to 1.1.69. - [Commits](https://github.com/miekg/dns/compare/v1.1.68...v1.1.69) --- updated-dependencies: - dependency-name: github.com/miekg/dns dependency-version: 1.1.69 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1014ca74..7031ecfe 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/golang/mock v1.7.0-rc.1 github.com/google/go-cmp v0.7.0 github.com/gorilla/websocket v1.5.3 - github.com/miekg/dns v1.1.68 + github.com/miekg/dns v1.1.69 github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 github.com/quic-go/quic-go v0.57.1 diff --git a/go.sum b/go.sum index 4938d051..5e97f408 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= -github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= +github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= From a3ba3eefb67cc52a91246975acd50b7d2acf4dd3 Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Sun, 14 Dec 2025 23:13:47 +0800 Subject: [PATCH 060/136] Wireguard: Decouple server endpoint DNS from address option (#5417) * Wireguard: Decouple server endpoint DNS from address option Previously, Wireguard server endpoint's domain resolution was incorrectly constrained by the local `address` option. For example, `ForceIPv6v4` might fail to resolve AAAA records for the server domain if no IPv6 was explicitly configured in the `address` option. This commit decouples the server endpoint's domain resolution from the local `address` configuration. It ensures the Wireguard server address is resolved independently, allowing its `domainStrategy` to function correctly without being limited by the client's local network or `address` settings. * Delete code instead of commenting it out --- proxy/wireguard/client.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proxy/wireguard/client.go b/proxy/wireguard/client.go index 9de24360..2e5fc286 100644 --- a/proxy/wireguard/client.go +++ b/proxy/wireguard/client.go @@ -300,14 +300,14 @@ func (h *Handler) createIPCRequest() string { errors.LogInfo(h.bind.ctx, "createIPCRequest use dialer dest ip: ", addr) } else { ips, _, err := h.dns.LookupIP(addr.Domain(), dns.IPOption{ - IPv4Enable: h.hasIPv4 && h.conf.preferIP4(), - IPv6Enable: h.hasIPv6 && h.conf.preferIP6(), + IPv4Enable: h.conf.preferIP4(), + IPv6Enable: h.conf.preferIP6(), }) { // Resolve fallback if (len(ips) == 0 || err != nil) && h.conf.hasFallback() { ips, _, err = h.dns.LookupIP(addr.Domain(), dns.IPOption{ - IPv4Enable: h.hasIPv4 && h.conf.fallbackIP4(), - IPv6Enable: h.hasIPv6 && h.conf.fallbackIP6(), + IPv4Enable: h.conf.fallbackIP4(), + IPv6Enable: h.conf.fallbackIP6(), }) } } From c40326dfd7f700ba1f1bb9934cff8245ddd2e382 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 22:00:53 -0500 Subject: [PATCH 061/136] Bump google.golang.org/protobuf from 1.36.10 to 1.36.11 (#5424) Bumps google.golang.org/protobuf from 1.36.10 to 1.36.11. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-version: 1.36.11 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7031ecfe..d60e18f8 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( golang.org/x/sys v0.39.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 google.golang.org/grpc v1.77.0 - google.golang.org/protobuf v1.36.10 + google.golang.org/protobuf v1.36.11 gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h12.io/socks v1.0.3 lukechampine.com/blake3 v1.4.1 diff --git a/go.sum b/go.sum index 5e97f408..f09b6603 100644 --- a/go.sum +++ b/go.sum @@ -144,8 +144,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 74df63add2cd7f11afb5b30a269156aecf4d1b17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 22:01:02 -0500 Subject: [PATCH 062/136] Bump actions/upload-artifact from 5 to 6 (#5425) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release-win7.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-win7.yml b/.github/workflows/release-win7.yml index 144e706c..9f1a33d8 100644 --- a/.github/workflows/release-win7.yml +++ b/.github/workflows/release-win7.yml @@ -132,7 +132,7 @@ jobs: mv build_assets Xray-${{ env.ASSET_NAME }} - name: Upload files to Artifacts - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: Xray-${{ env.ASSET_NAME }} path: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80ed94d3..8fe5cae7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -238,7 +238,7 @@ jobs: mv build_assets Xray-${{ env.ASSET_NAME }} - name: Upload files to Artifacts - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: Xray-${{ env.ASSET_NAME }} path: | From 6bf0376773e87e6691b10e88f7d91bf3c55f6b0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Dec 2025 22:28:33 -0500 Subject: [PATCH 063/136] Bump github.com/quic-go/quic-go from 0.57.1 to 0.58.0 (#5459) Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.57.1 to 0.58.0. - [Release notes](https://github.com/quic-go/quic-go/releases) - [Commits](https://github.com/quic-go/quic-go/compare/v0.57.1...v0.58.0) --- updated-dependencies: - dependency-name: github.com/quic-go/quic-go dependency-version: 0.58.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d60e18f8..6e4edc92 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/miekg/dns v1.1.69 github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 - github.com/quic-go/quic-go v0.57.1 + github.com/quic-go/quic-go v0.58.0 github.com/refraction-networking/utls v1.8.1 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 diff --git a/go.sum b/go.sum index f09b6603..58029a62 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= -github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug= +github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= From 04b433dd978793124148e996419fef50aee37663 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 08:13:09 +0000 Subject: [PATCH 064/136] Bump github.com/cloudflare/circl from 1.6.1 to 1.6.2 (#5465) Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) from 1.6.1 to 1.6.2. - [Release notes](https://github.com/cloudflare/circl/releases) - [Commits](https://github.com/cloudflare/circl/compare/v1.6.1...v1.6.2) --- updated-dependencies: - dependency-name: github.com/cloudflare/circl dependency-version: 1.6.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6e4edc92..ab8bed06 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/xtls/xray-core go 1.25 require ( - github.com/cloudflare/circl v1.6.1 + github.com/cloudflare/circl v1.6.2 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/golang/mock v1.7.0-rc.1 github.com/google/go-cmp v0.7.0 diff --git a/go.sum b/go.sum index 58029a62..4b65b8b0 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= +github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= From dd757ca27c23e854c17a7139a93eb8f68eb6759c Mon Sep 17 00:00:00 2001 From: xtlsee Date: Tue, 23 Dec 2025 16:27:40 +0800 Subject: [PATCH 065/136] VLESS inbound: Print invalid UUID string (#5426) Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- proxy/vless/encoding/encoding.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/proxy/vless/encoding/encoding.go b/proxy/vless/encoding/encoding.go index fe2c6bc8..b3b43bac 100644 --- a/proxy/vless/encoding/encoding.go +++ b/proxy/vless/encoding/encoding.go @@ -10,6 +10,7 @@ import ( "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/uuid" "github.com/xtls/xray-core/proxy" "github.com/xtls/xray-core/proxy/vless" ) @@ -91,7 +92,8 @@ func DecodeRequestHeader(isfb bool, first *buf.Buffer, reader io.Reader, validat } if request.User = validator.Get(id); request.User == nil { - return nil, nil, nil, isfb, errors.New("invalid request user id") + u := uuid.UUID(id) + return nil, nil, nil, isfb, errors.New("invalid request user id: %s" + u.String()) } if isfb { From 3572209cbd60d223e3e47aa17a0ccc17362fffcd Mon Sep 17 00:00:00 2001 From: ari-ahm <154451491+ari-ahm@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:28:43 +0330 Subject: [PATCH 066/136] REALITY client: Clearer log when receiving real certificate (#5427) Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- transport/internet/reality/reality.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/transport/internet/reality/reality.go b/transport/internet/reality/reality.go index 8cb59342..21b185ea 100644 --- a/transport/internet/reality/reality.go +++ b/transport/internet/reality/reality.go @@ -180,11 +180,14 @@ func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destinati fmt.Printf("REALITY localAddr: %v\tuConn.Verified: %v\n", localAddr, uConn.Verified) } if !uConn.Verified { + errors.LogError(ctx, "REALITY: received real certificate (potential MITM or redirection)") go func() { client := &http.Client{ Transport: &http2.Transport{ DialTLSContext: func(ctx context.Context, network, addr string, cfg *gotls.Config) (net.Conn, error) { - fmt.Printf("REALITY localAddr: %v\tDialTLSContext\n", localAddr) + if config.Show { + fmt.Printf("REALITY localAddr: %v\tDialTLSContext\n", localAddr) + } return uConn, nil }, }, From a6792dda69f879ba22e16fa9ce49198b22f07aa8 Mon Sep 17 00:00:00 2001 From: patterniha <71074308+patterniha@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:11:01 +0330 Subject: [PATCH 067/136] TLS ECH: Increase DOH timeout (#5455) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 风扇滑翔翼 --- transport/internet/tls/ech.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 2cd07c9d..c1c55eff 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -246,7 +246,7 @@ func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]b }, } c := &http.Client{ - Timeout: 5 * time.Second, + Timeout: 30 * time.Second, Transport: tr, } client, _ = clientForECHDOH.LoadOrStore(serverKey, c) From fa64775f07b5a53dc40679ef944977f76c086c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Tue, 23 Dec 2025 17:44:54 +0800 Subject: [PATCH 068/136] Tunnel/Dokodemo: Fix stats conn unwrap (#5440) Fixes https://github.com/XTLS/Xray-core/issues/5439 --- proxy/dokodemo/dokodemo.go | 3 ++- proxy/http/client.go | 5 +---- proxy/proxy.go | 5 +---- proxy/trojan/server.go | 6 +----- proxy/vless/inbound/inbound.go | 5 +---- proxy/vless/outbound/outbound.go | 5 +---- proxy/vmess/inbound/inbound.go | 5 +---- transport/internet/stat/connection.go | 10 ++++++++++ 8 files changed, 18 insertions(+), 26 deletions(-) diff --git a/proxy/dokodemo/dokodemo.go b/proxy/dokodemo/dokodemo.go index 90fc53e8..e56363db 100644 --- a/proxy/dokodemo/dokodemo.go +++ b/proxy/dokodemo/dokodemo.go @@ -111,7 +111,8 @@ func (d *DokodemoDoor) Process(ctx context.Context, network net.Network, conn st destinationOverridden = true } } - if tlsConn, ok := conn.(tls.Interface); ok && !destinationOverridden { + iConn := stat.TryUnwrapStatsConn(conn) + if tlsConn, ok := iConn.(tls.Interface); ok && !destinationOverridden { if serverName := tlsConn.HandshakeContextServerName(ctx); serverName != "" { dest.Address = net.DomainAddress(serverName) destinationOverridden = true diff --git a/proxy/http/client.go b/proxy/http/client.go index 9544ad37..1f804d0b 100644 --- a/proxy/http/client.go +++ b/proxy/http/client.go @@ -296,10 +296,7 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u return nil, err } - iConn := rawConn - if statConn, ok := iConn.(*stat.CounterConnection); ok { - iConn = statConn.Connection - } + iConn := stat.TryUnwrapStatsConn(rawConn) nextProto := "" if tlsConn, ok := iConn.(*tls.Conn); ok { diff --git a/proxy/proxy.go b/proxy/proxy.go index d6a797cb..29548d9f 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -787,10 +787,7 @@ func readV(ctx context.Context, reader buf.Reader, writer buf.Writer, timer sign } func IsRAWTransportWithoutSecurity(conn stat.Connection) bool { - iConn := conn - if statConn, ok := iConn.(*stat.CounterConnection); ok { - iConn = statConn.Connection - } + iConn := stat.TryUnwrapStatsConn(conn) _, ok1 := iConn.(*proxyproto.Conn) _, ok2 := iConn.(*net.TCPConn) _, ok3 := iConn.(*internet.UnixConnWrapper) diff --git a/proxy/trojan/server.go b/proxy/trojan/server.go index 8ed3b0e6..d66219c4 100644 --- a/proxy/trojan/server.go +++ b/proxy/trojan/server.go @@ -147,11 +147,7 @@ func (s *Server) Network() []net.Network { // Process implements proxy.Inbound.Process(). func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error { - iConn := conn - statConn, ok := iConn.(*stat.CounterConnection) - if ok { - iConn = statConn.Connection - } + iConn := stat.TryUnwrapStatsConn(conn) sessionPolicy := s.policyManager.ForLevel(0) if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil { diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index 89ed0e72..eeb1a25f 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -265,10 +265,7 @@ func (*Handler) Network() []net.Network { // Process implements proxy.Inbound.Process(). func (h *Handler) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatcher routing.Dispatcher) error { - iConn := connection - if statConn, ok := iConn.(*stat.CounterConnection); ok { - iConn = statConn.Connection - } + iConn := stat.TryUnwrapStatsConn(connection) if h.decryption != nil { var err error diff --git a/proxy/vless/outbound/outbound.go b/proxy/vless/outbound/outbound.go index 17e82210..a4fe1e93 100644 --- a/proxy/vless/outbound/outbound.go +++ b/proxy/vless/outbound/outbound.go @@ -192,10 +192,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte ob.Conn = conn // for Vision's pre-connect - iConn := conn - if statConn, ok := iConn.(*stat.CounterConnection); ok { - iConn = statConn.Connection - } + iConn := stat.TryUnwrapStatsConn(conn) target := ob.Target errors.LogInfo(ctx, "tunneling request to ", target, " via ", rec.Destination.NetAddr()) diff --git a/proxy/vmess/inbound/inbound.go b/proxy/vmess/inbound/inbound.go index 7975551b..6a8591ad 100644 --- a/proxy/vmess/inbound/inbound.go +++ b/proxy/vmess/inbound/inbound.go @@ -229,10 +229,7 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s return errors.New("unable to set read deadline").Base(err).AtWarning() } - iConn := connection - if statConn, ok := iConn.(*stat.CounterConnection); ok { - iConn = statConn.Connection - } + iConn := stat.TryUnwrapStatsConn(connection) _, isDrain := iConn.(*net.TCPConn) if !isDrain { _, isDrain = iConn.(*net.UnixConn) diff --git a/transport/internet/stat/connection.go b/transport/internet/stat/connection.go index 6921943d..b039b29c 100644 --- a/transport/internet/stat/connection.go +++ b/transport/internet/stat/connection.go @@ -32,3 +32,13 @@ func (c *CounterConnection) Write(b []byte) (int, error) { } return nBytes, err } + +func TryUnwrapStatsConn(conn net.Conn) net.Conn { + if conn == nil { + return conn + } + if conn, ok := conn.(*CounterConnection); ok { + return conn.Connection + } + return conn +} From 7f6ceb39f7f1bae9910b952df4ca932c5a00d40c Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:14:42 +0800 Subject: [PATCH 069/136] DomainMatcher: Prevent illegal domain rules from causing core startup failures (#5430) Closes https://github.com/XTLS/Xray-core/issues/5429 --- app/dns/dns.go | 3 +-- app/dns/hosts.go | 9 +++++++-- app/dns/nameserver.go | 10 ++++------ app/router/condition.go | 7 +++++-- common/strmatcher/strmatcher.go | 3 ++- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/dns/dns.go b/app/dns/dns.go index 79f9e8e1..603640f1 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -106,13 +106,12 @@ func New(ctx context.Context, config *Config) (*DNS, error) { for _, ns := range config.NameServer { clientIdx := len(clients) - updateDomain := func(domainRule strmatcher.Matcher, originalRuleIdx int, matcherInfos []*DomainMatcherInfo) error { + updateDomain := func(domainRule strmatcher.Matcher, originalRuleIdx int, matcherInfos []*DomainMatcherInfo) { midx := domainMatcher.Add(domainRule) matcherInfos[midx] = &DomainMatcherInfo{ clientIdx: uint16(clientIdx), domainRuleIdx: uint16(originalRuleIdx), } - return nil } myClientIP := clientIP diff --git a/app/dns/hosts.go b/app/dns/hosts.go index c2f7649d..0ee4fdd0 100644 --- a/app/dns/hosts.go +++ b/app/dns/hosts.go @@ -27,7 +27,8 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) { for _, mapping := range hosts { matcher, err := toStrMatcher(mapping.Type, mapping.Domain) if err != nil { - return nil, errors.New("failed to create domain matcher").Base(err) + errors.LogErrorInner(context.Background(), err, "failed to create domain matcher, ignore domain rule [type: ", mapping.Type, ", domain: ", mapping.Domain, "]") + continue } id := g.Add(matcher) ips := make([]net.Address, 0, len(mapping.Ip)+1) @@ -46,10 +47,14 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) { for _, ip := range mapping.Ip { addr := net.IPAddress(ip) if addr == nil { - return nil, errors.New("invalid IP address in static hosts: ", ip).AtWarning() + errors.LogError(context.Background(), "invalid IP address in static hosts: ", ip, ", ignore this ip for rule [type: ", mapping.Type, ", domain: ", mapping.Domain, "]") + continue } ips = append(ips, addr) } + if len(ips) == 0 { + continue + } } sh.ips[id] = ips diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go index e606d4b3..bad9277c 100644 --- a/app/dns/nameserver.go +++ b/app/dns/nameserver.go @@ -97,7 +97,7 @@ func NewClient( tag string, ipOption dns.IPOption, matcherInfos *[]*DomainMatcherInfo, - updateDomainRule func(strmatcher.Matcher, int, []*DomainMatcherInfo) error, + updateDomainRule func(strmatcher.Matcher, int, []*DomainMatcherInfo), ) (*Client, error) { client := &Client{} @@ -134,7 +134,8 @@ func NewClient( for _, domain := range ns.PrioritizedDomain { domainRule, err := toStrMatcher(domain.Type, domain.Domain) if err != nil { - return errors.New("failed to create prioritized domain").Base(err).AtWarning() + errors.LogErrorInner(ctx, err, "failed to create domain matcher, ignore domain rule [type: ", domain.Type, ", domain: ", domain.Domain, "]") + domainRule, _ = toStrMatcher(DomainMatchingType_Full, "hack.fix.index.for.illegal.domain.rule") } originalRuleIdx := ruleCurr if ruleCurr < len(ns.OriginalRules) { @@ -151,10 +152,7 @@ func NewClient( rules = append(rules, domainRule.String()) ruleCurr++ } - err = updateDomainRule(domainRule, originalRuleIdx, *matcherInfos) - if err != nil { - return errors.New("failed to create prioritized domain").Base(err).AtWarning() - } + updateDomainRule(domainRule, originalRuleIdx, *matcherInfos) } // Establish expected IPs diff --git a/app/router/condition.go b/app/router/condition.go index 083cbfaf..d21487a2 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -1,6 +1,7 @@ package router import ( + "context" "regexp" "strings" @@ -56,11 +57,13 @@ func NewMphMatcherGroup(domains []*Domain) (*DomainMatcher, error) { for _, d := range domains { matcherType, f := matcherTypeMap[d.Type] if !f { - return nil, errors.New("unsupported domain type", d.Type) + errors.LogError(context.Background(), "ignore unsupported domain type ", d.Type, " of rule ", d.Value) + continue } _, err := g.AddPattern(d.Value, matcherType) if err != nil { - return nil, err + errors.LogErrorInner(context.Background(), err, "ignore domain rule ", d.Type, " ", d.Value) + continue } } g.Build() diff --git a/common/strmatcher/strmatcher.go b/common/strmatcher/strmatcher.go index 294e6e73..4035acc3 100644 --- a/common/strmatcher/strmatcher.go +++ b/common/strmatcher/strmatcher.go @@ -1,6 +1,7 @@ package strmatcher import ( + "errors" "regexp" ) @@ -44,7 +45,7 @@ func (t Type) New(pattern string) (Matcher, error) { pattern: r, }, nil default: - panic("Unknown type") + return nil, errors.New("unk type") } } From 36968909a1da976c770412c968caaea2e78b4a78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 20:45:54 -0500 Subject: [PATCH 070/136] Bump google.golang.org/grpc from 1.77.0 to 1.78.0 (#5469) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.77.0 to 1.78.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.77.0...v1.78.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.78.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index ab8bed06..f0797bc0 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( golang.org/x/sync v0.19.0 golang.org/x/sys v0.39.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 - google.golang.org/grpc v1.77.0 + google.golang.org/grpc v1.78.0 google.golang.org/protobuf v1.36.11 gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h12.io/socks v1.0.3 @@ -51,7 +51,7 @@ require ( golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.39.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4b65b8b0..03f6eae5 100644 --- a/go.sum +++ b/go.sum @@ -140,10 +140,10 @@ golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uI golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 6738ecf68ec5c31e91a2d5ec0b539ea5745ce092 Mon Sep 17 00:00:00 2001 From: ari-ahm <154451491+ari-ahm@users.noreply.github.com> Date: Fri, 26 Dec 2025 23:47:24 +0330 Subject: [PATCH 071/136] common/uuid: fix panic when parsing 32-len invalid UUID string. (#5468) * common/uuid: fix panic when parsing 32-len invalid UUID string. * fix: removed typo --- common/uuid/uuid.go | 6 +++++- common/uuid/uuid_test.go | 5 +++++ proxy/vless/encoding/encoding.go | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/common/uuid/uuid.go b/common/uuid/uuid.go index 4ede12e6..ef6da4cb 100644 --- a/common/uuid/uuid.go +++ b/common/uuid/uuid.go @@ -85,10 +85,14 @@ func ParseString(str string) (UUID, error) { b := uuid.Bytes() for _, byteGroup := range byteGroups { - if text[0] == '-' { + if len(text) > 0 && text[0] == '-' { text = text[1:] } + if len(text) < byteGroup { + return uuid, errors.New("invalid UUID: ", str) + } + if _, err := hex.Decode(b[:byteGroup/2], text[:byteGroup]); err != nil { return uuid, err } diff --git a/common/uuid/uuid_test.go b/common/uuid/uuid_test.go index 6f26ca5b..7d909a78 100644 --- a/common/uuid/uuid_test.go +++ b/common/uuid/uuid_test.go @@ -44,6 +44,11 @@ func TestParseString(t *testing.T) { if err == nil { t.Fatal("Expect error but nil") } + + _, err = ParseString("2418d087-648d-4990-86e8-19dca1d0") + if err == nil { + t.Fatal("Expect error but nil") + } } func TestNewUUID(t *testing.T) { diff --git a/proxy/vless/encoding/encoding.go b/proxy/vless/encoding/encoding.go index b3b43bac..6cbacd8d 100644 --- a/proxy/vless/encoding/encoding.go +++ b/proxy/vless/encoding/encoding.go @@ -93,7 +93,7 @@ func DecodeRequestHeader(isfb bool, first *buf.Buffer, reader io.Reader, validat if request.User = validator.Get(id); request.User == nil { u := uuid.UUID(id) - return nil, nil, nil, isfb, errors.New("invalid request user id: %s" + u.String()) + return nil, nil, nil, isfb, errors.New("invalid request user id: " + u.String()) } if isfb { From ad468e462db717d7eef72f71bd89fcf51ef1c1e8 Mon Sep 17 00:00:00 2001 From: Maxim Plotnikov <51321197+mr1cloud@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:07:06 +0300 Subject: [PATCH 072/136] API: Add GetAllOnlineUsers RPC to StatsService for retrieving online users (#5080) --- app/stats/command/command.go | 6 + app/stats/command/command.pb.go | 462 ++++++++++-------- app/stats/command/command.proto | 7 + app/stats/command/command_grpc.pb.go | 44 +- app/stats/stats.go | 15 + features/stats/stats.go | 8 + main/commands/all/api/api.go | 1 + .../all/api/stats_get_all_online_users.go | 43 ++ 8 files changed, 366 insertions(+), 220 deletions(-) create mode 100644 main/commands/all/api/stats_get_all_online_users.go diff --git a/app/stats/command/command.go b/app/stats/command/command.go index db5283c8..a4e11374 100644 --- a/app/stats/command/command.go +++ b/app/stats/command/command.go @@ -80,6 +80,12 @@ func (s *statsServer) GetStatsOnlineIpList(ctx context.Context, request *GetStat }, nil } +func (s *statsServer) GetAllOnlineUsers(ctx context.Context, request *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error) { + return &GetAllOnlineUsersResponse{ + Users: s.stats.GetAllOnlineUsers(), + }, nil +} + func (s *statsServer) QueryStats(ctx context.Context, request *QueryStatsRequest) (*QueryStatsResponse, error) { matcher, err := strmatcher.Substr.New(request.Pattern) if err != nil { diff --git a/app/stats/command/command.pb.go b/app/stats/command/command.pb.go index 062c2d28..4fbff448 100644 --- a/app/stats/command/command.pb.go +++ b/app/stats/command/command.pb.go @@ -1,8 +1,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.2 -// source: app/stats/command/command.proto +// protoc-gen-go v1.36.8 +// protoc v6.32.0 +// source: command.proto package command @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -21,19 +22,18 @@ const ( ) type GetStatsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Name of the stat counter. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Whether or not to reset the counter to fetching its value. - Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` + Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetStatsRequest) Reset() { *x = GetStatsRequest{} - mi := &file_app_stats_command_command_proto_msgTypes[0] + mi := &file_command_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -45,7 +45,7 @@ func (x *GetStatsRequest) String() string { func (*GetStatsRequest) ProtoMessage() {} func (x *GetStatsRequest) ProtoReflect() protoreflect.Message { - mi := &file_app_stats_command_command_proto_msgTypes[0] + mi := &file_command_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -58,7 +58,7 @@ func (x *GetStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetStatsRequest.ProtoReflect.Descriptor instead. func (*GetStatsRequest) Descriptor() ([]byte, []int) { - return file_app_stats_command_command_proto_rawDescGZIP(), []int{0} + return file_command_proto_rawDescGZIP(), []int{0} } func (x *GetStatsRequest) GetName() string { @@ -76,17 +76,16 @@ func (x *GetStatsRequest) GetReset_() bool { } type Stat struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value int64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Value int64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Stat) Reset() { *x = Stat{} - mi := &file_app_stats_command_command_proto_msgTypes[1] + mi := &file_command_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -98,7 +97,7 @@ func (x *Stat) String() string { func (*Stat) ProtoMessage() {} func (x *Stat) ProtoReflect() protoreflect.Message { - mi := &file_app_stats_command_command_proto_msgTypes[1] + mi := &file_command_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -111,7 +110,7 @@ func (x *Stat) ProtoReflect() protoreflect.Message { // Deprecated: Use Stat.ProtoReflect.Descriptor instead. func (*Stat) Descriptor() ([]byte, []int) { - return file_app_stats_command_command_proto_rawDescGZIP(), []int{1} + return file_command_proto_rawDescGZIP(), []int{1} } func (x *Stat) GetName() string { @@ -129,16 +128,15 @@ func (x *Stat) GetValue() int64 { } type GetStatsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Stat *Stat `protobuf:"bytes,1,opt,name=stat,proto3" json:"stat,omitempty"` unknownFields protoimpl.UnknownFields - - Stat *Stat `protobuf:"bytes,1,opt,name=stat,proto3" json:"stat,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetStatsResponse) Reset() { *x = GetStatsResponse{} - mi := &file_app_stats_command_command_proto_msgTypes[2] + mi := &file_command_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -150,7 +148,7 @@ func (x *GetStatsResponse) String() string { func (*GetStatsResponse) ProtoMessage() {} func (x *GetStatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_app_stats_command_command_proto_msgTypes[2] + mi := &file_command_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -163,7 +161,7 @@ func (x *GetStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetStatsResponse.ProtoReflect.Descriptor instead. func (*GetStatsResponse) Descriptor() ([]byte, []int) { - return file_app_stats_command_command_proto_rawDescGZIP(), []int{2} + return file_command_proto_rawDescGZIP(), []int{2} } func (x *GetStatsResponse) GetStat() *Stat { @@ -174,17 +172,16 @@ func (x *GetStatsResponse) GetStat() *Stat { } type QueryStatsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Pattern string `protobuf:"bytes,1,opt,name=pattern,proto3" json:"pattern,omitempty"` + Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` unknownFields protoimpl.UnknownFields - - Pattern string `protobuf:"bytes,1,opt,name=pattern,proto3" json:"pattern,omitempty"` - Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` + sizeCache protoimpl.SizeCache } func (x *QueryStatsRequest) Reset() { *x = QueryStatsRequest{} - mi := &file_app_stats_command_command_proto_msgTypes[3] + mi := &file_command_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -196,7 +193,7 @@ func (x *QueryStatsRequest) String() string { func (*QueryStatsRequest) ProtoMessage() {} func (x *QueryStatsRequest) ProtoReflect() protoreflect.Message { - mi := &file_app_stats_command_command_proto_msgTypes[3] + mi := &file_command_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -209,7 +206,7 @@ func (x *QueryStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryStatsRequest.ProtoReflect.Descriptor instead. func (*QueryStatsRequest) Descriptor() ([]byte, []int) { - return file_app_stats_command_command_proto_rawDescGZIP(), []int{3} + return file_command_proto_rawDescGZIP(), []int{3} } func (x *QueryStatsRequest) GetPattern() string { @@ -227,16 +224,15 @@ func (x *QueryStatsRequest) GetReset_() bool { } type QueryStatsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Stat []*Stat `protobuf:"bytes,1,rep,name=stat,proto3" json:"stat,omitempty"` unknownFields protoimpl.UnknownFields - - Stat []*Stat `protobuf:"bytes,1,rep,name=stat,proto3" json:"stat,omitempty"` + sizeCache protoimpl.SizeCache } func (x *QueryStatsResponse) Reset() { *x = QueryStatsResponse{} - mi := &file_app_stats_command_command_proto_msgTypes[4] + mi := &file_command_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -248,7 +244,7 @@ func (x *QueryStatsResponse) String() string { func (*QueryStatsResponse) ProtoMessage() {} func (x *QueryStatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_app_stats_command_command_proto_msgTypes[4] + mi := &file_command_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -261,7 +257,7 @@ func (x *QueryStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryStatsResponse.ProtoReflect.Descriptor instead. func (*QueryStatsResponse) Descriptor() ([]byte, []int) { - return file_app_stats_command_command_proto_rawDescGZIP(), []int{4} + return file_command_proto_rawDescGZIP(), []int{4} } func (x *QueryStatsResponse) GetStat() []*Stat { @@ -272,14 +268,14 @@ func (x *QueryStatsResponse) GetStat() []*Stat { } type SysStatsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SysStatsRequest) Reset() { *x = SysStatsRequest{} - mi := &file_app_stats_command_command_proto_msgTypes[5] + mi := &file_command_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -291,7 +287,7 @@ func (x *SysStatsRequest) String() string { func (*SysStatsRequest) ProtoMessage() {} func (x *SysStatsRequest) ProtoReflect() protoreflect.Message { - mi := &file_app_stats_command_command_proto_msgTypes[5] + mi := &file_command_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -304,29 +300,28 @@ func (x *SysStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SysStatsRequest.ProtoReflect.Descriptor instead. func (*SysStatsRequest) Descriptor() ([]byte, []int) { - return file_app_stats_command_command_proto_rawDescGZIP(), []int{5} + return file_command_proto_rawDescGZIP(), []int{5} } type SysStatsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NumGoroutine uint32 `protobuf:"varint,1,opt,name=NumGoroutine,proto3" json:"NumGoroutine,omitempty"` + NumGC uint32 `protobuf:"varint,2,opt,name=NumGC,proto3" json:"NumGC,omitempty"` + Alloc uint64 `protobuf:"varint,3,opt,name=Alloc,proto3" json:"Alloc,omitempty"` + TotalAlloc uint64 `protobuf:"varint,4,opt,name=TotalAlloc,proto3" json:"TotalAlloc,omitempty"` + Sys uint64 `protobuf:"varint,5,opt,name=Sys,proto3" json:"Sys,omitempty"` + Mallocs uint64 `protobuf:"varint,6,opt,name=Mallocs,proto3" json:"Mallocs,omitempty"` + Frees uint64 `protobuf:"varint,7,opt,name=Frees,proto3" json:"Frees,omitempty"` + LiveObjects uint64 `protobuf:"varint,8,opt,name=LiveObjects,proto3" json:"LiveObjects,omitempty"` + PauseTotalNs uint64 `protobuf:"varint,9,opt,name=PauseTotalNs,proto3" json:"PauseTotalNs,omitempty"` + Uptime uint32 `protobuf:"varint,10,opt,name=Uptime,proto3" json:"Uptime,omitempty"` unknownFields protoimpl.UnknownFields - - NumGoroutine uint32 `protobuf:"varint,1,opt,name=NumGoroutine,proto3" json:"NumGoroutine,omitempty"` - NumGC uint32 `protobuf:"varint,2,opt,name=NumGC,proto3" json:"NumGC,omitempty"` - Alloc uint64 `protobuf:"varint,3,opt,name=Alloc,proto3" json:"Alloc,omitempty"` - TotalAlloc uint64 `protobuf:"varint,4,opt,name=TotalAlloc,proto3" json:"TotalAlloc,omitempty"` - Sys uint64 `protobuf:"varint,5,opt,name=Sys,proto3" json:"Sys,omitempty"` - Mallocs uint64 `protobuf:"varint,6,opt,name=Mallocs,proto3" json:"Mallocs,omitempty"` - Frees uint64 `protobuf:"varint,7,opt,name=Frees,proto3" json:"Frees,omitempty"` - LiveObjects uint64 `protobuf:"varint,8,opt,name=LiveObjects,proto3" json:"LiveObjects,omitempty"` - PauseTotalNs uint64 `protobuf:"varint,9,opt,name=PauseTotalNs,proto3" json:"PauseTotalNs,omitempty"` - Uptime uint32 `protobuf:"varint,10,opt,name=Uptime,proto3" json:"Uptime,omitempty"` + sizeCache protoimpl.SizeCache } func (x *SysStatsResponse) Reset() { *x = SysStatsResponse{} - mi := &file_app_stats_command_command_proto_msgTypes[6] + mi := &file_command_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -338,7 +333,7 @@ func (x *SysStatsResponse) String() string { func (*SysStatsResponse) ProtoMessage() {} func (x *SysStatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_app_stats_command_command_proto_msgTypes[6] + mi := &file_command_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -351,7 +346,7 @@ func (x *SysStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SysStatsResponse.ProtoReflect.Descriptor instead. func (*SysStatsResponse) Descriptor() ([]byte, []int) { - return file_app_stats_command_command_proto_rawDescGZIP(), []int{6} + return file_command_proto_rawDescGZIP(), []int{6} } func (x *SysStatsResponse) GetNumGoroutine() uint32 { @@ -425,17 +420,16 @@ func (x *SysStatsResponse) GetUptime() uint32 { } type GetStatsOnlineIpListResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Ips map[string]int64 `protobuf:"bytes,2,rep,name=ips,proto3" json:"ips,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Ips map[string]int64 `protobuf:"bytes,2,rep,name=ips,proto3" json:"ips,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` + sizeCache protoimpl.SizeCache } func (x *GetStatsOnlineIpListResponse) Reset() { *x = GetStatsOnlineIpListResponse{} - mi := &file_app_stats_command_command_proto_msgTypes[7] + mi := &file_command_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -447,7 +441,7 @@ func (x *GetStatsOnlineIpListResponse) String() string { func (*GetStatsOnlineIpListResponse) ProtoMessage() {} func (x *GetStatsOnlineIpListResponse) ProtoReflect() protoreflect.Message { - mi := &file_app_stats_command_command_proto_msgTypes[7] + mi := &file_command_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -460,7 +454,7 @@ func (x *GetStatsOnlineIpListResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetStatsOnlineIpListResponse.ProtoReflect.Descriptor instead. func (*GetStatsOnlineIpListResponse) Descriptor() ([]byte, []int) { - return file_app_stats_command_command_proto_rawDescGZIP(), []int{7} + return file_command_proto_rawDescGZIP(), []int{7} } func (x *GetStatsOnlineIpListResponse) GetName() string { @@ -477,15 +471,95 @@ func (x *GetStatsOnlineIpListResponse) GetIps() map[string]int64 { return nil } -type Config struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type GetAllOnlineUsersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAllOnlineUsersRequest) Reset() { + *x = GetAllOnlineUsersRequest{} + mi := &file_command_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAllOnlineUsersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAllOnlineUsersRequest) ProtoMessage() {} + +func (x *GetAllOnlineUsersRequest) ProtoReflect() protoreflect.Message { + mi := &file_command_proto_msgTypes[8] + 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 GetAllOnlineUsersRequest.ProtoReflect.Descriptor instead. +func (*GetAllOnlineUsersRequest) Descriptor() ([]byte, []int) { + return file_command_proto_rawDescGZIP(), []int{8} +} + +type GetAllOnlineUsersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Users []string `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAllOnlineUsersResponse) Reset() { + *x = GetAllOnlineUsersResponse{} + mi := &file_command_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAllOnlineUsersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAllOnlineUsersResponse) ProtoMessage() {} + +func (x *GetAllOnlineUsersResponse) ProtoReflect() protoreflect.Message { + mi := &file_command_proto_msgTypes[9] + 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 GetAllOnlineUsersResponse.ProtoReflect.Descriptor instead. +func (*GetAllOnlineUsersResponse) Descriptor() ([]byte, []int) { + return file_command_proto_rawDescGZIP(), []int{9} +} + +func (x *GetAllOnlineUsersResponse) GetUsers() []string { + if x != nil { + return x.Users + } + return nil +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Config) Reset() { *x = Config{} - mi := &file_app_stats_command_command_proto_msgTypes[8] + mi := &file_command_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -497,7 +571,7 @@ func (x *Config) String() string { func (*Config) ProtoMessage() {} func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_app_stats_command_command_proto_msgTypes[8] + mi := &file_command_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -510,125 +584,76 @@ func (x *Config) ProtoReflect() protoreflect.Message { // Deprecated: Use Config.ProtoReflect.Descriptor instead. func (*Config) Descriptor() ([]byte, []int) { - return file_app_stats_command_command_proto_rawDescGZIP(), []int{8} + return file_command_proto_rawDescGZIP(), []int{10} } -var File_app_stats_command_command_proto protoreflect.FileDescriptor +var File_command_proto protoreflect.FileDescriptor -var file_app_stats_command_command_proto_rawDesc = []byte{ - 0x0a, 0x1f, 0x61, 0x70, 0x70, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x16, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, - 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x3b, 0x0a, 0x0f, 0x47, 0x65, 0x74, - 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x22, 0x30, 0x0a, 0x04, 0x53, 0x74, 0x61, 0x74, 0x12, 0x12, - 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x44, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, - 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x04, - 0x73, 0x74, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61, - 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x22, 0x43, - 0x0a, 0x11, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x12, 0x14, 0x0a, - 0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x72, 0x65, - 0x73, 0x65, 0x74, 0x22, 0x46, 0x0a, 0x12, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x73, 0x74, 0x61, - 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, - 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x2e, 0x53, 0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x22, 0x11, 0x0a, 0x0f, 0x53, - 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa2, - 0x02, 0x0a, 0x10, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4e, 0x75, 0x6d, 0x47, 0x6f, 0x72, 0x6f, 0x75, 0x74, - 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x4e, 0x75, 0x6d, 0x47, 0x6f, - 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x75, 0x6d, 0x47, 0x43, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x4e, 0x75, 0x6d, 0x47, 0x43, 0x12, 0x14, 0x0a, - 0x05, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x41, 0x6c, - 0x6c, 0x6f, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, - 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, - 0x6c, 0x6f, 0x63, 0x12, 0x10, 0x0a, 0x03, 0x53, 0x79, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x03, 0x53, 0x79, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x73, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x4d, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x73, 0x12, - 0x14, 0x0a, 0x05, 0x46, 0x72, 0x65, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, - 0x46, 0x72, 0x65, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x4c, 0x69, 0x76, 0x65, 0x4f, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x4c, 0x69, 0x76, 0x65, - 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x50, 0x61, 0x75, 0x73, 0x65, - 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x50, - 0x61, 0x75, 0x73, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x55, - 0x70, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x55, 0x70, 0x74, - 0x69, 0x6d, 0x65, 0x22, 0xbb, 0x01, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, - 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x49, 0x70, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x4f, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, - 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, - 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x49, 0x70, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x49, 0x70, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x69, 0x70, 0x73, 0x1a, 0x36, 0x0a, 0x08, 0x49, 0x70, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x32, 0x9a, 0x04, 0x0a, 0x0c, - 0x53, 0x74, 0x61, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f, 0x0a, 0x08, - 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, - 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, - 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x65, 0x0a, - 0x0e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x12, - 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, - 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x65, 0x0a, 0x0a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, - 0x74, 0x73, 0x12, 0x29, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, - 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x51, 0x75, 0x65, 0x72, - 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, - 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, - 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x62, 0x0a, 0x0b, 0x47, - 0x65, 0x74, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61, - 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x2e, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, - 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x53, 0x79, 0x73, - 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x77, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, - 0x65, 0x49, 0x70, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, - 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x34, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, - 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, - 0x74, 0x73, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x49, 0x70, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x64, 0x0a, 0x1a, 0x63, 0x6f, 0x6d, 0x2e, - 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, - 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2b, 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, 0x61, 0x70, 0x70, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2f, 0x63, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x16, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, - 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_command_proto_rawDesc = "" + + "\n" + + "\rcommand.proto\x12\x16xray.app.stats.command\";\n" + + "\x0fGetStatsRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05reset\x18\x02 \x01(\bR\x05reset\"0\n" + + "\x04Stat\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05value\x18\x02 \x01(\x03R\x05value\"D\n" + + "\x10GetStatsResponse\x120\n" + + "\x04stat\x18\x01 \x01(\v2\x1c.xray.app.stats.command.StatR\x04stat\"C\n" + + "\x11QueryStatsRequest\x12\x18\n" + + "\apattern\x18\x01 \x01(\tR\apattern\x12\x14\n" + + "\x05reset\x18\x02 \x01(\bR\x05reset\"F\n" + + "\x12QueryStatsResponse\x120\n" + + "\x04stat\x18\x01 \x03(\v2\x1c.xray.app.stats.command.StatR\x04stat\"\x11\n" + + "\x0fSysStatsRequest\"\xa2\x02\n" + + "\x10SysStatsResponse\x12\"\n" + + "\fNumGoroutine\x18\x01 \x01(\rR\fNumGoroutine\x12\x14\n" + + "\x05NumGC\x18\x02 \x01(\rR\x05NumGC\x12\x14\n" + + "\x05Alloc\x18\x03 \x01(\x04R\x05Alloc\x12\x1e\n" + + "\n" + + "TotalAlloc\x18\x04 \x01(\x04R\n" + + "TotalAlloc\x12\x10\n" + + "\x03Sys\x18\x05 \x01(\x04R\x03Sys\x12\x18\n" + + "\aMallocs\x18\x06 \x01(\x04R\aMallocs\x12\x14\n" + + "\x05Frees\x18\a \x01(\x04R\x05Frees\x12 \n" + + "\vLiveObjects\x18\b \x01(\x04R\vLiveObjects\x12\"\n" + + "\fPauseTotalNs\x18\t \x01(\x04R\fPauseTotalNs\x12\x16\n" + + "\x06Uptime\x18\n" + + " \x01(\rR\x06Uptime\"\xbb\x01\n" + + "\x1cGetStatsOnlineIpListResponse\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12O\n" + + "\x03ips\x18\x02 \x03(\v2=.xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntryR\x03ips\x1a6\n" + + "\bIpsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\"\x1a\n" + + "\x18GetAllOnlineUsersRequest\"1\n" + + "\x19GetAllOnlineUsersResponse\x12\x14\n" + + "\x05users\x18\x01 \x03(\tR\x05users\"\b\n" + + "\x06Config2\x96\x05\n" + + "\fStatsService\x12_\n" + + "\bGetStats\x12'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12e\n" + + "\x0eGetStatsOnline\x12'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12e\n" + + "\n" + + "QueryStats\x12).xray.app.stats.command.QueryStatsRequest\x1a*.xray.app.stats.command.QueryStatsResponse\"\x00\x12b\n" + + "\vGetSysStats\x12'.xray.app.stats.command.SysStatsRequest\x1a(.xray.app.stats.command.SysStatsResponse\"\x00\x12w\n" + + "\x14GetStatsOnlineIpList\x12'.xray.app.stats.command.GetStatsRequest\x1a4.xray.app.stats.command.GetStatsOnlineIpListResponse\"\x00\x12z\n" + + "\x11GetAllOnlineUsers\x120.xray.app.stats.command.GetAllOnlineUsersRequest\x1a1.xray.app.stats.command.GetAllOnlineUsersResponse\"\x00Bd\n" + + "\x1acom.xray.app.stats.commandP\x01Z+github.com/xtls/xray-core/app/stats/command\xaa\x02\x16Xray.App.Stats.Commandb\x06proto3" var ( - file_app_stats_command_command_proto_rawDescOnce sync.Once - file_app_stats_command_command_proto_rawDescData = file_app_stats_command_command_proto_rawDesc + file_command_proto_rawDescOnce sync.Once + file_command_proto_rawDescData []byte ) -func file_app_stats_command_command_proto_rawDescGZIP() []byte { - file_app_stats_command_command_proto_rawDescOnce.Do(func() { - file_app_stats_command_command_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_stats_command_command_proto_rawDescData) +func file_command_proto_rawDescGZIP() []byte { + file_command_proto_rawDescOnce.Do(func() { + file_command_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_command_proto_rawDesc), len(file_command_proto_rawDesc))) }) - return file_app_stats_command_command_proto_rawDescData + return file_command_proto_rawDescData } -var file_app_stats_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 10) -var file_app_stats_command_command_proto_goTypes = []any{ +var file_command_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_command_proto_goTypes = []any{ (*GetStatsRequest)(nil), // 0: xray.app.stats.command.GetStatsRequest (*Stat)(nil), // 1: xray.app.stats.command.Stat (*GetStatsResponse)(nil), // 2: xray.app.stats.command.GetStatsResponse @@ -637,51 +662,54 @@ var file_app_stats_command_command_proto_goTypes = []any{ (*SysStatsRequest)(nil), // 5: xray.app.stats.command.SysStatsRequest (*SysStatsResponse)(nil), // 6: xray.app.stats.command.SysStatsResponse (*GetStatsOnlineIpListResponse)(nil), // 7: xray.app.stats.command.GetStatsOnlineIpListResponse - (*Config)(nil), // 8: xray.app.stats.command.Config - nil, // 9: xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry + (*GetAllOnlineUsersRequest)(nil), // 8: xray.app.stats.command.GetAllOnlineUsersRequest + (*GetAllOnlineUsersResponse)(nil), // 9: xray.app.stats.command.GetAllOnlineUsersResponse + (*Config)(nil), // 10: xray.app.stats.command.Config + nil, // 11: xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry } -var file_app_stats_command_command_proto_depIdxs = []int32{ - 1, // 0: xray.app.stats.command.GetStatsResponse.stat:type_name -> xray.app.stats.command.Stat - 1, // 1: xray.app.stats.command.QueryStatsResponse.stat:type_name -> xray.app.stats.command.Stat - 9, // 2: xray.app.stats.command.GetStatsOnlineIpListResponse.ips:type_name -> xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry - 0, // 3: xray.app.stats.command.StatsService.GetStats:input_type -> xray.app.stats.command.GetStatsRequest - 0, // 4: xray.app.stats.command.StatsService.GetStatsOnline:input_type -> xray.app.stats.command.GetStatsRequest - 3, // 5: xray.app.stats.command.StatsService.QueryStats:input_type -> xray.app.stats.command.QueryStatsRequest - 5, // 6: xray.app.stats.command.StatsService.GetSysStats:input_type -> xray.app.stats.command.SysStatsRequest - 0, // 7: xray.app.stats.command.StatsService.GetStatsOnlineIpList:input_type -> xray.app.stats.command.GetStatsRequest - 2, // 8: xray.app.stats.command.StatsService.GetStats:output_type -> xray.app.stats.command.GetStatsResponse - 2, // 9: xray.app.stats.command.StatsService.GetStatsOnline:output_type -> xray.app.stats.command.GetStatsResponse - 4, // 10: xray.app.stats.command.StatsService.QueryStats:output_type -> xray.app.stats.command.QueryStatsResponse - 6, // 11: xray.app.stats.command.StatsService.GetSysStats:output_type -> xray.app.stats.command.SysStatsResponse - 7, // 12: xray.app.stats.command.StatsService.GetStatsOnlineIpList:output_type -> xray.app.stats.command.GetStatsOnlineIpListResponse - 8, // [8:13] is the sub-list for method output_type - 3, // [3:8] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name +var file_command_proto_depIdxs = []int32{ + 1, // 0: xray.app.stats.command.GetStatsResponse.stat:type_name -> xray.app.stats.command.Stat + 1, // 1: xray.app.stats.command.QueryStatsResponse.stat:type_name -> xray.app.stats.command.Stat + 11, // 2: xray.app.stats.command.GetStatsOnlineIpListResponse.ips:type_name -> xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry + 0, // 3: xray.app.stats.command.StatsService.GetStats:input_type -> xray.app.stats.command.GetStatsRequest + 0, // 4: xray.app.stats.command.StatsService.GetStatsOnline:input_type -> xray.app.stats.command.GetStatsRequest + 3, // 5: xray.app.stats.command.StatsService.QueryStats:input_type -> xray.app.stats.command.QueryStatsRequest + 5, // 6: xray.app.stats.command.StatsService.GetSysStats:input_type -> xray.app.stats.command.SysStatsRequest + 0, // 7: xray.app.stats.command.StatsService.GetStatsOnlineIpList:input_type -> xray.app.stats.command.GetStatsRequest + 8, // 8: xray.app.stats.command.StatsService.GetAllOnlineUsers:input_type -> xray.app.stats.command.GetAllOnlineUsersRequest + 2, // 9: xray.app.stats.command.StatsService.GetStats:output_type -> xray.app.stats.command.GetStatsResponse + 2, // 10: xray.app.stats.command.StatsService.GetStatsOnline:output_type -> xray.app.stats.command.GetStatsResponse + 4, // 11: xray.app.stats.command.StatsService.QueryStats:output_type -> xray.app.stats.command.QueryStatsResponse + 6, // 12: xray.app.stats.command.StatsService.GetSysStats:output_type -> xray.app.stats.command.SysStatsResponse + 7, // 13: xray.app.stats.command.StatsService.GetStatsOnlineIpList:output_type -> xray.app.stats.command.GetStatsOnlineIpListResponse + 9, // 14: xray.app.stats.command.StatsService.GetAllOnlineUsers:output_type -> xray.app.stats.command.GetAllOnlineUsersResponse + 9, // [9:15] is the sub-list for method output_type + 3, // [3:9] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } -func init() { file_app_stats_command_command_proto_init() } -func file_app_stats_command_command_proto_init() { - if File_app_stats_command_command_proto != nil { +func init() { file_command_proto_init() } +func file_command_proto_init() { + if File_command_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_app_stats_command_command_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_command_proto_rawDesc), len(file_command_proto_rawDesc)), NumEnums: 0, - NumMessages: 10, + NumMessages: 12, NumExtensions: 0, NumServices: 1, }, - GoTypes: file_app_stats_command_command_proto_goTypes, - DependencyIndexes: file_app_stats_command_command_proto_depIdxs, - MessageInfos: file_app_stats_command_command_proto_msgTypes, + GoTypes: file_command_proto_goTypes, + DependencyIndexes: file_command_proto_depIdxs, + MessageInfos: file_command_proto_msgTypes, }.Build() - File_app_stats_command_command_proto = out.File - file_app_stats_command_command_proto_rawDesc = nil - file_app_stats_command_command_proto_goTypes = nil - file_app_stats_command_command_proto_depIdxs = nil + File_command_proto = out.File + file_command_proto_goTypes = nil + file_command_proto_depIdxs = nil } diff --git a/app/stats/command/command.proto b/app/stats/command/command.proto index 58ed7371..9c3f9131 100644 --- a/app/stats/command/command.proto +++ b/app/stats/command/command.proto @@ -51,12 +51,19 @@ message GetStatsOnlineIpListResponse { map ips = 2; } +message GetAllOnlineUsersRequest {} + +message GetAllOnlineUsersResponse { + repeated string users = 1; +} + service StatsService { rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {} rpc GetStatsOnline(GetStatsRequest) returns (GetStatsResponse) {} rpc QueryStats(QueryStatsRequest) returns (QueryStatsResponse) {} rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {} rpc GetStatsOnlineIpList(GetStatsRequest) returns (GetStatsOnlineIpListResponse) {} + rpc GetAllOnlineUsers(GetAllOnlineUsersRequest) returns (GetAllOnlineUsersResponse) {} } message Config {} diff --git a/app/stats/command/command_grpc.pb.go b/app/stats/command/command_grpc.pb.go index 6f72eadd..9864e7f7 100644 --- a/app/stats/command/command_grpc.pb.go +++ b/app/stats/command/command_grpc.pb.go @@ -1,8 +1,8 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.28.2 -// source: app/stats/command/command.proto +// - protoc v6.32.0 +// source: command.proto package command @@ -24,6 +24,7 @@ const ( StatsService_QueryStats_FullMethodName = "/xray.app.stats.command.StatsService/QueryStats" StatsService_GetSysStats_FullMethodName = "/xray.app.stats.command.StatsService/GetSysStats" StatsService_GetStatsOnlineIpList_FullMethodName = "/xray.app.stats.command.StatsService/GetStatsOnlineIpList" + StatsService_GetAllOnlineUsers_FullMethodName = "/xray.app.stats.command.StatsService/GetAllOnlineUsers" ) // StatsServiceClient is the client API for StatsService service. @@ -35,6 +36,7 @@ type StatsServiceClient interface { QueryStats(ctx context.Context, in *QueryStatsRequest, opts ...grpc.CallOption) (*QueryStatsResponse, error) GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error) GetStatsOnlineIpList(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsOnlineIpListResponse, error) + GetAllOnlineUsers(ctx context.Context, in *GetAllOnlineUsersRequest, opts ...grpc.CallOption) (*GetAllOnlineUsersResponse, error) } type statsServiceClient struct { @@ -95,6 +97,16 @@ func (c *statsServiceClient) GetStatsOnlineIpList(ctx context.Context, in *GetSt return out, nil } +func (c *statsServiceClient) GetAllOnlineUsers(ctx context.Context, in *GetAllOnlineUsersRequest, opts ...grpc.CallOption) (*GetAllOnlineUsersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetAllOnlineUsersResponse) + err := c.cc.Invoke(ctx, StatsService_GetAllOnlineUsers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // StatsServiceServer is the server API for StatsService service. // All implementations must embed UnimplementedStatsServiceServer // for forward compatibility. @@ -104,6 +116,7 @@ type StatsServiceServer interface { QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error) GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) GetStatsOnlineIpList(context.Context, *GetStatsRequest) (*GetStatsOnlineIpListResponse, error) + GetAllOnlineUsers(context.Context, *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error) mustEmbedUnimplementedStatsServiceServer() } @@ -129,6 +142,9 @@ func (UnimplementedStatsServiceServer) GetSysStats(context.Context, *SysStatsReq func (UnimplementedStatsServiceServer) GetStatsOnlineIpList(context.Context, *GetStatsRequest) (*GetStatsOnlineIpListResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetStatsOnlineIpList not implemented") } +func (UnimplementedStatsServiceServer) GetAllOnlineUsers(context.Context, *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetAllOnlineUsers not implemented") +} func (UnimplementedStatsServiceServer) mustEmbedUnimplementedStatsServiceServer() {} func (UnimplementedStatsServiceServer) testEmbeddedByValue() {} @@ -240,6 +256,24 @@ func _StatsService_GetStatsOnlineIpList_Handler(srv interface{}, ctx context.Con return interceptor(ctx, in, info, handler) } +func _StatsService_GetAllOnlineUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAllOnlineUsersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StatsServiceServer).GetAllOnlineUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StatsService_GetAllOnlineUsers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StatsServiceServer).GetAllOnlineUsers(ctx, req.(*GetAllOnlineUsersRequest)) + } + return interceptor(ctx, in, info, handler) +} + // StatsService_ServiceDesc is the grpc.ServiceDesc for StatsService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -267,7 +301,11 @@ var StatsService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetStatsOnlineIpList", Handler: _StatsService_GetStatsOnlineIpList_Handler, }, + { + MethodName: "GetAllOnlineUsers", + Handler: _StatsService_GetAllOnlineUsers_Handler, + }, }, Streams: []grpc.StreamDesc{}, - Metadata: "app/stats/command/command.proto", + Metadata: "command.proto", } diff --git a/app/stats/stats.go b/app/stats/stats.go index 5451c5e6..cc988029 100644 --- a/app/stats/stats.go +++ b/app/stats/stats.go @@ -161,6 +161,21 @@ func (m *Manager) GetChannel(name string) stats.Channel { return nil } +// GetAllOnlineUsers implements stats.Manager. +func (m *Manager) GetAllOnlineUsers() []string { + m.access.Lock() + defer m.access.Unlock() + + usersOnline := make([]string, 0, len(m.onlineMap)) + for user, onlineMap := range m.onlineMap { + if len(onlineMap.IpTimeMap()) > 0 { + usersOnline = append(usersOnline, user) + } + } + + return usersOnline +} + // Start implements common.Runnable. func (m *Manager) Start() error { m.access.Lock() diff --git a/features/stats/stats.go b/features/stats/stats.go index ab5b4067..0ed910e3 100644 --- a/features/stats/stats.go +++ b/features/stats/stats.go @@ -98,6 +98,9 @@ type Manager interface { UnregisterChannel(string) error // GetChannel returns a channel by its identifier. GetChannel(string) Channel + + // GetAllOnlineUsers returns all online users from all OnlineMaps. + GetAllOnlineUsers() []string } // GetOrRegisterCounter tries to get the StatCounter first. If not exist, it then tries to create a new counter. @@ -190,6 +193,11 @@ func (NoopManager) GetChannel(string) Channel { return nil } +// GetAllOnlineUsers implements Manager. +func (NoopManager) GetAllOnlineUsers() []string { + return nil +} + // Start implements common.Runnable. func (NoopManager) Start() error { return nil } diff --git a/main/commands/all/api/api.go b/main/commands/all/api/api.go index 968213b6..cd5e97f4 100644 --- a/main/commands/all/api/api.go +++ b/main/commands/all/api/api.go @@ -32,5 +32,6 @@ var CmdAPI = &base.Command{ cmdSourceIpBlock, cmdOnlineStats, cmdOnlineStatsIpList, + cmdGetAllOnlineUsers, }, } diff --git a/main/commands/all/api/stats_get_all_online_users.go b/main/commands/all/api/stats_get_all_online_users.go new file mode 100644 index 00000000..b8e59f6c --- /dev/null +++ b/main/commands/all/api/stats_get_all_online_users.go @@ -0,0 +1,43 @@ +package api + +import ( + statsService "github.com/xtls/xray-core/app/stats/command" + "github.com/xtls/xray-core/main/commands/base" +) + +var cmdGetAllOnlineUsers = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api statsgetallonlineusers [--server=127.0.0.1:8080]", + Short: "Retrieve array of all online users", + Long: ` +Retrieve array of all online users. + +Arguments: + + -s, -server + The API server address. Default 127.0.0.1:8080 + + -t, -timeout + Timeout in seconds for calling API. Default 3 + +Example: + + {{.Exec}} {{.LongName}} --server=127.0.0.1:8080" +`, + Run: executeGetAllOnlineUsers, +} + +func executeGetAllOnlineUsers(cmd *base.Command, args []string) { + setSharedFlags(cmd) + cmd.Flag.Parse(args) + conn, ctx, close := dialAPIServer() + defer close() + + client := statsService.NewStatsServiceClient(conn) + r := &statsService.GetAllOnlineUsersRequest{} + resp, err := client.GetAllOnlineUsers(ctx, r) + if err != nil { + base.Fatalf("failed to get stats: %s", err) + } + showJSONResponse(resp) +} From 5d94a62a83073bc4aa378fb7e2198e15878e90da Mon Sep 17 00:00:00 2001 From: Hossin Asaadi Date: Wed, 31 Dec 2025 12:20:30 +0330 Subject: [PATCH 073/136] Geofiles: Implement mmap in filesystem to reduce ram usage (#5480) For https://github.com/XTLS/Xray-core/issues/4422 --- common/platform/filesystem/file.go | 26 ++++++++++++ common/platform/filesystem/file_other.go | 54 ++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 common/platform/filesystem/file_other.go diff --git a/common/platform/filesystem/file.go b/common/platform/filesystem/file.go index e4fe2a9a..ebe9b0f1 100644 --- a/common/platform/filesystem/file.go +++ b/common/platform/filesystem/file.go @@ -1,9 +1,12 @@ +//go:build !windows && !wasm + package filesystem import ( "io" "os" "path/filepath" + "syscall" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/platform" @@ -16,6 +19,29 @@ var NewFileReader FileReaderFunc = func(path string) (io.ReadCloser, error) { } func ReadFile(path string) ([]byte, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + stat, err := file.Stat() + if err != nil { + return nil, err + } + + size := stat.Size() + if size == 0 { + return []byte{}, nil + } + + // use mmap to save RAM + bs, err := syscall.Mmap(int(file.Fd()), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED) + if err == nil { + return bs, nil + } + + // fallback reader, err := NewFileReader(path) if err != nil { return nil, err diff --git a/common/platform/filesystem/file_other.go b/common/platform/filesystem/file_other.go new file mode 100644 index 00000000..d3cbdcc7 --- /dev/null +++ b/common/platform/filesystem/file_other.go @@ -0,0 +1,54 @@ +//go:build windows || wasm + +package filesystem + +import ( + "io" + "os" + "path/filepath" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/platform" +) + +type FileReaderFunc func(path string) (io.ReadCloser, error) + +var NewFileReader FileReaderFunc = func(path string) (io.ReadCloser, error) { + return os.Open(path) +} + +func ReadFile(path string) ([]byte, error) { + reader, err := NewFileReader(path) + if err != nil { + return nil, err + } + defer reader.Close() + + return buf.ReadAllToBytes(reader) +} + +func ReadAsset(file string) ([]byte, error) { + return ReadFile(platform.GetAssetLocation(file)) +} + +func ReadCert(file string) ([]byte, error) { + if filepath.IsAbs(file) { + return ReadFile(file) + } + return ReadFile(platform.GetCertLocation(file)) +} + +func CopyFile(dst string, src string) error { + bytes, err := ReadFile(src) + if err != nil { + return err + } + f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + defer f.Close() + + _, err = f.Write(bytes) + return err +} From a54e1f2be4c0817404dc9d3c1132e8a8e3075925 Mon Sep 17 00:00:00 2001 From: yuhan6665 <1588741+yuhan6665@users.noreply.github.com> Date: Wed, 31 Dec 2025 06:00:45 -0500 Subject: [PATCH 074/136] Remove redundant stats in mux and bridge dispatcher (#5466) Fixes https://github.com/XTLS/Xray-core/issues/5446 --- app/dispatcher/default.go | 13 +++++++------ app/reverse/bridge.go | 4 ---- common/mux/server.go | 3 --- features/routing/dispatcher.go | 6 ------ proxy/vless/inbound/inbound.go | 21 +++++++++------------ 5 files changed, 16 insertions(+), 31 deletions(-) diff --git a/app/dispatcher/default.go b/app/dispatcher/default.go index 2ae64902..03722e8a 100644 --- a/app/dispatcher/default.go +++ b/app/dispatcher/default.go @@ -196,7 +196,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran return inboundLink, outboundLink } -func (d *DefaultDispatcher) WrapLink(ctx context.Context, link *transport.Link) *transport.Link { +func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager stats.Manager, link *transport.Link) *transport.Link { sessionInbound := session.InboundFromContext(ctx) var user *protocol.MemoryUser if sessionInbound != nil { @@ -206,16 +206,16 @@ func (d *DefaultDispatcher) WrapLink(ctx context.Context, link *transport.Link) link.Reader = &buf.TimeoutWrapperReader{Reader: link.Reader} if user != nil && len(user.Email) > 0 { - p := d.policy.ForLevel(user.Level) + p := policyManager.ForLevel(user.Level) if p.Stats.UserUplink { name := "user>>>" + user.Email + ">>>traffic>>>uplink" - if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil { + if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil { link.Reader.(*buf.TimeoutWrapperReader).Counter = c } } if p.Stats.UserDownlink { name := "user>>>" + user.Email + ">>>traffic>>>downlink" - if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil { + if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil { link.Writer = &SizeStatWriter{ Counter: c, Writer: link.Writer, @@ -224,7 +224,7 @@ func (d *DefaultDispatcher) WrapLink(ctx context.Context, link *transport.Link) } if p.Stats.UserOnline { name := "user>>>" + user.Email + ">>>online" - if om, _ := stats.GetOrRegisterOnlineMap(d.stats, name); om != nil { + if om, _ := stats.GetOrRegisterOnlineMap(statsManager, name); om != nil { sessionInbounds := session.InboundFromContext(ctx) userIP := sessionInbounds.Source.Address.String() om.AddIP(userIP) @@ -357,7 +357,7 @@ func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.De content = new(session.Content) ctx = session.ContextWithContent(ctx, content) } - outbound = d.WrapLink(ctx, outbound) + outbound = WrapLink(ctx, d.policy, d.stats, outbound) sniffingRequest := content.SniffingRequest if !sniffingRequest.Enabled { d.routedDispatch(ctx, outbound, destination) @@ -449,6 +449,7 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, netw } return contentResult, contentErr } + func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination) { outbounds := session.OutboundsFromContext(ctx) ob := outbounds[len(outbounds)-1] diff --git a/app/reverse/bridge.go b/app/reverse/bridge.go index 324fea59..f6dfec48 100644 --- a/app/reverse/bridge.go +++ b/app/reverse/bridge.go @@ -229,10 +229,6 @@ func (w *BridgeWorker) DispatchLink(ctx context.Context, dest net.Destination, l } return w.Dispatcher.DispatchLink(ctx, dest, link) } - - if d, ok := w.Dispatcher.(routing.WrapLinkDispatcher); ok { - link = d.WrapLink(ctx, link) - } w.handleInternalConn(link) return nil diff --git a/common/mux/server.go b/common/mux/server.go index 1c090185..d1cdac11 100644 --- a/common/mux/server.go +++ b/common/mux/server.go @@ -63,9 +63,6 @@ func (s *Server) DispatchLink(ctx context.Context, dest net.Destination, link *t if dest.Address != muxCoolAddress { return s.dispatcher.DispatchLink(ctx, dest, link) } - if d, ok := s.dispatcher.(routing.WrapLinkDispatcher); ok { - link = d.WrapLink(ctx, link) - } worker, err := NewServerWorker(ctx, s.dispatcher, link) if err != nil { return err diff --git a/features/routing/dispatcher.go b/features/routing/dispatcher.go index c8354446..53d3bf90 100644 --- a/features/routing/dispatcher.go +++ b/features/routing/dispatcher.go @@ -26,9 +26,3 @@ type Dispatcher interface { func DispatcherType() interface{} { return (*Dispatcher)(nil) } - -// Just for type assertion -type WrapLinkDispatcher interface { - Dispatcher - WrapLink(ctx context.Context, link *transport.Link) *transport.Link -} diff --git a/proxy/vless/inbound/inbound.go b/proxy/vless/inbound/inbound.go index eeb1a25f..d12495b4 100644 --- a/proxy/vless/inbound/inbound.go +++ b/proxy/vless/inbound/inbound.go @@ -12,6 +12,7 @@ import ( "time" "unsafe" + "github.com/xtls/xray-core/app/dispatcher" "github.com/xtls/xray-core/app/reverse" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" @@ -31,6 +32,7 @@ import ( "github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/policy" "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/features/stats" "github.com/xtls/xray-core/proxy" "github.com/xtls/xray-core/proxy/vless" "github.com/xtls/xray-core/proxy/vless/encoding" @@ -72,10 +74,11 @@ func init() { type Handler struct { inboundHandlerManager feature_inbound.Manager policyManager policy.Manager + stats stats.Manager validator vless.Validator decryption *encryption.ServerInstance outboundHandlerManager outbound.Manager - wrapLink func(ctx context.Context, link *transport.Link) *transport.Link + defaultDispatcher routing.Dispatcher ctx context.Context fallbacks map[string]map[string]map[string]*Fallback // or nil // regexps map[string]*regexp.Regexp // or nil @@ -84,16 +87,13 @@ type Handler struct { // New creates a new VLess inbound handler. func New(ctx context.Context, config *Config, dc dns.Client, validator vless.Validator) (*Handler, error) { v := core.MustFromContext(ctx) - var wrapLinkFunc func(ctx context.Context, link *transport.Link) *transport.Link - if dispatcher, ok := v.GetFeature(routing.DispatcherType()).(routing.WrapLinkDispatcher); ok { - wrapLinkFunc = dispatcher.WrapLink - } handler := &Handler{ inboundHandlerManager: v.GetFeature(feature_inbound.ManagerType()).(feature_inbound.Manager), policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + stats: v.GetFeature(stats.ManagerType()).(stats.Manager), validator: validator, outboundHandlerManager: v.GetFeature(outbound.ManagerType()).(outbound.Manager), - wrapLink: wrapLinkFunc, + defaultDispatcher: v.GetFeature(routing.DispatcherType()).(routing.Dispatcher), ctx: ctx, } @@ -264,7 +264,7 @@ func (*Handler) Network() []net.Network { } // Process implements proxy.Inbound.Process(). -func (h *Handler) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatcher routing.Dispatcher) error { +func (h *Handler) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatch routing.Dispatcher) error { iConn := stat.TryUnwrapStatsConn(connection) if h.decryption != nil { @@ -623,13 +623,10 @@ func (h *Handler) Process(ctx context.Context, network net.Network, connection s if err != nil { return err } - if h.wrapLink == nil { - return errors.New("VLESS reverse must have a dispatcher that implemented routing.WrapLinkDispatcher") - } - return r.NewMux(ctx, h.wrapLink(ctx, &transport.Link{Reader: clientReader, Writer: clientWriter})) + return r.NewMux(ctx, dispatcher.WrapLink(ctx, h.policyManager, h.stats, &transport.Link{Reader: clientReader, Writer: clientWriter})) } - if err := dispatcher.DispatchLink(ctx, request.Destination(), &transport.Link{ + if err := dispatch.DispatchLink(ctx, request.Destination(), &transport.Link{ Reader: clientReader, Writer: clientWriter}, ); err != nil { From e7c72c011f52255bf9fe434d26063345949012d2 Mon Sep 17 00:00:00 2001 From: fanymagnet Date: Mon, 5 Jan 2026 04:02:04 +0300 Subject: [PATCH 075/136] XHTTP server: Fix ScStreamUpServerSecs' non-default value (#5486) --- transport/internet/splithttp/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/internet/splithttp/config.go b/transport/internet/splithttp/config.go index 21e81aa1..c1481dda 100644 --- a/transport/internet/splithttp/config.go +++ b/transport/internet/splithttp/config.go @@ -118,7 +118,7 @@ func (c *Config) GetNormalizedScStreamUpServerSecs() RangeConfig { } } - return *c.ScMinPostsIntervalMs + return *c.ScStreamUpServerSecs } func (m *XmuxConfig) GetNormalizedMaxConcurrency() RangeConfig { From 7265b5ac3f8f7955a750301f6de262bb8cfcd53f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Mon, 5 Jan 2026 09:12:13 +0800 Subject: [PATCH 076/136] Routing config: Add `processName` (#5489) --- app/router/condition.go | 39 +++++ app/router/config.go | 8 + app/router/config.pb.go | 132 ++++++++-------- app/router/config.proto | 1 + common/net/find_process_linux.go | 178 +++++++++++++++++++++ common/net/find_process_others.go | 11 ++ common/net/find_process_windows.go | 243 +++++++++++++++++++++++++++++ common/net/net.go | 42 ++++- infra/conf/router.go | 37 +++-- 9 files changed, 614 insertions(+), 77 deletions(-) create mode 100644 common/net/find_process_linux.go create mode 100644 common/net/find_process_others.go create mode 100644 common/net/find_process_windows.go diff --git a/app/router/condition.go b/app/router/condition.go index d21487a2..8d9027fe 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -2,6 +2,7 @@ package router import ( "context" + "os" "regexp" "strings" @@ -305,3 +306,41 @@ func (m *AttributeMatcher) Apply(ctx routing.Context) bool { } return m.Match(attributes) } + +type ProcessNameMatcher struct { + names []string +} + +func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool { + srcPort := ctx.GetSourcePort().String() + srcIP := ctx.GetSourceIPs()[0].String() + var network string + switch ctx.GetNetwork() { + case net.Network_TCP: + network = "tcp" + case net.Network_UDP: + network = "udp" + default: + return false + } + src, err := net.ParseDestination(strings.Join([]string{network, srcIP, srcPort}, ":")) + if err != nil { + return false + } + pid, name, err := net.FindProcess(src) + if err != nil { + if err != net.ErrNotLocal { + errors.LogError(context.Background(), "Unables to find local process name: ", err) + } + return false + } + for _, n := range m.names { + if name == "/self" && pid == os.Getpid() { + return true + } + if n == name { + return true + } + } + return false +} diff --git a/app/router/config.go b/app/router/config.go index 9f6a4844..dca62583 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -106,6 +106,14 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { conds.Add(matcher) } + if len(rr.ProcessName) > 0 { + refinedNames := make([]string, 0, len(rr.ProcessName)) + for _, name := range rr.ProcessName { + refinedNames = append(refinedNames, strings.TrimSuffix(name, ".exe")) + } + conds.Add(&ProcessNameMatcher{refinedNames}) + } + if conds.Len() == 0 { return nil, errors.New("this rule has no effective fields").AtWarning() } diff --git a/app/router/config.pb.go b/app/router/config.pb.go index 25f679b0..c5803f56 100644 --- a/app/router/config.pb.go +++ b/app/router/config.pb.go @@ -490,6 +490,7 @@ type RoutingRule struct { LocalGeoip []*GeoIP `protobuf:"bytes,17,rep,name=local_geoip,json=localGeoip,proto3" json:"local_geoip,omitempty"` LocalPortList *net.PortList `protobuf:"bytes,18,opt,name=local_port_list,json=localPortList,proto3" json:"local_port_list,omitempty"` VlessRouteList *net.PortList `protobuf:"bytes,20,opt,name=vless_route_list,json=vlessRouteList,proto3" json:"vless_route_list,omitempty"` + ProcessName []string `protobuf:"bytes,21,rep,name=process_name,json=processName,proto3" json:"process_name,omitempty"` } func (x *RoutingRule) Reset() { @@ -641,6 +642,13 @@ func (x *RoutingRule) GetVlessRouteList() *net.PortList { return nil } +func (x *RoutingRule) GetProcessName() []string { + if x != nil { + return x.ProcessName + } + return nil +} + type isRoutingRule_TargetTag interface { isRoutingRule_TargetTag() } @@ -1081,7 +1089,7 @@ var file_app_router_config_proto_rawDesc = []byte{ 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x53, 0x69, - 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x22, 0xe8, 0x06, 0x0a, 0x0b, 0x52, 0x6f, + 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x8b, 0x07, 0x0a, 0x0b, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x25, 0x0a, 0x0d, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x0c, @@ -1131,66 +1139,68 @@ var file_app_router_config_proto_rawDesc = []byte{ 0x65, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x0e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x1a, 0x3d, 0x0a, 0x0f, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, - 0x5f, 0x74, 0x61, 0x67, 0x22, 0xdc, 0x01, 0x0a, 0x0d, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, - 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x2b, 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x62, - 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, - 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, - 0x79, 0x12, 0x4d, 0x0a, 0x11, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x5f, 0x73, 0x65, - 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, - 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, - 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x10, - 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, - 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x74, 0x61, 0x67, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, - 0x54, 0x61, 0x67, 0x22, 0x54, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x57, - 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x65, 0x78, 0x70, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x72, 0x65, 0x67, 0x65, 0x78, 0x70, 0x12, 0x14, 0x0a, - 0x05, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x61, - 0x74, 0x63, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x02, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xc0, 0x01, 0x0a, 0x17, 0x53, 0x74, - 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x4c, 0x65, 0x61, 0x73, 0x74, 0x4c, 0x6f, 0x61, 0x64, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x35, 0x0a, 0x05, 0x63, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x57, - 0x65, 0x69, 0x67, 0x68, 0x74, 0x52, 0x05, 0x63, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x1c, 0x0a, 0x09, - 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, 0x52, - 0x09, 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, - 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x65, 0x78, - 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, 0x54, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, 0x54, 0x12, 0x1c, - 0x0a, 0x09, 0x74, 0x6f, 0x6c, 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x02, 0x52, 0x09, 0x74, 0x6f, 0x6c, 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x22, 0x90, 0x02, 0x0a, - 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4f, 0x0a, 0x0f, 0x64, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x26, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0e, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x30, 0x0a, 0x04, 0x72, 0x75, 0x6c, 0x65, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, - 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, - 0x52, 0x75, 0x6c, 0x65, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, 0x45, 0x0a, 0x0e, 0x62, 0x61, - 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x72, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, - 0x6c, 0x65, 0x52, 0x0d, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, - 0x65, 0x22, 0x3c, 0x0a, 0x0e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, - 0x65, 0x67, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x73, 0x49, 0x73, 0x10, 0x00, 0x12, 0x10, 0x0a, - 0x0c, 0x49, 0x70, 0x49, 0x66, 0x4e, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x10, 0x02, 0x12, - 0x0e, 0x0a, 0x0a, 0x49, 0x70, 0x4f, 0x6e, 0x44, 0x65, 0x6d, 0x61, 0x6e, 0x64, 0x10, 0x03, 0x42, - 0x4f, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x50, 0x01, 0x5a, 0x24, 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, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0xaa, 0x02, - 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x75, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x63, 0x65, + 0x73, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x15, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x70, + 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x1a, 0x3d, 0x0a, 0x0f, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x5f, 0x74, 0x61, 0x67, 0x22, 0xdc, 0x01, 0x0a, 0x0d, 0x42, 0x61, 0x6c, 0x61, + 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x2b, 0x0a, 0x11, 0x6f, + 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x74, 0x72, 0x61, + 0x74, 0x65, 0x67, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x74, 0x72, 0x61, + 0x74, 0x65, 0x67, 0x79, 0x12, 0x4d, 0x0a, 0x11, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, + 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x52, 0x10, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, + 0x74, 0x61, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x61, 0x6c, 0x6c, 0x62, + 0x61, 0x63, 0x6b, 0x54, 0x61, 0x67, 0x22, 0x54, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, + 0x67, 0x79, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x65, + 0x78, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x72, 0x65, 0x67, 0x65, 0x78, 0x70, + 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xc0, 0x01, 0x0a, + 0x17, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x4c, 0x65, 0x61, 0x73, 0x74, 0x4c, 0x6f, + 0x61, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x35, 0x0a, 0x05, 0x63, 0x6f, 0x73, 0x74, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, + 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, + 0x67, 0x79, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x52, 0x05, 0x63, 0x6f, 0x73, 0x74, 0x73, 0x12, + 0x1c, 0x0a, 0x09, 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x03, 0x52, 0x09, 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x1a, 0x0a, + 0x08, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x08, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61, 0x78, + 0x52, 0x54, 0x54, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, + 0x54, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x6c, 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x02, 0x52, 0x09, 0x74, 0x6f, 0x6c, 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x22, + 0x90, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4f, 0x0a, 0x0f, 0x64, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x44, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0e, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x30, 0x0a, 0x04, 0x72, + 0x75, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61, 0x79, + 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x52, 0x6f, 0x75, 0x74, + 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, 0x45, 0x0a, + 0x0e, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, + 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, + 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, + 0x52, 0x75, 0x6c, 0x65, 0x22, 0x3c, 0x0a, 0x0e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, + 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x73, 0x49, 0x73, 0x10, 0x00, + 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x70, 0x49, 0x66, 0x4e, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, + 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x70, 0x4f, 0x6e, 0x44, 0x65, 0x6d, 0x61, 0x6e, 0x64, + 0x10, 0x03, 0x42, 0x4f, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, + 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x50, 0x01, 0x5a, 0x24, 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, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, + 0x72, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/app/router/config.proto b/app/router/config.proto index f26bccb4..222f395d 100644 --- a/app/router/config.proto +++ b/app/router/config.proto @@ -113,6 +113,7 @@ message RoutingRule { xray.common.net.PortList local_port_list = 18; xray.common.net.PortList vless_route_list = 20; + repeated string process_name = 21; } message BalancingRule { diff --git a/common/net/find_process_linux.go b/common/net/find_process_linux.go new file mode 100644 index 00000000..80080b70 --- /dev/null +++ b/common/net/find_process_linux.go @@ -0,0 +1,178 @@ +//go:build linux + +package net + +import ( + "bufio" + "encoding/hex" + "fmt" + "os" + "strconv" + "strings" + + "github.com/xtls/xray-core/common/errors" +) + +func FindProcess(dest Destination) (int, string, error) { + isLocal, err := IsLocal(dest.Address.IP()) + if err != nil { + return 0, "", errors.New("failed to determine if address is local: ", err) + } + if !isLocal { + return 0, "", ErrNotLocal + } + if dest.Network != Network_TCP && dest.Network != Network_UDP { + panic("Unsupported network type for process lookup.") + } + // the core should never has a domain as source(? + if dest.Address.Family() == AddressFamilyDomain { + panic("Domain addresses are not supported for process lookup.") + } + var procFile string + + switch dest.Network { + case Network_TCP: + if dest.Address.Family() == AddressFamilyIPv4 { + procFile = "/proc/net/tcp" + } + if dest.Address.Family() == AddressFamilyIPv6 { + procFile = "/proc/net/tcp6" + } + case Network_UDP: + if dest.Address.Family() == AddressFamilyIPv4 { + procFile = "/proc/net/udp" + } + if dest.Address.Family() == AddressFamilyIPv6 { + procFile = "/proc/net/udp6" + } + default: + panic("Unsupported network type for process lookup.") + } + + targetHexAddr, err := formatLittleEndianString(dest.Address, dest.Port) + if err != nil { + return 0, "", errors.New("failed to format address: ", err) + } + + inode, err := findInodeInFile(procFile, targetHexAddr) + if err != nil { + return 0, "", errors.New("could not search in ", procFile).Base(err) + } + if inode == "" { + return 0, "", errors.New("connection for ", dest.Address, ":", dest.Port, " not found in ", procFile) + } + + pidStr, err := findPidByInode(inode) + if err != nil { + return 0, "", errors.New("could not find PID for inode ", inode, ": ", err) + } + if pidStr == "" { + return 0, "", errors.New("no process found for inode ", inode) + } + + procName, err := getProcessName(pidStr) + if err != nil { + return 0, "", fmt.Errorf("could not get process name for PID %s: %w", pidStr, err) + } + + pid, err := strconv.Atoi(pidStr) + if err != nil { + return 0, "", errors.New("failed to parse PID: ", err) + } + + return pid, procName, nil +} + +func formatLittleEndianString(addr Address, port Port) (string, error) { + ip := addr.IP() + var ipBytes []byte + if addr.Family() == AddressFamilyIPv4 { + ipBytes = ip.To4() + } else { + ipBytes = ip.To16() + } + if ipBytes == nil { + return "", errors.New("invalid IP format for ", addr.Family(), ": ", ip) + } + + for i, j := 0, len(ipBytes)-1; i < j; i, j = i+1, j-1 { + ipBytes[i], ipBytes[j] = ipBytes[j], ipBytes[i] + } + portHex := fmt.Sprintf("%04X", uint16(port)) + ipHex := strings.ToUpper(hex.EncodeToString(ipBytes)) + return fmt.Sprintf("%s:%s", ipHex, portHex), nil +} + +func findInodeInFile(filePath, targetHexAddr string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + + if len(fields) < 10 { + continue + } + + localAddress := fields[1] + if localAddress == targetHexAddr { + inode := fields[9] + return inode, nil + } + } + + return "", scanner.Err() +} + +func findPidByInode(inode string) (string, error) { + procDir, err := os.ReadDir("/proc") + if err != nil { + return "", err + } + + targetLink := "socket:[" + inode + "]" + + for _, entry := range procDir { + if !entry.IsDir() { + continue + } + pid := entry.Name() + if _, err := strconv.Atoi(pid); err != nil { + continue + } + + fdPath := fmt.Sprintf("/proc/%s/fd", pid) + fdDir, err := os.ReadDir(fdPath) + if err != nil { + continue + } + + for _, fdEntry := range fdDir { + linkPath := fmt.Sprintf("%s/%s", fdPath, fdEntry.Name()) + linkTarget, err := os.Readlink(linkPath) + if err != nil { + continue + } + if linkTarget == targetLink { + return pid, nil + } + } + } + return "", nil +} + +func getProcessName(pid string) (string, error) { + path := fmt.Sprintf("/proc/%s/comm", pid) + content, err := os.ReadFile(path) + if err != nil { + return "", err + } + // remove trailing \n + return strings.TrimSpace(string(content)), nil +} diff --git a/common/net/find_process_others.go b/common/net/find_process_others.go new file mode 100644 index 00000000..472d4f85 --- /dev/null +++ b/common/net/find_process_others.go @@ -0,0 +1,11 @@ +//go:build !windows && !linux + +package net + +import ( + "github.com/xtls/xray-core/common/errors" +) + +func FindProcess(dest Destination) (int, string, error) { + return 0, "", errors.New("process lookup is not supported on this platform") +} diff --git a/common/net/find_process_windows.go b/common/net/find_process_windows.go new file mode 100644 index 00000000..fe223254 --- /dev/null +++ b/common/net/find_process_windows.go @@ -0,0 +1,243 @@ +//go:build windows + +package net + +import ( + "net/netip" + "strings" + "sync" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" + + "github.com/xtls/xray-core/common/errors" +) + +const ( + tcpTableFunc = "GetExtendedTcpTable" + tcpTablePidConn = 4 + udpTableFunc = "GetExtendedUdpTable" + udpTablePid = 1 +) + +var ( + getExTCPTable uintptr + getExUDPTable uintptr + + once sync.Once + initErr error +) + +func initWin32API() error { + h, err := windows.LoadLibrary("iphlpapi.dll") + if err != nil { + return errors.New("LoadLibrary iphlpapi.dll failed").Base(err) + } + + getExTCPTable, err = windows.GetProcAddress(h, tcpTableFunc) + if err != nil { + return errors.New("GetProcAddress of ", tcpTableFunc, " failed").Base(err) + } + + getExUDPTable, err = windows.GetProcAddress(h, udpTableFunc) + if err != nil { + return errors.New("GetProcAddress of ", udpTableFunc, " failed").Base(err) + } + + return nil +} + +func FindProcess(dest Destination) (int, string, error) { + once.Do(func() { + initErr = initWin32API() + }) + if initErr != nil { + return 0, "", initErr + } + isLocal, err := IsLocal(dest.Address.IP()) + if err != nil { + return 0, "", errors.New("failed to determine if address is local: ", err) + } + if !isLocal { + return 0, "", ErrNotLocal + } + if dest.Network != Network_TCP && dest.Network != Network_UDP { + panic("Unsupported network type for process lookup.") + } + // the core should never has a domain as source(? + if dest.Address.Family() == AddressFamilyDomain { + panic("Domain addresses are not supported for process lookup.") + } + var class int + var fn uintptr + switch dest.Network { + case Network_TCP: + fn = getExTCPTable + class = tcpTablePidConn + case Network_UDP: + fn = getExUDPTable + class = udpTablePid + default: + panic("Unsupported network type for process lookup.") + } + ip := dest.Address.IP() + port := int(dest.Port) + + addr, ok := netip.AddrFromSlice(ip) + if !ok { + return 0, "", errors.New("invalid IP address") + } + addr = addr.Unmap() + + family := windows.AF_INET + if addr.Is6() { + family = windows.AF_INET6 + } + + buf, err := getTransportTable(fn, family, class) + if err != nil { + return 0, "", err + } + + s := newSearcher(dest.Network, dest.Address.Family()) + + pid, err := s.Search(buf, addr, uint16(port)) + if err != nil { + return 0, "", err + } + name, err := getExecPathFromPID(pid) + // drop .exe + name = strings.TrimSuffix(name, ".exe") + return int(pid), name, err +} + +type searcher struct { + itemSize int + port int + ip int + ipSize int + pid int + tcpState int +} + +func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) { + n := int(readNativeUint32(b[:4])) + itemSize := s.itemSize + for i := range n { + row := b[4+itemSize*i : 4+itemSize*(i+1)] + + if s.tcpState >= 0 { + tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4]) + // MIB_TCP_STATE_ESTAB, only check established connections for TCP + if tcpState != 5 { + continue + } + } + + // according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian. + // this field can be illustrated as follows depends on different machine endianess: + // little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB) + // big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB) + // so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32 + srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4]))) + if srcPort != port { + continue + } + + srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize]) + srcIP = srcIP.Unmap() + // windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto + if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) { + continue + } + + pid := readNativeUint32(row[s.pid : s.pid+4]) + return pid, nil + } + return 0, errors.New("not found") +} + +func newSearcher(network Network, family AddressFamily) *searcher { + var itemSize, port, ip, ipSize, pid int + tcpState := -1 + switch network { + case Network_TCP: + if family == AddressFamilyIPv4 { + // struct MIB_TCPROW_OWNER_PID + itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0 + } + if family == AddressFamilyIPv6 { + // struct MIB_TCP6ROW_OWNER_PID + itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48 + } + case Network_UDP: + if family == AddressFamilyIPv4 { + // struct MIB_UDPROW_OWNER_PID + itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8 + } + if family == AddressFamilyIPv6 { + // struct MIB_UDP6ROW_OWNER_PID + itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24 + } + } + + return &searcher{ + itemSize: itemSize, + port: port, + ip: ip, + ipSize: ipSize, + pid: pid, + tcpState: tcpState, + } +} + +func getTransportTable(fn uintptr, family int, class int) ([]byte, error) { + for size, buf := uint32(8), make([]byte, 8); ; { + ptr := unsafe.Pointer(&buf[0]) + err, _, _ := syscall.Syscall6(fn, 6, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0) + + switch err { + case 0: + return buf, nil + case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER): + buf = make([]byte, size) + default: + return nil, errors.New("syscall error: ", int(err)) + } + } +} + +func readNativeUint32(b []byte) uint32 { + return *(*uint32)(unsafe.Pointer(&b[0])) +} + +func getExecPathFromPID(pid uint32) (string, error) { + // kernel process starts with a colon in order to distinguish with normal processes + switch pid { + case 0: + // reserved pid for system idle process + return ":System Idle Process", nil + case 4: + // reserved pid for windows kernel image + return ":System", nil + } + h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid) + if err != nil { + return "", err + } + defer windows.CloseHandle(h) + + buf := make([]uint16, syscall.MAX_LONG_PATH) + size := uint32(len(buf)) + err = windows.QueryFullProcessImageName(h, 0, &buf[0], &size) + if err != nil { + return "", err + } + // full path will like: C:\Windows\System32\curl.exe + // we only need the executable name + fullPathName := syscall.UTF16ToString(buf[:size]) + nameSplit := strings.Split(fullPathName, "\\") + name := nameSplit[len(nameSplit)-1] + return name, nil +} diff --git a/common/net/net.go b/common/net/net.go index 92431c42..115ede7d 100644 --- a/common/net/net.go +++ b/common/net/net.go @@ -1,7 +1,13 @@ // Package net is a drop-in replacement to Golang's net package, with some more functionalities. package net // import "github.com/xtls/xray-core/common/net" -import "time" +import ( + "net" + "sync/atomic" + "time" + + "github.com/xtls/xray-core/common/errors" +) // defines the maximum time an idle TCP session can survive in the tunnel, so // it should be consistent across HTTP versions and with other transports. @@ -12,3 +18,37 @@ const QuicgoH3KeepAlivePeriod = 10 * time.Second // consistent with chrome const ChromeH2KeepAlivePeriod = 45 * time.Second + +var ErrNotLocal = errors.New("the source address is not from local machine.") + +type localIPCahceEntry struct { + addrs []net.Addr + lastUpdate time.Time +} + +var localIPCahce = atomic.Pointer[localIPCahceEntry]{} + +func IsLocal(ip net.IP) (bool, error) { + var addrs []net.Addr + if entry := localIPCahce.Load(); entry == nil || time.Since(entry.lastUpdate) > time.Minute { + var err error + addrs, err = net.InterfaceAddrs() + if err != nil { + return false, err + } + localIPCahce.Store(&localIPCahceEntry{ + addrs: addrs, + lastUpdate: time.Now(), + }) + } else { + addrs = entry.addrs + } + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ipnet.IP.Equal(ip) { + return true, nil + } + } + } + return false, nil +} diff --git a/infra/conf/router.go b/infra/conf/router.go index 6e09038b..38158b78 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -520,21 +520,22 @@ func ToCidrList(ips StringList) ([]*router.GeoIP, error) { func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) { type RawFieldRule struct { RouterRule - Domain *StringList `json:"domain"` - Domains *StringList `json:"domains"` - IP *StringList `json:"ip"` - Port *PortList `json:"port"` - Network *NetworkList `json:"network"` - SourceIP *StringList `json:"sourceIP"` - Source *StringList `json:"source"` - SourcePort *PortList `json:"sourcePort"` - User *StringList `json:"user"` - VlessRoute *PortList `json:"vlessRoute"` - InboundTag *StringList `json:"inboundTag"` - Protocols *StringList `json:"protocol"` - Attributes map[string]string `json:"attrs"` - LocalIP *StringList `json:"localIP"` - LocalPort *PortList `json:"localPort"` + Domain *StringList `json:"domain"` + Domains *StringList `json:"domains"` + IP *StringList `json:"ip"` + Port *PortList `json:"port"` + Network *NetworkList `json:"network"` + SourceIP *StringList `json:"sourceIP"` + Source *StringList `json:"source"` + SourcePort *PortList `json:"sourcePort"` + User *StringList `json:"user"` + VlessRoute *PortList `json:"vlessRoute"` + InboundTag *StringList `json:"inboundTag"` + Protocols *StringList `json:"protocol"` + Attributes map[string]string `json:"attrs"` + LocalIP *StringList `json:"localIP"` + LocalPort *PortList `json:"localPort"` + ProcessName *StringList `json:"processName"` } rawFieldRule := new(RawFieldRule) err := json.Unmarshal(msg, rawFieldRule) @@ -647,6 +648,12 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) { rule.Attributes = rawFieldRule.Attributes } + if rawFieldRule.ProcessName != nil { + for _, s := range *rawFieldRule.ProcessName { + rule.ProcessName = append(rule.ProcessName, s) + } + } + return rule, nil } From b38a41249fd80c08c2a32e2dc5b06d42abc0bc9f Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Mon, 5 Jan 2026 08:42:35 +0000 Subject: [PATCH 077/136] README.md: Re-add 3X-UI to Web Panels https://github.com/XTLS/Xray-core/pull/3884#issuecomment-3678495173 https://github.com/XTLS/Xray-core/issues/5478#issuecomment-3700567911 --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7e13caaf..017d868c 100644 --- a/README.md +++ b/README.md @@ -56,12 +56,13 @@ - [ghcr.io/xtls/xray-core](https://ghcr.io/xtls/xray-core) (**Official**) - [teddysun/xray](https://hub.docker.com/r/teddysun/xray) - [wulabing/xray_docker](https://github.com/wulabing/xray_docker) -- Web Panel - **WARNING: Please DO NOT USE plain HTTP panels like 3X-UI**, as they are believed to be bribed by Iran GFW for supporting plain HTTP by default and refused to change (https://github.com/XTLS/Xray-core/pull/3884#issuecomment-2439595331), which has already put many users' data security in danger in the past few years. **If you are already using 3X-UI, please switch to the following panels, which are verified to support HTTPS and SSH port forwarding only:** +- Web Panel - [Remnawave](https://github.com/remnawave/panel) - - [X-Panel](https://github.com/xeefei/X-Panel) + - [3X-UI](https://github.com/MHSanaei/3x-ui) - [PasarGuard](https://github.com/PasarGuard/panel) - - [Marzban](https://github.com/Gozargah/Marzban) - [Xray-UI](https://github.com/qist/xray-ui) + - [X-Panel](https://github.com/xeefei/X-Panel) + - [Marzban](https://github.com/Gozargah/Marzban) - [Hiddify](https://github.com/hiddify/Hiddify-Manager) - One Click - [Xray-REALITY](https://github.com/zxcvos/Xray-script), [xray-reality](https://github.com/sajjaddg/xray-reality), [reality-ezpz](https://github.com/aleskxyz/reality-ezpz) From c715154309e41e759f544edd3667889e257fb3af Mon Sep 17 00:00:00 2001 From: Hossin Asaadi Date: Tue, 6 Jan 2026 02:32:40 +0330 Subject: [PATCH 078/136] Routing: Reduce peak memory usage (#5488) https://github.com/XTLS/Xray-core/pull/5488#issuecomment-3711430369 For https://github.com/XTLS/Xray-core/issues/4422 --- app/router/condition.go | 44 +++++- app/router/config.go | 101 +++++++++++++- common/platform/filesystem/asset_tools.go | 52 ++++++++ infra/conf/router.go | 155 ++++++---------------- 4 files changed, 236 insertions(+), 116 deletions(-) create mode 100644 common/platform/filesystem/asset_tools.go diff --git a/app/router/condition.go b/app/router/condition.go index 8d9027fe..4e7b406b 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -307,6 +307,48 @@ func (m *AttributeMatcher) Apply(ctx routing.Context) bool { return m.Match(attributes) } +// Geo attribute +type GeoAttributeMatcher interface { + Match(*Domain) bool +} + +type GeoBooleanMatcher string + +func (m GeoBooleanMatcher) Match(domain *Domain) bool { + for _, attr := range domain.Attribute { + if attr.Key == string(m) { + return true + } + } + return false +} + +type GeoAttributeList struct { + Matcher []GeoAttributeMatcher +} + +func (al *GeoAttributeList) Match(domain *Domain) bool { + for _, matcher := range al.Matcher { + if !matcher.Match(domain) { + return false + } + } + return true +} + +func (al *GeoAttributeList) IsEmpty() bool { + return len(al.Matcher) == 0 +} + +func ParseAttrs(attrs []string) *GeoAttributeList { + al := new(GeoAttributeList) + for _, attr := range attrs { + lc := strings.ToLower(attr) + al.Matcher = append(al.Matcher, GeoBooleanMatcher(lc)) + } + return al +} + type ProcessNameMatcher struct { names []string } @@ -343,4 +385,4 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool { } } return false -} +} \ No newline at end of file diff --git a/app/router/config.go b/app/router/config.go index dca62583..26833f46 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -3,11 +3,14 @@ package router import ( "context" "regexp" + "runtime" "strings" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/routing" + "google.golang.org/protobuf/proto" ) type Rule struct { @@ -73,7 +76,15 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } if len(rr.Geoip) > 0 { - cond, err := NewIPMatcher(rr.Geoip, MatcherAsType_Target) + geoip := rr.Geoip + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { + var err error + geoip, err = getGeoIPList(rr.Geoip) + if err != nil { + return nil, errors.New("failed to build geoip from mmap").Base(err) + } + } + cond, err := NewIPMatcher(geoip, MatcherAsType_Target) if err != nil { return nil, err } @@ -98,7 +109,16 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } if len(rr.Domain) > 0 { - matcher, err := NewMphMatcherGroup(rr.Domain) + domains := rr.Domain + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { + var err error + domains, err = getDomainList(rr.Domain) + if err != nil { + return nil, errors.New("failed to build domains from mmap").Base(err) + } + } + + matcher, err := NewMphMatcherGroup(domains) if err != nil { return nil, errors.New("failed to build domain condition with MphDomainMatcher").Base(err) } @@ -167,3 +187,80 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch return nil, errors.New("unrecognized balancer type") } } + +func getGeoIPList(ips []*GeoIP) ([]*GeoIP, error) { + geoipList := []*GeoIP{} + for _, ip := range ips { + if ip.CountryCode != "" { + val := strings.Split(ip.CountryCode, "_") + fileName := "geoip.dat" + if len(val) == 2 { + fileName = strings.ToLower(val[0]) + } + bs, err := filesystem.ReadAsset(fileName) + if err != nil { + return nil, errors.New("failed to load file: ", fileName).Base(err) + } + bs = filesystem.Find(bs, []byte(ip.CountryCode)) + + var geoip GeoIP + + if err := proto.Unmarshal(bs, &geoip); err != nil { + return nil, errors.New("failed Unmarshal :").Base(err) + } + geoipList = append(geoipList, &geoip) + + } else { + geoipList = append(geoipList, ip) + } + } + return geoipList, nil + +} + +func getDomainList(domains []*Domain) ([]*Domain, error) { + domainList := []*Domain{} + for _, domain := range domains { + val := strings.Split(domain.Value, "_") + + if len(val) >= 2 { + + fileName := val[0] + code := val[1] + + bs, err := filesystem.ReadAsset(fileName) + if err != nil { + return nil, errors.New("failed to load file: ", fileName).Base(err) + } + bs = filesystem.Find(bs, []byte(code)) + var geosite GeoSite + + if err := proto.Unmarshal(bs, &geosite); err != nil { + return nil, errors.New("failed Unmarshal :").Base(err) + } + + // parse attr + if len(val) == 3 { + siteWithAttr := strings.Split(val[2], ",") + attrs := ParseAttrs(siteWithAttr) + + if !attrs.IsEmpty() { + filteredDomains := make([]*Domain, 0, len(domains)) + for _, domain := range geosite.Domain { + if attrs.Match(domain) { + filteredDomains = append(filteredDomains, domain) + } + } + geosite.Domain = filteredDomains + } + + } + + domainList = append(domainList, geosite.Domain...) + + } else { + domainList = append(domainList, domain) + } + } + return domainList, nil +} diff --git a/common/platform/filesystem/asset_tools.go b/common/platform/filesystem/asset_tools.go new file mode 100644 index 00000000..45316f80 --- /dev/null +++ b/common/platform/filesystem/asset_tools.go @@ -0,0 +1,52 @@ +package filesystem + +func DecodeVarint(buf []byte) (x uint64, n int) { + for shift := uint(0); shift < 64; shift += 7 { + if n >= len(buf) { + return 0, 0 + } + b := uint64(buf[n]) + n++ + x |= (b & 0x7F) << shift + if (b & 0x80) == 0 { + return x, n + } + } + + // The number is too large to represent in a 64-bit value. + return 0, 0 +} + +func Find(data, code []byte) []byte { + codeL := len(code) + if codeL == 0 { + return nil + } + for { + dataL := len(data) + if dataL < 2 { + return nil + } + x, y := DecodeVarint(data[1:]) + if x == 0 && y == 0 { + return nil + } + headL, bodyL := 1+y, int(x) + dataL -= headL + if dataL < bodyL { + return nil + } + data = data[headL:] + if int(data[1]) == codeL { + for i := 0; i < codeL && data[2+i] == code[i]; i++ { + if i+1 == codeL { + return data[:bodyL] + } + } + } + if dataL == bodyL { + return nil + } + data = data[bodyL:] + } +} diff --git a/infra/conf/router.go b/infra/conf/router.go index 38158b78..8aa15065 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -203,17 +203,23 @@ func loadFile(file string) ([]byte, error) { func loadIP(file, code string) ([]*router.CIDR, error) { index := file + ":" + code if IPCache[index] == nil { - bs, err := loadFile(file) - if err != nil { - return nil, errors.New("failed to load file: ", file).Base(err) - } - bs = find(bs, []byte(code)) - if bs == nil { - return nil, errors.New("code not found in ", file, ": ", code) - } var geoip router.GeoIP - if err := proto.Unmarshal(bs, &geoip); err != nil { - return nil, errors.New("error unmarshal IP in ", file, ": ", code).Base(err) + + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { + // dont pass code becuase we have country code in top level router.GeoIP + geoip = router.GeoIP{Cidr: []*router.CIDR{}} + } else { + bs, err := loadFile(file) + if err != nil { + return nil, errors.New("failed to load file: ", file).Base(err) + } + bs = filesystem.Find(bs, []byte(code)) + if bs == nil { + return nil, errors.New("code not found in ", file, ": ", code) + } + if err := proto.Unmarshal(bs, &geoip); err != nil { + return nil, errors.New("error unmarshal IP in ", file, ": ", code).Base(err) + } } defer runtime.GC() // or debug.FreeOSMemory() return geoip.Cidr, nil // do not cache geoip @@ -225,18 +231,28 @@ func loadIP(file, code string) ([]*router.CIDR, error) { func loadSite(file, code string) ([]*router.Domain, error) { index := file + ":" + code if SiteCache[index] == nil { - bs, err := loadFile(file) - if err != nil { - return nil, errors.New("failed to load file: ", file).Base(err) - } - bs = find(bs, []byte(code)) - if bs == nil { - return nil, errors.New("list not found in ", file, ": ", code) - } var geosite router.GeoSite - if err := proto.Unmarshal(bs, &geosite); err != nil { - return nil, errors.New("error unmarshal Site in ", file, ": ", code).Base(err) + + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { + // pass file:code so can build optimized matcher later + domain := router.Domain{Value: file + "_" + code} + geosite = router.GeoSite{Domain: []*router.Domain{&domain}} + + } else { + + bs, err := loadFile(file) + if err != nil { + return nil, errors.New("failed to load file: ", file).Base(err) + } + bs = filesystem.Find(bs, []byte(code)) + if bs == nil { + return nil, errors.New("list not found in ", file, ": ", code) + } + if err := proto.Unmarshal(bs, &geosite); err != nil { + return nil, errors.New("error unmarshal Site in ", file, ": ", code).Base(err) + } } + defer runtime.GC() // or debug.FreeOSMemory() return geosite.Domain, nil // do not cache geosite SiteCache[index] = &geosite @@ -244,105 +260,13 @@ func loadSite(file, code string) ([]*router.Domain, error) { return SiteCache[index].Domain, nil } -func DecodeVarint(buf []byte) (x uint64, n int) { - for shift := uint(0); shift < 64; shift += 7 { - if n >= len(buf) { - return 0, 0 - } - b := uint64(buf[n]) - n++ - x |= (b & 0x7F) << shift - if (b & 0x80) == 0 { - return x, n - } - } - - // The number is too large to represent in a 64-bit value. - return 0, 0 -} - -func find(data, code []byte) []byte { - codeL := len(code) - if codeL == 0 { - return nil - } - for { - dataL := len(data) - if dataL < 2 { - return nil - } - x, y := DecodeVarint(data[1:]) - if x == 0 && y == 0 { - return nil - } - headL, bodyL := 1+y, int(x) - dataL -= headL - if dataL < bodyL { - return nil - } - data = data[headL:] - if int(data[1]) == codeL { - for i := 0; i < codeL && data[2+i] == code[i]; i++ { - if i+1 == codeL { - return data[:bodyL] - } - } - } - if dataL == bodyL { - return nil - } - data = data[bodyL:] - } -} - -type AttributeMatcher interface { - Match(*router.Domain) bool -} - -type BooleanMatcher string - -func (m BooleanMatcher) Match(domain *router.Domain) bool { - for _, attr := range domain.Attribute { - if attr.Key == string(m) { - return true - } - } - return false -} - -type AttributeList struct { - matcher []AttributeMatcher -} - -func (al *AttributeList) Match(domain *router.Domain) bool { - for _, matcher := range al.matcher { - if !matcher.Match(domain) { - return false - } - } - return true -} - -func (al *AttributeList) IsEmpty() bool { - return len(al.matcher) == 0 -} - -func parseAttrs(attrs []string) *AttributeList { - al := new(AttributeList) - for _, attr := range attrs { - lc := strings.ToLower(attr) - al.matcher = append(al.matcher, BooleanMatcher(lc)) - } - return al -} - func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) { parts := strings.Split(siteWithAttr, "@") if len(parts) == 0 { return nil, errors.New("empty site") } country := strings.ToUpper(parts[0]) - attrs := parseAttrs(parts[1:]) + attrs := router.ParseAttrs(parts[1:]) domains, err := loadSite(file, country) if err != nil { return nil, err @@ -352,6 +276,11 @@ func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, er return domains, nil } + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { + domains[0].Value = domains[0].Value + "_" + strings.Join(parts[1:], ",") + return domains, nil + } + filteredDomains := make([]*router.Domain, 0, len(domains)) for _, domain := range domains { if attrs.Match(domain) { From 961c352127f32c04034a88d021a683e6a80eac20 Mon Sep 17 00:00:00 2001 From: Hossin Asaadi Date: Tue, 6 Jan 2026 15:51:50 +0330 Subject: [PATCH 079/136] DNS: Fix parse domain and geoip (#5499) Fixes https://github.com/XTLS/Xray-core/pull/5488#issuecomment-3712856715 --- app/dns/dns.go | 95 +++++++++++++++++++++++++++++++++++++++++++ app/dns/dns_test.go | 8 ++-- app/dns/nameserver.go | 15 +++++++ app/router/config.go | 4 +- infra/conf/dns.go | 17 +------- 5 files changed, 117 insertions(+), 22 deletions(-) diff --git a/app/dns/dns.go b/app/dns/dns.go index 603640f1..5d6154f9 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -12,12 +12,15 @@ import ( "sync" "time" + router "github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/strmatcher" "github.com/xtls/xray-core/features/dns" + "google.golang.org/protobuf/proto" ) // DNS is a DNS rely server. @@ -97,6 +100,25 @@ func New(ctx context.Context, config *Config) (*DNS, error) { } for _, ns := range config.NameServer { + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { + err := parseDomains(ns) + if err != nil { + return nil, errors.New("failed to parse dns domain rules: ").Base(err) + } + + expectedGeoip, err := router.GetGeoIPList(ns.ExpectedGeoip) + if err != nil { + return nil, errors.New("failed to parse dns expectIPs rules: ").Base(err) + } + ns.ExpectedGeoip = expectedGeoip + + unexpectedGeoip, err := router.GetGeoIPList(ns.UnexpectedGeoip) + if err != nil { + return nil, errors.New("failed to parse dns unexpectedGeoip rules: ").Base(err) + } + ns.UnexpectedGeoip = unexpectedGeoip + + } domainRuleCount += len(ns.PrioritizedDomain) } @@ -580,3 +602,76 @@ func detectGUIPlatform() bool { } return false } + +func parseDomains(ns *NameServer) error { + pureDomains := []*router.Domain{} + + // convert to pure domain + for _, pd := range ns.PrioritizedDomain { + pureDomains = append(pureDomains, &router.Domain{ + Type: router.Domain_Type(pd.Type), + Value: pd.Domain, + }) + } + + domainList := []*router.Domain{} + for _, domain := range pureDomains { + val := strings.Split(domain.Value, "_") + if len(val) >= 2 { + + fileName := val[0] + code := val[1] + + bs, err := filesystem.ReadAsset(fileName) + if err != nil { + return errors.New("failed to load file: ", fileName).Base(err) + } + bs = filesystem.Find(bs, []byte(code)) + var geosite router.GeoSite + + if err := proto.Unmarshal(bs, &geosite); err != nil { + return errors.New("failed Unmarshal :").Base(err) + } + + // parse attr + if len(val) == 3 { + siteWithAttr := strings.Split(val[2], ",") + attrs := router.ParseAttrs(siteWithAttr) + if !attrs.IsEmpty() { + filteredDomains := make([]*router.Domain, 0, len(pureDomains)) + for _, domain := range geosite.Domain { + if attrs.Match(domain) { + filteredDomains = append(filteredDomains, domain) + } + } + geosite.Domain = filteredDomains + } + + } + + domainList = append(domainList, geosite.Domain...) + + // update ns.OriginalRules Size + ruleTag := strings.Join(val, ":") + for i, oRule := range ns.OriginalRules { + if oRule.Rule == strings.ToLower(ruleTag) { + ns.OriginalRules[i].Size = uint32(len(geosite.Domain)) + } + } + + } else { + domainList = append(domainList, domain) + } + } + + // convert back to NameServer_PriorityDomain + ns.PrioritizedDomain = []*NameServer_PriorityDomain{} + for _, pd := range domainList { + ns.PrioritizedDomain = append(ns.PrioritizedDomain, &NameServer_PriorityDomain{ + Type: ToDomainMatchingType(pd.Type), + Domain: pd.Value, + }) + } + + return nil +} diff --git a/app/dns/dns_test.go b/app/dns/dns_test.go index cb70b0b3..d103704c 100644 --- a/app/dns/dns_test.go +++ b/app/dns/dns_test.go @@ -541,7 +541,7 @@ func TestIPMatch(t *testing.T) { }, ExpectedGeoip: []*router.GeoIP{ { - CountryCode: "local", + // local Cidr: []*router.CIDR{ { // inner ip, will not match @@ -565,7 +565,7 @@ func TestIPMatch(t *testing.T) { }, ExpectedGeoip: []*router.GeoIP{ { - CountryCode: "test", + // test Cidr: []*router.CIDR{ { Ip: []byte{8, 8, 8, 8}, @@ -574,7 +574,7 @@ func TestIPMatch(t *testing.T) { }, }, { - CountryCode: "test", + // test Cidr: []*router.CIDR{ { Ip: []byte{8, 8, 8, 4}, @@ -669,7 +669,7 @@ func TestLocalDomain(t *testing.T) { }, ExpectedGeoip: []*router.GeoIP{ { // Will match localhost, localhost-a and localhost-b, - CountryCode: "local", + // local Cidr: []*router.CIDR{ {Ip: []byte{127, 0, 0, 2}, Prefix: 32}, {Ip: []byte{127, 0, 0, 3}, Prefix: 32}, diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go index bad9277c..e57b74bf 100644 --- a/app/dns/nameserver.go +++ b/app/dns/nameserver.go @@ -297,3 +297,18 @@ func ResolveIpOptionOverride(queryStrategy QueryStrategy, ipOption dns.IPOption) return ipOption } } + +func ToDomainMatchingType(t router.Domain_Type) DomainMatchingType { + switch t { + case router.Domain_Domain: + return DomainMatchingType_Subdomain + case router.Domain_Full: + return DomainMatchingType_Full + case router.Domain_Plain: + return DomainMatchingType_Keyword + case router.Domain_Regex: + return DomainMatchingType_Regex + default: + panic("unknown domain type") + } +} diff --git a/app/router/config.go b/app/router/config.go index 26833f46..0b2ebd94 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -79,7 +79,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { geoip := rr.Geoip if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { var err error - geoip, err = getGeoIPList(rr.Geoip) + geoip, err = GetGeoIPList(rr.Geoip) if err != nil { return nil, errors.New("failed to build geoip from mmap").Base(err) } @@ -188,7 +188,7 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch } } -func getGeoIPList(ips []*GeoIP) ([]*GeoIP, error) { +func GetGeoIPList(ips []*GeoIP) ([]*GeoIP, error) { geoipList := []*GeoIP{} for _, ip := range ips { if ip.CountryCode != "" { diff --git a/infra/conf/dns.go b/infra/conf/dns.go index a65f0ee8..6ec307c6 100644 --- a/infra/conf/dns.go +++ b/infra/conf/dns.go @@ -80,21 +80,6 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error { return errors.New("failed to parse name server: ", string(data)) } -func toDomainMatchingType(t router.Domain_Type) dns.DomainMatchingType { - switch t { - case router.Domain_Domain: - return dns.DomainMatchingType_Subdomain - case router.Domain_Full: - return dns.DomainMatchingType_Full - case router.Domain_Plain: - return dns.DomainMatchingType_Keyword - case router.Domain_Regex: - return dns.DomainMatchingType_Regex - default: - panic("unknown domain type") - } -} - func (c *NameServerConfig) Build() (*dns.NameServer, error) { if c.Address == nil { return nil, errors.New("NameServer address is not specified.") @@ -111,7 +96,7 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) { for _, pd := range parsedDomain { domains = append(domains, &dns.NameServer_PriorityDomain{ - Type: toDomainMatchingType(pd.Type), + Type: dns.ToDomainMatchingType(pd.Type), Domain: pd.Value, }) } From ced3e75bf348557eccfcf68db5885c310cc99639 Mon Sep 17 00:00:00 2001 From: Alireza Ahmand Date: Tue, 6 Jan 2026 15:54:32 +0330 Subject: [PATCH 080/136] README.md: Add TX-UI to Web Panels (#4981) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 017d868c..af45948a 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ - [X-Panel](https://github.com/xeefei/X-Panel) - [Marzban](https://github.com/Gozargah/Marzban) - [Hiddify](https://github.com/hiddify/Hiddify-Manager) + - [TX-UI](https://github.com/AghayeCoder/tx-ui) - One Click - [Xray-REALITY](https://github.com/zxcvos/Xray-script), [xray-reality](https://github.com/sajjaddg/xray-reality), [reality-ezpz](https://github.com/aleskxyz/reality-ezpz) - [Xray_bash_onekey](https://github.com/hello-yunshu/Xray_bash_onekey), [XTool](https://github.com/LordPenguin666/XTool), [VPainLess](https://github.com/vpainless/vpainless) From d9025857fee8636dc9aed68cf3e8fce83ea7e7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Tue, 6 Jan 2026 20:43:08 +0800 Subject: [PATCH 081/136] transport/pipe/impl.go: Remove runtime.Gosched() in WriteMultiBuffer() (#5467) --- transport/pipe/impl.go | 45 +++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/transport/pipe/impl.go b/transport/pipe/impl.go index e5d67827..81172906 100644 --- a/transport/pipe/impl.go +++ b/transport/pipe/impl.go @@ -3,7 +3,6 @@ package pipe import ( "errors" "io" - "runtime" "sync" "time" @@ -136,11 +135,10 @@ func (p *pipe) writeMultiBufferInternal(mb buf.MultiBuffer) error { if p.data == nil { p.data = mb - return nil + } else { + p.data, _ = buf.MergeMulti(p.data, mb) } - - p.data, _ = buf.MergeMulti(p.data, mb) - return errSlowDown + return nil } func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error { @@ -155,30 +153,23 @@ func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error { return nil } - if err == errSlowDown { - p.readSignal.Signal() - - // Yield current goroutine. Hopefully the reading counterpart can pick up the payload. - runtime.Gosched() - return nil + if err == errBufferFull { + if p.option.discardOverflow { + buf.ReleaseMulti(mb) + return nil + } + select { + case <-p.writeSignal.Wait(): + continue + case <-p.done.Wait(): + buf.ReleaseMulti(mb) + return io.ErrClosedPipe + } } - if err == errBufferFull && p.option.discardOverflow { - buf.ReleaseMulti(mb) - return nil - } - - if err != errBufferFull { - buf.ReleaseMulti(mb) - p.readSignal.Signal() - return err - } - - select { - case <-p.writeSignal.Wait(): - case <-p.done.Wait(): - return io.ErrClosedPipe - } + buf.ReleaseMulti(mb) + p.readSignal.Signal() + return err } } From 446df149bd7ff830d05ee2d09a24c771f4f1f1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Tue, 6 Jan 2026 21:57:11 +0800 Subject: [PATCH 082/136] Routing config: Replace `processName` with `process` (full-name/abs-path/abs-folder) (#5496) About `self/` & `xray/`: https://github.com/XTLS/Xray-core/pull/5496#issuecomment-3714620380 Replaces https://github.com/XTLS/Xray-core/pull/5489 --- app/router/condition.go | 64 ++++++++++++-- app/router/config.go | 8 +- app/router/config.pb.go | 132 ++++++++++++++--------------- app/router/config.proto | 2 +- common/net/find_process_linux.go | 38 ++++----- common/net/find_process_others.go | 4 +- common/net/find_process_windows.go | 34 ++++---- common/net/net.go | 8 +- infra/conf/router.go | 38 ++++----- 9 files changed, 187 insertions(+), 141 deletions(-) diff --git a/app/router/condition.go b/app/router/condition.go index 4e7b406b..245ef6c2 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -3,7 +3,9 @@ package router import ( "context" "os" + "path/filepath" "regexp" + "slices" "strings" "github.com/xtls/xray-core/common/errors" @@ -350,7 +352,51 @@ func ParseAttrs(attrs []string) *GeoAttributeList { } type ProcessNameMatcher struct { - names []string + ProcessNames []string + AbsPaths []string + Folders []string + MatchXraySelf bool +} + +func NewProcessNameMatcher(names []string) *ProcessNameMatcher { + processNames := []string{} + folders := []string{} + absPaths := []string{} + matchXraySelf := false + for _, name := range names { + if name == "self/" { + matchXraySelf = true + continue + } + // replace xray/ with self executable path + if name == "xray/" { + xrayPath, err := os.Executable() + if err != nil { + errors.LogError(context.Background(), "Failed to get xray executable path: ", err) + continue + } + name = xrayPath + } + name := filepath.ToSlash(name) + // /usr/bin/ + if strings.HasSuffix(name, "/") { + folders = append(folders, name) + continue + } + // /usr/bin/curl + if strings.Contains(name, "/") { + absPaths = append(absPaths, name) + continue + } + // curl.exe or curl + processNames = append(processNames, strings.TrimSuffix(name, ".exe")) + } + return &ProcessNameMatcher{ + ProcessNames: processNames, + AbsPaths: absPaths, + Folders: folders, + MatchXraySelf: matchXraySelf, + } } func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool { @@ -369,18 +415,26 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool { if err != nil { return false } - pid, name, err := net.FindProcess(src) + pid, name, absPath, err := net.FindProcess(src) if err != nil { if err != net.ErrNotLocal { errors.LogError(context.Background(), "Unables to find local process name: ", err) } return false } - for _, n := range m.names { - if name == "/self" && pid == os.Getpid() { + if m.MatchXraySelf { + if pid == os.Getpid() { return true } - if n == name { + } + if slices.Contains(m.ProcessNames, name) { + return true + } + if slices.Contains(m.AbsPaths, absPath) { + return true + } + for _, f := range m.Folders { + if strings.HasPrefix(absPath, f) { return true } } diff --git a/app/router/config.go b/app/router/config.go index 0b2ebd94..1566b560 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -126,12 +126,8 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { conds.Add(matcher) } - if len(rr.ProcessName) > 0 { - refinedNames := make([]string, 0, len(rr.ProcessName)) - for _, name := range rr.ProcessName { - refinedNames = append(refinedNames, strings.TrimSuffix(name, ".exe")) - } - conds.Add(&ProcessNameMatcher{refinedNames}) + if len(rr.Process) > 0 { + conds.Add(NewProcessNameMatcher(rr.Process)) } if conds.Len() == 0 { diff --git a/app/router/config.pb.go b/app/router/config.pb.go index c5803f56..ff5838a8 100644 --- a/app/router/config.pb.go +++ b/app/router/config.pb.go @@ -490,7 +490,7 @@ type RoutingRule struct { LocalGeoip []*GeoIP `protobuf:"bytes,17,rep,name=local_geoip,json=localGeoip,proto3" json:"local_geoip,omitempty"` LocalPortList *net.PortList `protobuf:"bytes,18,opt,name=local_port_list,json=localPortList,proto3" json:"local_port_list,omitempty"` VlessRouteList *net.PortList `protobuf:"bytes,20,opt,name=vless_route_list,json=vlessRouteList,proto3" json:"vless_route_list,omitempty"` - ProcessName []string `protobuf:"bytes,21,rep,name=process_name,json=processName,proto3" json:"process_name,omitempty"` + Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"` } func (x *RoutingRule) Reset() { @@ -642,9 +642,9 @@ func (x *RoutingRule) GetVlessRouteList() *net.PortList { return nil } -func (x *RoutingRule) GetProcessName() []string { +func (x *RoutingRule) GetProcess() []string { if x != nil { - return x.ProcessName + return x.Process } return nil } @@ -1089,7 +1089,7 @@ var file_app_router_config_proto_rawDesc = []byte{ 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x53, 0x69, - 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x8b, 0x07, 0x0a, 0x0b, 0x52, 0x6f, + 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x82, 0x07, 0x0a, 0x0b, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x25, 0x0a, 0x0d, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x0c, @@ -1139,68 +1139,68 @@ var file_app_router_config_proto_rawDesc = []byte{ 0x65, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x0e, 0x76, 0x6c, 0x65, 0x73, 0x73, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x63, 0x65, - 0x73, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x15, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x70, - 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x1a, 0x3d, 0x0a, 0x0f, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x74, 0x61, 0x72, - 0x67, 0x65, 0x74, 0x5f, 0x74, 0x61, 0x67, 0x22, 0xdc, 0x01, 0x0a, 0x0d, 0x42, 0x61, 0x6c, 0x61, - 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x2b, 0x0a, 0x11, 0x6f, - 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x74, 0x72, 0x61, - 0x74, 0x65, 0x67, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x74, 0x72, 0x61, - 0x74, 0x65, 0x67, 0x79, 0x12, 0x4d, 0x0a, 0x11, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, - 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x52, 0x10, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, - 0x6e, 0x67, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, - 0x74, 0x61, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x61, 0x6c, 0x6c, 0x62, - 0x61, 0x63, 0x6b, 0x54, 0x61, 0x67, 0x22, 0x54, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, - 0x67, 0x79, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x65, - 0x78, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x72, 0x65, 0x67, 0x65, 0x78, 0x70, - 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xc0, 0x01, 0x0a, - 0x17, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x4c, 0x65, 0x61, 0x73, 0x74, 0x4c, 0x6f, - 0x61, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x35, 0x0a, 0x05, 0x63, 0x6f, 0x73, 0x74, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, - 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, - 0x67, 0x79, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x52, 0x05, 0x63, 0x6f, 0x73, 0x74, 0x73, 0x12, - 0x1c, 0x0a, 0x09, 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x03, 0x52, 0x09, 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x1a, 0x0a, - 0x08, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x08, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61, 0x78, - 0x52, 0x54, 0x54, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, - 0x54, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x6c, 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x02, 0x52, 0x09, 0x74, 0x6f, 0x6c, 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x22, - 0x90, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4f, 0x0a, 0x0f, 0x64, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0e, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x30, 0x0a, 0x04, 0x72, - 0x75, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61, 0x79, - 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x52, 0x6f, 0x75, 0x74, - 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x12, 0x45, 0x0a, - 0x0e, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, - 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, - 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, - 0x52, 0x75, 0x6c, 0x65, 0x22, 0x3c, 0x0a, 0x0e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, - 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x73, 0x49, 0x73, 0x10, 0x00, - 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x70, 0x49, 0x66, 0x4e, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, - 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x70, 0x4f, 0x6e, 0x44, 0x65, 0x6d, 0x61, 0x6e, 0x64, - 0x10, 0x03, 0x42, 0x4f, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, - 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x50, 0x01, 0x5a, 0x24, 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, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x72, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x75, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x63, 0x65, + 0x73, 0x73, 0x18, 0x15, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, + 0x73, 0x1a, 0x3d, 0x0a, 0x0f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x42, 0x0c, 0x0a, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x74, 0x61, 0x67, 0x22, 0xdc, + 0x01, 0x0a, 0x0d, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, + 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, + 0x61, 0x67, 0x12, 0x2b, 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x73, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x6f, + 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, + 0x1a, 0x0a, 0x08, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x4d, 0x0a, 0x11, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x10, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, + 0x67, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x61, + 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x54, 0x61, 0x67, 0x22, 0x54, 0x0a, + 0x0e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, + 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x65, 0x78, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x06, 0x72, 0x65, 0x67, 0x65, 0x78, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x61, 0x74, 0x63, 0x68, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x22, 0xc0, 0x01, 0x0a, 0x17, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, + 0x4c, 0x65, 0x61, 0x73, 0x74, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x35, 0x0a, 0x05, 0x63, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, + 0x2e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x52, + 0x05, 0x63, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, + 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, 0x52, 0x09, 0x62, 0x61, 0x73, 0x65, 0x6c, + 0x69, 0x6e, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, 0x54, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, 0x54, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x6c, 0x65, + 0x72, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x02, 0x52, 0x09, 0x74, 0x6f, 0x6c, + 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x22, 0x90, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x4f, 0x0a, 0x0f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, + 0x74, 0x65, 0x67, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x78, 0x72, 0x61, + 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, + 0x67, 0x79, 0x52, 0x0e, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, + 0x67, 0x79, 0x12, 0x30, 0x0a, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x72, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x04, + 0x72, 0x75, 0x6c, 0x65, 0x12, 0x45, 0x0a, 0x0e, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, + 0x67, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x42, + 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x62, 0x61, + 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x22, 0x3c, 0x0a, 0x0e, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x08, 0x0a, + 0x04, 0x41, 0x73, 0x49, 0x73, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x70, 0x49, 0x66, 0x4e, + 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x70, 0x4f, + 0x6e, 0x44, 0x65, 0x6d, 0x61, 0x6e, 0x64, 0x10, 0x03, 0x42, 0x4f, 0x0a, 0x13, 0x63, 0x6f, 0x6d, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, + 0x50, 0x01, 0x5a, 0x24, 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, 0x61, 0x70, + 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, + 0x41, 0x70, 0x70, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/app/router/config.proto b/app/router/config.proto index 222f395d..20da23ba 100644 --- a/app/router/config.proto +++ b/app/router/config.proto @@ -113,7 +113,7 @@ message RoutingRule { xray.common.net.PortList local_port_list = 18; xray.common.net.PortList vless_route_list = 20; - repeated string process_name = 21; + repeated string process = 21; } message BalancingRule { diff --git a/common/net/find_process_linux.go b/common/net/find_process_linux.go index 80080b70..95e03935 100644 --- a/common/net/find_process_linux.go +++ b/common/net/find_process_linux.go @@ -13,13 +13,13 @@ import ( "github.com/xtls/xray-core/common/errors" ) -func FindProcess(dest Destination) (int, string, error) { +func FindProcess(dest Destination) (PID int, Name string, AbsolutePath string, err error) { isLocal, err := IsLocal(dest.Address.IP()) if err != nil { - return 0, "", errors.New("failed to determine if address is local: ", err) + return 0, "", "", errors.New("failed to determine if address is local: ", err) } if !isLocal { - return 0, "", ErrNotLocal + return 0, "", "", ErrNotLocal } if dest.Network != Network_TCP && dest.Network != Network_UDP { panic("Unsupported network type for process lookup.") @@ -51,36 +51,39 @@ func FindProcess(dest Destination) (int, string, error) { targetHexAddr, err := formatLittleEndianString(dest.Address, dest.Port) if err != nil { - return 0, "", errors.New("failed to format address: ", err) + return 0, "", "", errors.New("failed to format address: ", err) } inode, err := findInodeInFile(procFile, targetHexAddr) if err != nil { - return 0, "", errors.New("could not search in ", procFile).Base(err) + return 0, "", "", errors.New("could not search in ", procFile).Base(err) } if inode == "" { - return 0, "", errors.New("connection for ", dest.Address, ":", dest.Port, " not found in ", procFile) + return 0, "", "", errors.New("connection for ", dest.Address, ":", dest.Port, " not found in ", procFile) } pidStr, err := findPidByInode(inode) if err != nil { - return 0, "", errors.New("could not find PID for inode ", inode, ": ", err) + return 0, "", "", errors.New("could not find PID for inode ", inode, ": ", err) } if pidStr == "" { - return 0, "", errors.New("no process found for inode ", inode) + return 0, "", "", errors.New("no process found for inode ", inode) } - procName, err := getProcessName(pidStr) + absPath, err := getAbsPath(pidStr) if err != nil { - return 0, "", fmt.Errorf("could not get process name for PID %s: %w", pidStr, err) + return 0, "", "", errors.New("could not get process name for PID ", pidStr, ":", err) } + nameSplit := strings.Split(absPath, "/") + procName := nameSplit[len(nameSplit)-1] + pid, err := strconv.Atoi(pidStr) if err != nil { - return 0, "", errors.New("failed to parse PID: ", err) + return 0, "", "", errors.New("failed to parse PID: ", err) } - return pid, procName, nil + return pid, procName, absPath, nil } func formatLittleEndianString(addr Address, port Port) (string, error) { @@ -167,12 +170,7 @@ func findPidByInode(inode string) (string, error) { return "", nil } -func getProcessName(pid string) (string, error) { - path := fmt.Sprintf("/proc/%s/comm", pid) - content, err := os.ReadFile(path) - if err != nil { - return "", err - } - // remove trailing \n - return strings.TrimSpace(string(content)), nil +func getAbsPath(pid string) (string, error) { + path := fmt.Sprintf("/proc/%s/exe", pid) + return os.Readlink(path) } diff --git a/common/net/find_process_others.go b/common/net/find_process_others.go index 472d4f85..a47b5b93 100644 --- a/common/net/find_process_others.go +++ b/common/net/find_process_others.go @@ -6,6 +6,6 @@ import ( "github.com/xtls/xray-core/common/errors" ) -func FindProcess(dest Destination) (int, string, error) { - return 0, "", errors.New("process lookup is not supported on this platform") +func FindProcess(dest Destination) (int, string, string, error) { + return 0, "", "", errors.New("process lookup is not supported on this platform") } diff --git a/common/net/find_process_windows.go b/common/net/find_process_windows.go index fe223254..46ddcc9c 100644 --- a/common/net/find_process_windows.go +++ b/common/net/find_process_windows.go @@ -4,6 +4,7 @@ package net import ( "net/netip" + "path/filepath" "strings" "sync" "syscall" @@ -48,19 +49,19 @@ func initWin32API() error { return nil } -func FindProcess(dest Destination) (int, string, error) { +func FindProcess(dest Destination) (PID int, Name string, AbsolutePath string, err error) { once.Do(func() { initErr = initWin32API() }) if initErr != nil { - return 0, "", initErr + return 0, "", "", initErr } isLocal, err := IsLocal(dest.Address.IP()) if err != nil { - return 0, "", errors.New("failed to determine if address is local: ", err) + return 0, "", "", errors.New("failed to determine if address is local: ", err) } if !isLocal { - return 0, "", ErrNotLocal + return 0, "", "", ErrNotLocal } if dest.Network != Network_TCP && dest.Network != Network_UDP { panic("Unsupported network type for process lookup.") @@ -86,7 +87,7 @@ func FindProcess(dest Destination) (int, string, error) { addr, ok := netip.AddrFromSlice(ip) if !ok { - return 0, "", errors.New("invalid IP address") + return 0, "", "", errors.New("invalid IP address") } addr = addr.Unmap() @@ -97,19 +98,23 @@ func FindProcess(dest Destination) (int, string, error) { buf, err := getTransportTable(fn, family, class) if err != nil { - return 0, "", err + return 0, "", "", err } s := newSearcher(dest.Network, dest.Address.Family()) pid, err := s.Search(buf, addr, uint16(port)) if err != nil { - return 0, "", err + return 0, "", "", err } - name, err := getExecPathFromPID(pid) - // drop .exe - name = strings.TrimSuffix(name, ".exe") - return int(pid), name, err + NameWithPath, err := getExecPathFromPID(pid) + NameWithPath = filepath.ToSlash(NameWithPath) + + // drop .exe and path + nameSplit := strings.Split(NameWithPath, "/") + procName := nameSplit[len(nameSplit)-1] + procName = strings.TrimSuffix(procName, ".exe") + return int(pid), procName, NameWithPath, err } type searcher struct { @@ -234,10 +239,5 @@ func getExecPathFromPID(pid uint32) (string, error) { if err != nil { return "", err } - // full path will like: C:\Windows\System32\curl.exe - // we only need the executable name - fullPathName := syscall.UTF16ToString(buf[:size]) - nameSplit := strings.Split(fullPathName, "\\") - name := nameSplit[len(nameSplit)-1] - return name, nil + return syscall.UTF16ToString(buf[:size]), nil } diff --git a/common/net/net.go b/common/net/net.go index 115ede7d..9a21313a 100644 --- a/common/net/net.go +++ b/common/net/net.go @@ -21,22 +21,22 @@ const ChromeH2KeepAlivePeriod = 45 * time.Second var ErrNotLocal = errors.New("the source address is not from local machine.") -type localIPCahceEntry struct { +type localIPCacheEntry struct { addrs []net.Addr lastUpdate time.Time } -var localIPCahce = atomic.Pointer[localIPCahceEntry]{} +var localIPCache = atomic.Pointer[localIPCacheEntry]{} func IsLocal(ip net.IP) (bool, error) { var addrs []net.Addr - if entry := localIPCahce.Load(); entry == nil || time.Since(entry.lastUpdate) > time.Minute { + if entry := localIPCache.Load(); entry == nil || time.Since(entry.lastUpdate) > time.Minute { var err error addrs, err = net.InterfaceAddrs() if err != nil { return false, err } - localIPCahce.Store(&localIPCahceEntry{ + localIPCache.Store(&localIPCacheEntry{ addrs: addrs, lastUpdate: time.Now(), }) diff --git a/infra/conf/router.go b/infra/conf/router.go index 8aa15065..54320b3b 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -449,22 +449,22 @@ func ToCidrList(ips StringList) ([]*router.GeoIP, error) { func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) { type RawFieldRule struct { RouterRule - Domain *StringList `json:"domain"` - Domains *StringList `json:"domains"` - IP *StringList `json:"ip"` - Port *PortList `json:"port"` - Network *NetworkList `json:"network"` - SourceIP *StringList `json:"sourceIP"` - Source *StringList `json:"source"` - SourcePort *PortList `json:"sourcePort"` - User *StringList `json:"user"` - VlessRoute *PortList `json:"vlessRoute"` - InboundTag *StringList `json:"inboundTag"` - Protocols *StringList `json:"protocol"` - Attributes map[string]string `json:"attrs"` - LocalIP *StringList `json:"localIP"` - LocalPort *PortList `json:"localPort"` - ProcessName *StringList `json:"processName"` + Domain *StringList `json:"domain"` + Domains *StringList `json:"domains"` + IP *StringList `json:"ip"` + Port *PortList `json:"port"` + Network *NetworkList `json:"network"` + SourceIP *StringList `json:"sourceIP"` + Source *StringList `json:"source"` + SourcePort *PortList `json:"sourcePort"` + User *StringList `json:"user"` + VlessRoute *PortList `json:"vlessRoute"` + InboundTag *StringList `json:"inboundTag"` + Protocols *StringList `json:"protocol"` + Attributes map[string]string `json:"attrs"` + LocalIP *StringList `json:"localIP"` + LocalPort *PortList `json:"localPort"` + Process *StringList `json:"process"` } rawFieldRule := new(RawFieldRule) err := json.Unmarshal(msg, rawFieldRule) @@ -577,10 +577,8 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) { rule.Attributes = rawFieldRule.Attributes } - if rawFieldRule.ProcessName != nil { - for _, s := range *rawFieldRule.ProcessName { - rule.ProcessName = append(rule.ProcessName, s) - } + if rawFieldRule.Process != nil && len(*rawFieldRule.Process) > 0 { + rule.Process = *rawFieldRule.Process } return rule, nil From 394e117998dd3d560b9429d1f938fb8a658b5dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=90=B2=93=F0=90=B3=9B=F0=90=B3=AA=F0=90=B3=82?= =?UTF-8?q?=F0=90=B3=90=20=F0=90=B2=80=F0=90=B3=A2=F0=90=B3=A6=F0=90=B3=AB?= =?UTF-8?q?=F0=90=B3=A2=20=F0=90=B2=A5=F0=90=B3=94=F0=90=B3=9B=F0=90=B3=AA?= =?UTF-8?q?=F0=90=B3=8C=F0=90=B3=91=F0=90=B3=96=F0=90=B3=87?= <26771058+KobeArthurScofield@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:59:23 +0800 Subject: [PATCH 083/136] GitHub Actions: Add wintun.dll into Windows zips; Workflow refinement (#5501) For https://github.com/XTLS/Xray-core/pull/5464 --- .github/workflows/release-win7.yml | 60 +++++++++++++---- .github/workflows/release.yml | 67 +++++++++++++++---- .github/workflows/scheduled-assets-update.yml | 66 +++++++++++++++++- 3 files changed, 164 insertions(+), 29 deletions(-) diff --git a/.github/workflows/release-win7.yml b/.github/workflows/release-win7.yml index 9f1a33d8..914beca3 100644 --- a/.github/workflows/release-win7.yml +++ b/.github/workflows/release-win7.yml @@ -18,6 +18,12 @@ jobs: path: resources key: xray-geodat- + - name: Restore Wintun Cache + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-wintun- + - name: Check Assets Existence id: check-assets run: | @@ -34,6 +40,18 @@ jobs: break fi done + LIST=('amd64' 'x86') + for ARCHITECTURE in "${LIST[@]}" + do + echo -e "Checking wintun.dll for ${ARCHITECTURE}..." + if [ -s "./resources/wintun/bin/${ARCHITECTURE}/wintun.dll" ]; then + echo -e "wintun.dll for ${ARCHITECTURE} exists." + else + echo -e "wintun.dll for ${ARCHITECTURE} is missing." + echo "missing=true" >> $GITHUB_OUTPUT + break + fi + done - name: Sleep for 90 seconds if Assets Missing if: steps.check-assets.outputs.missing == 'true' @@ -95,8 +113,6 @@ jobs: COMMID=$(git describe --always --dirty) echo 'Building Xray for Windows 7...' go build -o build_assets/xray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main - echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs - echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1 # The line below is for without running conhost.exe version. Commented for not being used. Provided for reference. # go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main @@ -106,9 +122,29 @@ jobs: path: resources key: xray-geodat- + - name: Restore Wintun Cache + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-wintun- + + - name: Add additional assets into package + run: | + mv -f resources/geo* build_assets/ + if [[ ${GOOS} == 'windows' ]]; then + echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs + echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1 + if [[ ${GOARCH} == 'amd64' ]]; then + mv resources/wintun/bin/amd64/wintun.dll build_assets/ + fi + if [[ ${GOARCH} == '386' ]]; then + mv resources/wintun/bin/x86/wintun.dll build_assets/ + fi + mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt + fi + - name: Copy README.md & LICENSE run: | - mv -f resources/* build_assets cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE @@ -127,17 +163,6 @@ jobs: openssl dgst -$METHOD $FILE | sed 's/([^)]*)//g' >>$DGST done - - name: Change the name - run: | - mv build_assets Xray-${{ env.ASSET_NAME }} - - - name: Upload files to Artifacts - uses: actions/upload-artifact@v6 - with: - name: Xray-${{ env.ASSET_NAME }} - path: | - ./Xray-${{ env.ASSET_NAME }}/* - - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 if: github.event_name == 'release' @@ -146,3 +171,10 @@ jobs: file: ./Xray-${{ env.ASSET_NAME }}.zip* tag: ${{ github.ref }} file_glob: true + + - name: Upload files to Artifacts + uses: actions/upload-artifact@v6 + with: + name: Xray-${{ env.ASSET_NAME }} + path: | + ./build_assets/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8fe5cae7..a4fe5a60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,12 @@ jobs: path: resources key: xray-geodat- + - name: Restore Wintun Cache + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-wintun- + - name: Check Assets Existence id: check-assets run: | @@ -34,6 +40,18 @@ jobs: break fi done + LIST=('amd64' 'x86' 'arm64' 'arm') + for ARCHITECTURE in "${LIST[@]}" + do + echo -e "Checking wintun.dll for ${ARCHITECTURE}..." + if [ -s "./resources/wintun/bin/${ARCHITECTURE}/wintun.dll" ]; then + echo -e "wintun.dll for ${ARCHITECTURE} exists." + else + echo -e "wintun.dll for ${ARCHITECTURE} is missing." + echo "missing=true" >> $GITHUB_OUTPUT + break + fi + done - name: Trigger Asset Update Workflow if Assets Missing if: steps.check-assets.outputs.missing == 'true' @@ -191,8 +209,6 @@ jobs: if [[ ${GOOS} == 'windows' ]]; then echo 'Building Xray for Windows...' go build -o build_assets/xray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main - echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs - echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1 # The line below is for without running conhost.exe version. Commented for not being used. Provided for reference. # go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main else @@ -212,9 +228,36 @@ jobs: path: resources key: xray-geodat- + - name: Restore Wintun Cache + if: matrix.goos == 'windows' + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-wintun- + + - name: Add additional assets into package + run: | + mv -f resources/geo* build_assets/ + if [[ ${GOOS} == 'windows' ]]; then + echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs + echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1 + if [[ ${GOARCH} == 'amd64' ]]; then + mv resources/wintun/bin/amd64/wintun.dll build_assets/ + fi + if [[ ${GOARCH} == '386' ]]; then + mv resources/wintun/bin/x86/wintun.dll build_assets/ + fi + if [[ ${GOARCH} == 'arm64' ]]; then + mv resources/wintun/bin/arm64/wintun.dll build_assets/ + fi + if [[ ${GOARCH} == 'arm' ]]; then + mv resources/wintun/bin/arm/wintun.dll build_assets/ + fi + mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt + fi + - name: Copy README.md & LICENSE run: | - mv -f resources/* build_assets cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE @@ -233,17 +276,6 @@ jobs: openssl dgst -$METHOD $FILE | sed 's/([^)]*)//g' >>$DGST done - - name: Change the name - run: | - mv build_assets Xray-${{ env.ASSET_NAME }} - - - name: Upload files to Artifacts - uses: actions/upload-artifact@v6 - with: - name: Xray-${{ env.ASSET_NAME }} - path: | - ./Xray-${{ env.ASSET_NAME }}/* - - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 if: github.event_name == 'release' @@ -252,3 +284,10 @@ jobs: file: ./Xray-${{ env.ASSET_NAME }}.zip* tag: ${{ github.ref }} file_glob: true + + - name: Upload files to Artifacts + uses: actions/upload-artifact@v6 + with: + name: Xray-${{ env.ASSET_NAME }} + path: | + ./build_assets/* diff --git a/.github/workflows/scheduled-assets-update.yml b/.github/workflows/scheduled-assets-update.yml index 24183528..2e5da622 100644 --- a/.github/workflows/scheduled-assets-update.yml +++ b/.github/workflows/scheduled-assets-update.yml @@ -4,6 +4,7 @@ name: Scheduled assets update # routine manner, for example: GeoIP/GeoSite. # Currently updating: # - Geodat (GeoIP/Geosite) +# - Wintun (wintun.dll) on: workflow_dispatch: @@ -21,7 +22,7 @@ on: jobs: geodat: - if: github.event.schedule == '30 22 * * *' || github.event_name == 'push'|| github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + if: github.event.schedule == '30 22 * * *' || github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - name: Restore Geodat Cache @@ -63,3 +64,66 @@ jobs: with: path: resources key: xray-geodat-${{ github.sha }}-${{ github.run_number }} + + wintun: + if: github.event.schedule == '30 22 * * *' || github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - name: Restore Wintun Cache + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-wintun- + + - name: Force downloading if run manually or on file update + if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' + run: | + echo "FORCE_UPDATE=true" >> $GITHUB_ENV + + - name: Update Wintun + id: update + uses: nick-fields/retry@v3 + with: + timeout_minutes: 60 + retry_wait_seconds: 60 + max_attempts: 60 + command: | + [ -d 'resources' ] || mkdir resources + LIST=('amd64' 'x86' 'arm64' 'arm') + for ARCHITECTURE in "${LIST[@]}" + do + FILE_PATH="resources/wintun/bin/${ARCHITECTURE}/wintun.dll" + echo -e "Checking if wintun.dll for ${ARCHITECTURE} exists..." + if [ -s "./resources/wintun/bin/${ARCHITECTURE}/wintun.dll" ]; then + echo -e "wintun.dll for ${ARCHITECTURE} exists" + continue + else + echo -e "wintun.dll for ${ARCHITECTURE} is missing" + missing=true + fi + done + if [ -s "./resources/wintun/LICENSE.txt" ]; then + echo -e "LICENSE for Wintun exists" + else + echo -e "LICENSE for Wintun is missing" + missing=true + fi + if [[ -v FORCE_UPDATE ]]; then + missing=true + fi + if [[ "$missing" == true ]]; then + FILENAME=wintun.zip + DOWNLOAD_FILE=wintun-0.14.1.zip + echo -e "Downloading https://www.wintun.net/builds/${DOWNLOAD_FILE}..." + curl -L "https://www.wintun.net/builds/${DOWNLOAD_FILE}" -o "${FILENAME}" + echo -e "Unpacking wintun..." + unzip -u ${FILENAME} -d resources/ + echo "unhit=true" >> $GITHUB_OUTPUT + fi + + - name: Save Wintun Cache + uses: actions/cache/save@v5 + if: ${{ steps.update.outputs.unhit }} + with: + path: resources + key: xray-wintun-${{ github.sha }}-${{ github.run_number }} From 39ba1f7952197ca99e8e99fb86cfa892fe2bc527 Mon Sep 17 00:00:00 2001 From: Owersun <4807375+Owersun@users.noreply.github.com> Date: Wed, 7 Jan 2026 23:05:08 +0100 Subject: [PATCH 084/136] Proxy: Add TUN inbound for Windows & Linux, including Android (#5464) * Proxy: Implement tun raw network interface inbound support for Linux * Proxy: Tun. Include "android" as build condition for build of tun_default implementation * Proxy: Tun. Add .Close() cleanup calls to Handler.Init() where needed * Proxy: Add Tun for Android * Proxy: Tun. Implement Windows support --------- Co-authored-by: yuhan6665 <1588741+yuhan6665@users.noreply.github.com> --- common/platform/platform.go | 2 + infra/conf/tun.go | 30 +++++ infra/conf/xray.go | 1 + proxy/tun/README.md | 174 +++++++++++++++++++++++++ proxy/tun/config.go | 1 + proxy/tun/config.pb.go | 149 +++++++++++++++++++++ proxy/tun/config.proto | 13 ++ proxy/tun/handler.go | 142 ++++++++++++++++++++ proxy/tun/stack.go | 17 +++ proxy/tun/stack_gvisor.go | 206 ++++++++++++++++++++++++++++++ proxy/tun/tun.go | 13 ++ proxy/tun/tun_android.go | 58 +++++++++ proxy/tun/tun_default.go | 34 +++++ proxy/tun/tun_linux.go | 120 +++++++++++++++++ proxy/tun/tun_windows.go | 84 ++++++++++++ proxy/tun/tun_windows_endpoint.go | 180 ++++++++++++++++++++++++++ 16 files changed, 1224 insertions(+) create mode 100644 infra/conf/tun.go create mode 100644 proxy/tun/README.md create mode 100644 proxy/tun/config.go create mode 100644 proxy/tun/config.pb.go create mode 100644 proxy/tun/config.proto create mode 100644 proxy/tun/handler.go create mode 100644 proxy/tun/stack.go create mode 100644 proxy/tun/stack_gvisor.go create mode 100644 proxy/tun/tun.go create mode 100644 proxy/tun/tun_android.go create mode 100644 proxy/tun/tun_default.go create mode 100644 proxy/tun/tun_linux.go create mode 100644 proxy/tun/tun_windows.go create mode 100644 proxy/tun/tun_windows_endpoint.go diff --git a/common/platform/platform.go b/common/platform/platform.go index bc7baf65..80e62874 100644 --- a/common/platform/platform.go +++ b/common/platform/platform.go @@ -22,6 +22,8 @@ const ( BrowserDialerAddress = "xray.browser.dialer" XUDPLog = "xray.xudp.show" XUDPBaseKey = "xray.xudp.basekey" + + TunFdKey = "xray.tun.fd" ) type EnvFlag struct { diff --git a/infra/conf/tun.go b/infra/conf/tun.go new file mode 100644 index 00000000..2d95e3cd --- /dev/null +++ b/infra/conf/tun.go @@ -0,0 +1,30 @@ +package conf + +import ( + "github.com/xtls/xray-core/proxy/tun" + "google.golang.org/protobuf/proto" +) + +type TunConfig struct { + Name string `json:"name"` + MTU uint32 `json:"MTU"` + UserLevel uint32 `json:"userLevel"` +} + +func (v *TunConfig) Build() (proto.Message, error) { + config := &tun.Config{ + Name: v.Name, + MTU: v.MTU, + UserLevel: v.UserLevel, + } + + if v.Name == "" { + config.Name = "xray0" + } + + if v.MTU == 0 { + config.MTU = 1500 + } + + return config, nil +} diff --git a/infra/conf/xray.go b/infra/conf/xray.go index 0dfb2a98..2080bd8c 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -28,6 +28,7 @@ var ( "vmess": func() interface{} { return new(VMessInboundConfig) }, "trojan": func() interface{} { return new(TrojanServerConfig) }, "wireguard": func() interface{} { return &WireGuardConfig{IsClient: false} }, + "tun": func() interface{} { return new(TunConfig) }, }, "protocol", "settings") outboundConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{ diff --git a/proxy/tun/README.md b/proxy/tun/README.md new file mode 100644 index 00000000..684747b8 --- /dev/null +++ b/proxy/tun/README.md @@ -0,0 +1,174 @@ +# TUN network layer 3 input support + +TUN interface support bridges the gap between network layer 3 and layer 7, introducing raw network input. + +This functionality is targeted to assist applications/end devices that don't have proxy support, or can't run external applications (like Smart TV's). Making it possible to run Xray proxy right on network edge devices (routers) with support to route raw network traffic. \ +Primary targets are Linux based router devices. Like most popular OpenWRT option. \ +Although support for Windows is also implemented (see below). + +## PLEASE READ FOLLOWING CAREFULLY + +If you are not sure what this is and do you need it or not - you don't. \ +This functionality is intended to be configured by network professionals, who understand the deployment case and scenarios. \ +Plainly enabling it in the config probably will result nothing, or lock your router up in infinite network loop. + +## DETAILS + +Current implementation does not contain options to configure network level addresses, routing or rules. +Enabling the feature will result only tun interface up, and that's it. \ +This is explicit decision, significantly simplifying implementation, and allowing any number of custom configurations, consumers could come up with. Network interface is OS level entity, and OS is what should manage it. \ +Working configuration, is tun enabled in Xray config with specific name (e.g. xray0), and OS level configuration to manage "xray0" interface, applying routing and rules on interface up. +This way consistency of system level routing and rules is ensured from single place of responsibility - the OS itself. \ +Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README. + +Due to this inbound not actually being a proxy, the configuration ignore required listen and port options, and never listen on any port. \ +Here is simple Xray config snippet to enable the inbound: +``` +{ + "inbounds": [ + { + "port": 0, + "protocol": "tun", + "settings": { + "name": "xray0", + "MTU": 1492 + } + } + ], +``` + +## SUPPORTED FEATURES + +- IPv4 and IPv6 +- TCP and UDP + +## LIMITATION + +- No ICMP support +- Connections are established to any host, as connection success is only a mark of successful accepting packet for proxying. Hosts that are not accepting connections or don't even exists, will look like they opened a connection (SYN-ACK), and never send back a single byte, closing connection (RST) after some time. This is the side effect of the whole process actually being a proxy, and not real network layer 3 vpn + +## CONSIDERATIONS + +This feature being network level interface that require raw routing, bring some ambiguities that need to be taken in account. \ +Xray-core itself is connecting to its uplinks on a network level, therefore, it's really simple to lock network up in an infinite loop, when trying to pass "everything through Xray". \ +You can't just route 0.0.0.0/0 through xray0 interface, as that will result Xray-core itself try to reach its uplink through xray0 interface, resulting infinite network loop. +There are several ways to address this: + +- Approach 1: \ + Add precise static route to Xray upstreams, having them always routed through static internet gateway. + E.g. when 123.123.123.123 is the Xray VLESS uplink, this network configuration will work just fine: + ``` + ip route add 123.123.123.123/32 via + ip route add 0.0.0.0/0 dev xray0 + ``` + This has disadvantages, - a lot of conditions must be kept static: internet gateway address, xray uplink ip address, and so on. +- Approach 1-b: \ + Route only specific networks through Xray, keeping the default gateway unchanged. + This can be done in many different ways, using ip sets, routing daemons like BGP peers, etc... All you need to do is to route the paths through xray0 dev. + The disadvantage in this case is smaller, - you need to make sure the uplink will not become part of those sets and that's it. Can easily be done with route metric priorities. +- Approach 2: \ + Separate main route table and Xray route table with default gateways pointing to different destinations. + This way you can achieve full protection of hosts behind the router, keeping router configuration as flexible as desired. \ + There are two ways to do that: \ + Either configure xray0 interface to appear and operate as default gateway in a separate route table, e.g. 1001. Then mark and route protected traffic by ip rules to that table. \ + It's a simplest way to make a "non-damaging" configuration, when the only thing you need to do to enable/disable proxying is to flip the ip rules off. Which is also a disadvantage of itself - if by accident ip rules will get disabled, the traffic will spill out of the internet interface unprotected. \ + Or, other way around, move default routing to a separate route table, so that all usual routing information is set in e.g. route table 1000, + and Xray interface operate in the main route table. This will allow proper flexibility, but you need to ensure traffic from the Xray process, running on the router, is marked to get routed through table 1000. This again can be achieved in same ways, using ip rules and iptable rules combinations. \ + Big advantage of that, is that traffic of the route itself is going to be wrapped into the proxy, including DNS queries, without any additional effort. Although, the disadvantage of that is, that in any case proxying stops (uplink dies, Xray hangs, encryption start to flap), it will result complete internet inaccessibility. \ + Any approach is applicable, and if you know what you are doing (which is expected, if you read until here) you do understand which one you want and can manage. \ + +### Important: + +TUN is network level entity, therefore communication through it, is always ip to ip, there are no host names. \ +Therefore, DNS resolution will always happen before traffic even enter the interface (it will be separate ip-to-ip packets/connections to resolve hostnames). \ +You always need to consider that DNS queries in any configuration you chose, most likely, will originate from the router itself (hosts behind the router access router DNS, router DNS fire queries to the outside). +Without proper addressing that, DNS queries will expose actual destinations/websites accessed through the router. \ +To address that you can as ignore (not use) DNS of the router (just delegate some public DNS in DHCP configuration to your devices), or make sure routing rules are configured the way, DNS resolution of the router itself runs through Xray interface/routing table. + +You also need to remember that local traffic of the router (e.g. DNS, firmware updates, etc.), is subject of firewall rules as outcoming/incoming traffic (not forward). +If you have restrictive firewall, you need to allow input/output traffic through xray0 interface, for it to properly dispatch and reach the OS back. + +Additionally, working with two route tables is not taken lightly by Linux, and sometimes make it panic about "martian packets", which it calls the packets arriving through interfaces it does not expect they could arrive from. \ +It was just a warning until recent kernel versions, but now traffic is often dropped. \ +In simple case this can be just disabled with +``` +/usr/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +``` +But proper approach should be defining your route tables fully and consistently, adding all routes corresponding to traffic that flow through them. + +## EXAMPLES + +systemd-networkd \ +configuration file you can place in /etc/systemd/networkd as 90-xray0.network +which will configure xray0 interface and routing using route table 1001, when the interface will appear in the system (Xray starts). And deconfigure when disappears. +``` +[Match] +Name = xray0 + +[Network] +KeepConfiguration = yes + +[Link] +ActivationPolicy = manual +RequiredForOnline = no + +[Route] +Table = 1001 +Destination = 0.0.0.0/0 + +[RoutingPolicyRule] +From = 192.168.0.0/24 +Table = 1001 +``` +RoutingPolicyRule will add the record into ip rules, that will funnel all traffic from 192.168.0.0/24 through the table 1001 \ +Please note that for ideal configuration of the routing you will also need to add the route to 192.168.0.0/24 to the route table 1001. +You can do that e.g. in the file, describing your adapter serving local network (e.g. 10-br0.network), additionally to its native properties like: +``` +[Match] +Name = br0 +Type = bridge + +[Network] +...skip... +Address = 192.168.0.1/24 +...skip... + +[Route] +Table = 1001 +Destination = 192.168.0.0/24 +PreferredSource = 192.168.0.1 +Scope = link +``` +All in all systemd-networkd and its derivatives (like netplan or NetworkManager) provide all means to configure your networking, according to your wish, that will ensure network consistency of xray0 interface coming up and down, relative to other network configuration like internet interfaces, nat rules and so on. + +## WINDOWS SUPPORT + +Windows version of the same functionality is implemented through Wintun library. \ +To make it start, wintun.dll specific for your Windows/arch must be present next to Xray.exe binary. + +After the start network adapter with the name you chose in the config will be created in the system, and exist while Xray is running. + +You can give the adapter ip address manually, you can live Windows to give it autogenerated ip address (which take few seconds), it doesn't matter, the traffic going _through_ the interface will be forwarded into the app for proxying. \ +Minimal configuration that will work for local machine is routing passing the traffic on-link through the interface. +You will need the interface id for that, unfortunately it is going to change with every Xray start due to implementation ambiguity between Xray and wintun driver. +You can find the interface id with the command +``` +route print +``` +it will be in the list of interfaces on the top of the output +``` +=========================================================================== +Interface List + 8...cc cc cc cc cc cc ......Realtek PCIe GbE Family Controller + 47...........................Xray Tunnel + 1...........................Software Loopback Interface 1 +=========================================================================== +``` +In this case the interface id is "47". \ +Then you can add on-link route through the adapter with (example) command +``` +route add 1.1.1.1 mask 255.0.0.0 0.0.0.0 if 47 +``` +Note on ipv6 support. \ +Despite Windows also giving the adapter autoconfigured ipv6 address, the ipv6 is not possible until the interface has any _routable_ ipv6 address (given link-local address will not accept traffic from external addresses). \ +So everything applicable for ipv4 above also works for ipv6, you only need to give the interface some address manually, e.g. anything private like fc00::a:b:c:d/64 will do just fine diff --git a/proxy/tun/config.go b/proxy/tun/config.go new file mode 100644 index 00000000..75e8485f --- /dev/null +++ b/proxy/tun/config.go @@ -0,0 +1 @@ +package tun diff --git a/proxy/tun/config.pb.go b/proxy/tun/config.pb.go new file mode 100644 index 00000000..dc62698b --- /dev/null +++ b/proxy/tun/config.pb.go @@ -0,0 +1,149 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.1 +// protoc v6.30.2 +// source: proxy/tun/config.proto + +package tun + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + MTU uint32 `protobuf:"varint,2,opt,name=MTU,proto3" json:"MTU,omitempty"` + UserLevel uint32 `protobuf:"varint,3,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_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 Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Config) GetMTU() uint32 { + if x != nil { + return x.MTU + } + return 0 +} + +func (x *Config) GetUserLevel() uint32 { + if x != nil { + return x.UserLevel + } + return 0 +} + +var File_config_proto protoreflect.FileDescriptor + +var file_config_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, + 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x74, 0x75, 0x6e, 0x22, 0x4d, + 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, + 0x4d, 0x54, 0x55, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x4d, 0x54, 0x55, 0x12, 0x1d, + 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x42, 0x4c, 0x0a, + 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, + 0x74, 0x75, 0x6e, 0x50, 0x01, 0x5a, 0x23, 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, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x74, 0x75, 0x6e, 0xaa, 0x02, 0x0e, 0x58, 0x72, 0x61, + 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x54, 0x75, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_config_proto_rawDescOnce sync.Once + file_config_proto_rawDescData = file_config_proto_rawDesc +) + +func file_config_proto_rawDescGZIP() []byte { + file_config_proto_rawDescOnce.Do(func() { + file_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_config_proto_rawDescData) + }) + return file_config_proto_rawDescData +} + +var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.proxy.tun.Config +} +var file_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_config_proto_init() } +func file_config_proto_init() { + if File_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_config_proto_goTypes, + DependencyIndexes: file_config_proto_depIdxs, + MessageInfos: file_config_proto_msgTypes, + }.Build() + File_config_proto = out.File + file_config_proto_rawDesc = nil + file_config_proto_goTypes = nil + file_config_proto_depIdxs = nil +} diff --git a/proxy/tun/config.proto b/proxy/tun/config.proto new file mode 100644 index 00000000..24e5e527 --- /dev/null +++ b/proxy/tun/config.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package xray.proxy.tun; +option csharp_namespace = "Xray.Proxy.Tun"; +option go_package = "github.com/xtls/xray-core/proxy/tun"; +option java_package = "com.xray.proxy.tun"; +option java_multiple_files = true; + +message Config { + string name = 1; + uint32 MTU = 2; + uint32 user_level = 3; +} diff --git a/proxy/tun/handler.go b/proxy/tun/handler.go new file mode 100644 index 00000000..760d872e --- /dev/null +++ b/proxy/tun/handler.go @@ -0,0 +1,142 @@ +package tun + +import ( + "context" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + c "github.com/xtls/xray-core/common/ctx" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet/stat" +) + +// Handler is managing object that tie together tun interface, ip stack and dispatch connections to the routing +type Handler struct { + ctx context.Context + config *Config + stack Stack + policyManager policy.Manager + dispatcher routing.Dispatcher +} + +// ConnectionHandler interface with the only method that stack is going to push new connections to +type ConnectionHandler interface { + HandleConnection(conn net.Conn, destination net.Destination) +} + +// Handler implements ConnectionHandler +var _ ConnectionHandler = (*Handler)(nil) + +func (t *Handler) policy() policy.Session { + p := t.policyManager.ForLevel(t.config.UserLevel) + return p +} + +// Init the Handler instance with necessary parameters +func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routing.Dispatcher) error { + var err error + + t.ctx = core.ToBackgroundDetachedContext(ctx) + t.policyManager = pm + t.dispatcher = dispatcher + + tunName := t.config.Name + tunOptions := TunOptions{ + Name: tunName, + MTU: t.config.MTU, + } + tunInterface, err := NewTun(tunOptions) + if err != nil { + return err + } + + errors.LogInfo(t.ctx, tunName, " created") + + tunStackOptions := StackOptions{ + Tun: tunInterface, + IdleTimeout: pm.ForLevel(t.config.UserLevel).Timeouts.ConnectionIdle, + } + tunStack, err := NewStack(t.ctx, tunStackOptions, t) + if err != nil { + _ = tunInterface.Close() + return err + } + + err = tunStack.Start() + if err != nil { + _ = tunStack.Close() + _ = tunInterface.Close() + return err + } + + err = tunInterface.Start() + if err != nil { + _ = tunStack.Close() + _ = tunInterface.Close() + return err + } + + t.stack = tunStack + + errors.LogInfo(t.ctx, tunName, " up") + return nil +} + +// HandleConnection pass the connection coming from the ip stack to the routing dispatcher +func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) { + sid := session.NewID() + ctx := c.ContextWithID(t.ctx, sid) + errors.LogInfo(ctx, "processing connection from: ", conn.RemoteAddr()) + + inbound := session.Inbound{} + inbound.Name = "tun" + inbound.CanSpliceCopy = 1 + inbound.Source = net.DestinationFromAddr(conn.RemoteAddr()) + inbound.User = &protocol.MemoryUser{ + Level: t.config.UserLevel, + } + + ctx = session.ContextWithInbound(ctx, &inbound) + ctx = session.SubContextFromMuxInbound(ctx) + + link := &transport.Link{ + Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)}, + Writer: buf.NewWriter(conn), + } + if err := t.dispatcher.DispatchLink(ctx, destination, link); err != nil { + errors.LogError(ctx, errors.New("connection closed").Base(err)) + return + } + + errors.LogInfo(ctx, "connection completed") +} + +// Network implements proxy.Inbound +// and exists only to comply to proxy interface, declaring it doesn't listen on any network, +// making the process not open any port for this inbound (input will be network interface) +func (t *Handler) Network() []net.Network { + return []net.Network{} +} + +// Process implements proxy.Inbound +// and exists only to comply to proxy interface, which should never get any inputs due to no listening ports +func (t *Handler) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error { + return nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + t := &Handler{config: config.(*Config)} + err := core.RequireFeatures(ctx, func(pm policy.Manager, dispatcher routing.Dispatcher) error { + return t.Init(ctx, pm, dispatcher) + }) + return t, err + })) +} diff --git a/proxy/tun/stack.go b/proxy/tun/stack.go new file mode 100644 index 00000000..ba67a488 --- /dev/null +++ b/proxy/tun/stack.go @@ -0,0 +1,17 @@ +package tun + +import ( + "time" +) + +// Stack interface implement ip protocol stack, bridging raw network packets and data streams +type Stack interface { + Start() error + Close() error +} + +// StackOptions for the stack implementation +type StackOptions struct { + Tun Tun + IdleTimeout time.Duration +} diff --git a/proxy/tun/stack_gvisor.go b/proxy/tun/stack_gvisor.go new file mode 100644 index 00000000..81f30784 --- /dev/null +++ b/proxy/tun/stack_gvisor.go @@ -0,0 +1,206 @@ +package tun + +import ( + "context" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + "gvisor.dev/gvisor/pkg/waiter" +) + +const ( + defaultNIC tcpip.NICID = 1 + + tcpRXBufMinSize = tcp.MinBufferSize + tcpRXBufDefSize = tcp.DefaultSendBufferSize + tcpRXBufMaxSize = 8 << 20 // 8MiB + + tcpTXBufMinSize = tcp.MinBufferSize + tcpTXBufDefSize = tcp.DefaultReceiveBufferSize + tcpTXBufMaxSize = 6 << 20 // 6MiB +) + +// stackGVisor is ip stack implemented by gVisor package +type stackGVisor struct { + ctx context.Context + tun GVisorTun + idleTimeout time.Duration + handler *Handler + stack *stack.Stack + endpoint stack.LinkEndpoint +} + +// GVisorTun implements a bridge to connect gVisor ip stack to tun interface +type GVisorTun interface { + newEndpoint() (stack.LinkEndpoint, error) +} + +// NewStack builds new ip stack (using gVisor) +func NewStack(ctx context.Context, options StackOptions, handler *Handler) (Stack, error) { + gStack := &stackGVisor{ + ctx: ctx, + tun: options.Tun.(GVisorTun), + idleTimeout: options.IdleTimeout, + handler: handler, + } + + return gStack, nil +} + +// Start is called by Handler to bring stack to life +func (t *stackGVisor) Start() error { + linkEndpoint, err := t.tun.newEndpoint() + if err != nil { + return err + } + + ipStack, err := createStack(linkEndpoint) + if err != nil { + return err + } + + tcpForwarder := tcp.NewForwarder(ipStack, 0, 65535, func(r *tcp.ForwarderRequest) { + go func(r *tcp.ForwarderRequest) { + var wq waiter.Queue + var id = r.ID() + + // Perform a TCP three-way handshake. + ep, err := r.CreateEndpoint(&wq) + if err != nil { + errors.LogError(t.ctx, err.String()) + r.Complete(true) + return + } + + options := ep.SocketOptions() + options.SetKeepAlive(false) + options.SetReuseAddress(true) + options.SetReusePort(true) + + t.handler.HandleConnection( + gonet.NewTCPConn(&wq, ep), + // local address on the gVisor side is connection destination + net.TCPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)), + ) + + // close the socket + ep.Close() + // send connection complete upstream + r.Complete(false) + }(r) + }) + ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) + + udpForwarder := udp.NewForwarder(ipStack, func(r *udp.ForwarderRequest) { + go func(r *udp.ForwarderRequest) { + var wq waiter.Queue + var id = r.ID() + + ep, err := r.CreateEndpoint(&wq) + if err != nil { + errors.LogError(t.ctx, err.String()) + return + } + + options := ep.SocketOptions() + options.SetReuseAddress(true) + options.SetReusePort(true) + + t.handler.HandleConnection( + gonet.NewUDPConn(&wq, ep), + // local address on the gVisor side is connection destination + net.UDPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)), + ) + + // close the socket + ep.Close() + }(r) + }) + ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) + + t.stack = ipStack + t.endpoint = linkEndpoint + + return nil +} + +// Close is called by Handler to shut down the stack +func (t *stackGVisor) Close() error { + if t.stack == nil { + return nil + } + t.endpoint.Attach(nil) + t.stack.Close() + for _, endpoint := range t.stack.CleanupEndpoints() { + endpoint.Abort() + } + + return nil +} + +// createStack configure gVisor ip stack +func createStack(ep stack.LinkEndpoint) (*stack.Stack, error) { + opts := stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol}, + HandleLocal: false, + } + gStack := stack.New(opts) + + err := gStack.CreateNIC(defaultNIC, ep) + if err != nil { + return nil, errors.New(err.String()) + } + + gStack.SetRouteTable([]tcpip.Route{ + {Destination: header.IPv4EmptySubnet, NIC: defaultNIC}, + {Destination: header.IPv6EmptySubnet, NIC: defaultNIC}, + }) + + err = gStack.SetSpoofing(defaultNIC, true) + if err != nil { + return nil, errors.New(err.String()) + } + err = gStack.SetPromiscuousMode(defaultNIC, true) + if err != nil { + return nil, errors.New(err.String()) + } + + cOpt := tcpip.CongestionControlOption("cubic") + gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &cOpt) + sOpt := tcpip.TCPSACKEnabled(true) + gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &sOpt) + mOpt := tcpip.TCPModerateReceiveBufferOption(true) + gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &mOpt) + + tcpRXBufOpt := tcpip.TCPReceiveBufferSizeRangeOption{ + Min: tcpRXBufMinSize, + Default: tcpRXBufDefSize, + Max: tcpRXBufMaxSize, + } + err = gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpRXBufOpt) + if err != nil { + return nil, errors.New(err.String()) + } + + tcpTXBufOpt := tcpip.TCPSendBufferSizeRangeOption{ + Min: tcpTXBufMinSize, + Default: tcpTXBufDefSize, + Max: tcpTXBufMaxSize, + } + err = gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpTXBufOpt) + if err != nil { + return nil, errors.New(err.String()) + } + + return gStack, nil +} diff --git a/proxy/tun/tun.go b/proxy/tun/tun.go new file mode 100644 index 00000000..deb8511f --- /dev/null +++ b/proxy/tun/tun.go @@ -0,0 +1,13 @@ +package tun + +// Tun interface implements tun interface interaction +type Tun interface { + Start() error + Close() error +} + +// TunOptions for tun interface implementation +type TunOptions struct { + Name string + MTU uint32 +} diff --git a/proxy/tun/tun_android.go b/proxy/tun/tun_android.go new file mode 100644 index 00000000..eaa9339f --- /dev/null +++ b/proxy/tun/tun_android.go @@ -0,0 +1,58 @@ +//go:build android + +package tun + +import ( + "context" + "strconv" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform" + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/tcpip/link/fdbased" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +type AndroidTun struct { + tunFd int + options TunOptions +} + +// DefaultTun implements Tun +var _ Tun = (*AndroidTun)(nil) + +// DefaultTun implements GVisorTun +var _ GVisorTun = (*AndroidTun)(nil) + +// NewTun builds new tun interface handler +func NewTun(options TunOptions) (Tun, error) { + fd, err := strconv.Atoi(platform.NewEnvFlag(platform.TunFdKey).GetValue(func() string { return "0" })) + errors.LogInfo(context.Background(), "read Android Tun Fd ", fd, err) + + err = unix.SetNonblock(fd, true) + if err != nil { + _ = unix.Close(fd) + return nil, err + } + + return &AndroidTun{ + tunFd: fd, + options: options, + }, nil +} + +func (t *AndroidTun) Start() error { + return nil +} + +func (t *AndroidTun) Close() error { + return nil +} + +func (t *AndroidTun) newEndpoint() (stack.LinkEndpoint, error) { + return fdbased.New(&fdbased.Options{ + FDs: []int{t.tunFd}, + MTU: t.options.MTU, + RXChecksumOffload: true, + }) +} diff --git a/proxy/tun/tun_default.go b/proxy/tun/tun_default.go new file mode 100644 index 00000000..34075fa2 --- /dev/null +++ b/proxy/tun/tun_default.go @@ -0,0 +1,34 @@ +//go:build !linux && !windows && !android + +package tun + +import ( + "github.com/xtls/xray-core/common/errors" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +type DefaultTun struct { +} + +// DefaultTun implements Tun +var _ Tun = (*DefaultTun)(nil) + +// DefaultTun implements GVisorTun +var _ GVisorTun = (*DefaultTun)(nil) + +// NewTun builds new tun interface handler +func NewTun(options TunOptions) (Tun, error) { + return nil, errors.New("Tun is not supported on your platform") +} + +func (t *DefaultTun) Start() error { + return errors.New("Tun is not supported on your platform") +} + +func (t *DefaultTun) Close() error { + return errors.New("Tun is not supported on your platform") +} + +func (t *DefaultTun) newEndpoint() (stack.LinkEndpoint, error) { + return nil, errors.New("Tun is not supported on your platform") +} diff --git a/proxy/tun/tun_linux.go b/proxy/tun/tun_linux.go new file mode 100644 index 00000000..0813a216 --- /dev/null +++ b/proxy/tun/tun_linux.go @@ -0,0 +1,120 @@ +//go:build linux && !android + +package tun + +import ( + "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/tcpip/link/fdbased" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +// LinuxTun is an object that handles tun network interface on linux +// current version is heavily stripped to do nothing more, +// then create a network interface, to be provided as file descriptor to gVisor ip stack +type LinuxTun struct { + tunFd int + tunLink netlink.Link + options TunOptions +} + +// LinuxTun implements Tun +var _ Tun = (*LinuxTun)(nil) + +// LinuxTun implements GVisorTun +var _ GVisorTun = (*LinuxTun)(nil) + +// NewTun builds new tun interface handler (linux specific) +func NewTun(options TunOptions) (Tun, error) { + tunFd, err := open(options.Name) + if err != nil { + return nil, err + } + + tunLink, err := setup(options.Name, int(options.MTU)) + if err != nil { + _ = unix.Close(tunFd) + return nil, err + } + + linuxTun := &LinuxTun{ + tunFd: tunFd, + tunLink: tunLink, + options: options, + } + + return linuxTun, nil +} + +// open the file that implements tun interface in the OS +func open(name string) (int, error) { + fd, err := unix.Open("/dev/net/tun", unix.O_RDWR, 0) + if err != nil { + return -1, err + } + + ifr, err := unix.NewIfreq(name) + if err != nil { + _ = unix.Close(fd) + return 0, err + } + + flags := unix.IFF_TUN | unix.IFF_NO_PI + ifr.SetUint16(uint16(flags)) + err = unix.IoctlIfreq(fd, unix.TUNSETIFF, ifr) + if err != nil { + _ = unix.Close(fd) + return 0, err + } + + err = unix.SetNonblock(fd, true) + if err != nil { + _ = unix.Close(fd) + return 0, err + } + + return fd, nil +} + +// setup the interface through netlink socket +func setup(name string, MTU int) (netlink.Link, error) { + tunLink, err := netlink.LinkByName(name) + if err != nil { + return nil, err + } + + err = netlink.LinkSetMTU(tunLink, MTU) + if err != nil { + _ = netlink.LinkSetDown(tunLink) + return nil, err + } + + return tunLink, nil +} + +// Start is called by handler to bring tun interface to life +func (t *LinuxTun) Start() error { + err := netlink.LinkSetUp(t.tunLink) + if err != nil { + return err + } + + return nil +} + +// Close is called to shut down the tun interface +func (t *LinuxTun) Close() error { + _ = netlink.LinkSetDown(t.tunLink) + _ = unix.Close(t.tunFd) + + return nil +} + +// newEndpoint builds new gVisor stack.LinkEndpoint from the tun interface file descriptor +func (t *LinuxTun) newEndpoint() (stack.LinkEndpoint, error) { + return fdbased.New(&fdbased.Options{ + FDs: []int{t.tunFd}, + MTU: t.options.MTU, + RXChecksumOffload: true, + }) +} diff --git a/proxy/tun/tun_windows.go b/proxy/tun/tun_windows.go new file mode 100644 index 00000000..92d200e9 --- /dev/null +++ b/proxy/tun/tun_windows.go @@ -0,0 +1,84 @@ +//go:build windows + +package tun + +import ( + "golang.org/x/sys/windows" + "golang.zx2c4.com/wintun" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +// WindowsTun is an object that handles tun network interface on Windows +// current version is heavily stripped to do nothing more, +// then create a network interface, to be provided as endpoint to gVisor ip stack +type WindowsTun struct { + options TunOptions + adapter *wintun.Adapter + session wintun.Session + MTU uint32 +} + +// WindowsTun implements Tun +var _ Tun = (*WindowsTun)(nil) + +// WindowsTun implements GVisorTun +var _ GVisorTun = (*WindowsTun)(nil) + +// NewTun creates a Wintun interface with the given name. Should a Wintun +// interface with the same name exist, it tried to be reused. +func NewTun(options TunOptions) (Tun, error) { + // instantiate wintun adapter + adapter, err := open(options.Name) + if err != nil { + return nil, err + } + + // start the interface with ring buffer capacity of 8 MiB + session, err := adapter.StartSession(0x800000) + if err != nil { + _ = adapter.Close() + return nil, err + } + + tun := &WindowsTun{ + options: options, + adapter: adapter, + session: session, + // there is currently no iphndl.dll support, which is the netlink library for windows + // so there is nowhere to change MTU for the Wintun interface, and we take its default value + MTU: wintun.PacketSizeMax, + } + + return tun, nil +} + +func open(name string) (*wintun.Adapter, error) { + var guid *windows.GUID + // try to open existing adapter by name + adapter, err := wintun.OpenAdapter(name) + if err == nil { + return adapter, nil + } + // try to create adapter anew + adapter, err = wintun.CreateAdapter(name, "Xray", guid) + if err == nil { + return adapter, nil + } + return nil, err +} + +func (t *WindowsTun) Start() error { + return nil +} + +func (t *WindowsTun) Close() error { + t.session.End() + _ = t.adapter.Close() + + return nil +} + +// newEndpoint builds new gVisor stack.LinkEndpoint (WintunEndpoint) on top of WindowsTun +func (t *WindowsTun) newEndpoint() (stack.LinkEndpoint, error) { + return &WintunEndpoint{tun: t}, nil +} diff --git a/proxy/tun/tun_windows_endpoint.go b/proxy/tun/tun_windows_endpoint.go new file mode 100644 index 00000000..f8769788 --- /dev/null +++ b/proxy/tun/tun_windows_endpoint.go @@ -0,0 +1,180 @@ +//go:build windows + +package tun + +import ( + "context" + "errors" + _ "unsafe" + + "golang.org/x/sys/windows" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +// WintunEndpoint implements GVisor stack.LinkEndpoint +var _ stack.LinkEndpoint = (*WintunEndpoint)(nil) + +type WintunEndpoint struct { + tun *WindowsTun + dispatcherCancel context.CancelFunc +} + +var ErrUnsupportedNetworkProtocol = errors.New("unsupported ip version") + +//go:linkname procyield runtime.procyield +func procyield(cycles uint32) + +func (e *WintunEndpoint) MTU() uint32 { + return e.tun.MTU +} + +func (e *WintunEndpoint) SetMTU(mtu uint32) { + // not Implemented, as it is not expected GVisor will be asking tun device to be modified +} + +func (e *WintunEndpoint) MaxHeaderLength() uint16 { + return 0 +} + +func (e *WintunEndpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +func (e *WintunEndpoint) SetLinkAddress(addr tcpip.LinkAddress) { + // not Implemented, as it is not expected GVisor will be asking tun device to be modified +} + +func (e *WintunEndpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityRXChecksumOffload +} + +func (e *WintunEndpoint) Attach(dispatcher stack.NetworkDispatcher) { + if e.dispatcherCancel != nil { + e.dispatcherCancel() + e.dispatcherCancel = nil + } + + if dispatcher != nil { + ctx, cancel := context.WithCancel(context.Background()) + go e.dispatchLoop(ctx, dispatcher) + e.dispatcherCancel = cancel + } +} + +func (e *WintunEndpoint) IsAttached() bool { + return e.dispatcherCancel != nil +} + +func (e *WintunEndpoint) Wait() { + +} + +func (e *WintunEndpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +func (e *WintunEndpoint) AddHeader(buffer *stack.PacketBuffer) { + // tun interface doesn't have link layer header, it will be added by the OS +} + +func (e *WintunEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { + return true +} + +func (e *WintunEndpoint) Close() { + if e.dispatcherCancel != nil { + e.dispatcherCancel() + e.dispatcherCancel = nil + } +} + +func (e *WintunEndpoint) SetOnCloseAction(f func()) { + +} + +func (e *WintunEndpoint) WritePackets(packetBufferList stack.PacketBufferList) (int, tcpip.Error) { + var n int + // for all packets in the list to send + for _, packetBuffer := range packetBufferList.AsSlice() { + // request buffer from Wintun + packet, err := e.tun.session.AllocateSendPacket(packetBuffer.Size()) + if err != nil { + return n, &tcpip.ErrAborted{} + } + + // copy the bytes of slices that compose the packet into the allocated buffer + var index int + for _, packetElement := range packetBuffer.AsSlices() { + index += copy(packet[index:], packetElement) + } + + // signal Wintun to send that buffer as the packet + e.tun.session.SendPacket(packet) + n++ + } + return n, nil +} + +func (e *WintunEndpoint) readPacket() (tcpip.NetworkProtocolNumber, *stack.PacketBuffer, error) { + packet, err := e.tun.session.ReceivePacket() + if err != nil { + return 0, nil, err + } + + var networkProtocol tcpip.NetworkProtocolNumber + switch header.IPVersion(packet) { + case header.IPv4Version: + networkProtocol = header.IPv4ProtocolNumber + case header.IPv6Version: + networkProtocol = header.IPv6ProtocolNumber + default: + e.tun.session.ReleaseReceivePacket(packet) + return 0, nil, ErrUnsupportedNetworkProtocol + } + + packetBuffer := buffer.MakeWithView(buffer.NewViewWithData(packet)) + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: packetBuffer, + IsForwardedPacket: true, + OnRelease: func() { + e.tun.session.ReleaseReceivePacket(packet) + }, + }) + return networkProtocol, pkt, nil +} + +func (e *WintunEndpoint) dispatchLoop(ctx context.Context, dispatcher stack.NetworkDispatcher) { + readWait := e.tun.session.ReadWaitEvent() + + for { + select { + case <-ctx.Done(): + return + default: + networkProtocolNumber, packet, err := e.readPacket() + // read queue empty, yield slightly, wait for the spinlock, retry + if errors.Is(err, windows.ERROR_NO_MORE_ITEMS) { + procyield(1) + _, _ = windows.WaitForSingleObject(readWait, windows.INFINITE) + continue + } + // discard unknown network protocol packet + if errors.Is(err, ErrUnsupportedNetworkProtocol) { + continue + } + // stop dispatcher loop on any other interface failure + if err != nil { + e.Attach(nil) + continue + } + + // dispatch the buffer to the stack + dispatcher.DeliverNetworkPacket(networkProtocolNumber, packet) + // signal the buffer that it can be released + packet.DecRef() + } + } +} From 36425d2a6e53c755c96d25ef2323b7fffd1fab3f Mon Sep 17 00:00:00 2001 From: Hossin Asaadi Date: Thu, 8 Jan 2026 11:30:49 +0330 Subject: [PATCH 085/136] Tests: Improve geosite & geoip tests (#5502) https://github.com/XTLS/Xray-core/pull/5488#issuecomment-3711843548 --- app/router/condition_geoip_test.go | 83 +++++++++----------------- app/router/condition_test.go | 93 +++++++++++++++++++++--------- app/router/config.go | 6 +- common/platform/windows.go | 5 +- infra/conf/dns.go | 2 +- infra/conf/router.go | 6 +- 6 files changed, 103 insertions(+), 92 deletions(-) diff --git a/app/router/condition_geoip_test.go b/app/router/condition_geoip_test.go index b712db9e..de289a71 100644 --- a/app/router/condition_geoip_test.go +++ b/app/router/condition_geoip_test.go @@ -1,40 +1,17 @@ package router_test import ( - "fmt" "os" "path/filepath" + "runtime" "testing" "github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/common/platform" - "github.com/xtls/xray-core/common/platform/filesystem" - "google.golang.org/protobuf/proto" + "github.com/xtls/xray-core/infra/conf" ) -func getAssetPath(file string) (string, error) { - path := platform.GetAssetLocation(file) - _, err := os.Stat(path) - if os.IsNotExist(err) { - path := filepath.Join("..", "..", "resources", file) - _, err := os.Stat(path) - if os.IsNotExist(err) { - return "", fmt.Errorf("can't find %s in standard asset locations or {project_root}/resources", file) - } - if err != nil { - return "", fmt.Errorf("can't stat %s: %v", path, err) - } - return path, nil - } - if err != nil { - return "", fmt.Errorf("can't stat %s: %v", path, err) - } - - return path, nil -} - func TestGeoIPMatcher(t *testing.T) { cidrList := []*router.CIDR{ {Ip: []byte{0, 0, 0, 0}, Prefix: 8}, @@ -182,12 +159,11 @@ func TestGeoIPReverseMatcher(t *testing.T) { } func TestGeoIPMatcher4CN(t *testing.T) { - ips, err := loadGeoIP("CN") + geo := "geoip:cn" + geoip, err := loadGeoIP(geo) common.Must(err) - matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ - Cidr: ips, - }) + matcher, err := router.BuildOptimizedGeoIPMatcher(geoip) common.Must(err) if matcher.Match([]byte{8, 8, 8, 8}) { @@ -196,12 +172,11 @@ func TestGeoIPMatcher4CN(t *testing.T) { } func TestGeoIPMatcher6US(t *testing.T) { - ips, err := loadGeoIP("US") + geo := "geoip:us" + geoip, err := loadGeoIP(geo) common.Must(err) - matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ - Cidr: ips, - }) + matcher, err := router.BuildOptimizedGeoIPMatcher(geoip) common.Must(err) if !matcher.Match(net.ParseAddress("2001:4860:4860::8888").IP()) { @@ -209,37 +184,34 @@ func TestGeoIPMatcher6US(t *testing.T) { } } -func loadGeoIP(country string) ([]*router.CIDR, error) { - path, err := getAssetPath("geoip.dat") - if err != nil { - return nil, err - } - geoipBytes, err := filesystem.ReadFile(path) +func loadGeoIP(geo string) (*router.GeoIP, error) { + os.Setenv("XRAY_LOCATION_ASSET", filepath.Join("..", "..", "resources")) + + geoip, err := conf.ToCidrList([]string{geo}) if err != nil { return nil, err } - var geoipList router.GeoIPList - if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil { - return nil, err - } - - for _, geoip := range geoipList.Entry { - if geoip.CountryCode == country { - return geoip.Cidr, nil + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { + geoip, err = router.GetGeoIPList(geoip) + if err != nil { + return nil, err } } - panic("country not found: " + country) + if len(geoip) == 0 { + panic("country not found: " + geo) + } + + return geoip[0], nil } func BenchmarkGeoIPMatcher4CN(b *testing.B) { - ips, err := loadGeoIP("CN") + geo := "geoip:cn" + geoip, err := loadGeoIP(geo) common.Must(err) - matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ - Cidr: ips, - }) + matcher, err := router.BuildOptimizedGeoIPMatcher(geoip) common.Must(err) b.ResetTimer() @@ -250,12 +222,11 @@ func BenchmarkGeoIPMatcher4CN(b *testing.B) { } func BenchmarkGeoIPMatcher6US(b *testing.B) { - ips, err := loadGeoIP("US") + geo := "geoip:us" + geoip, err := loadGeoIP(geo) common.Must(err) - matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ - Cidr: ips, - }) + matcher, err := router.BuildOptimizedGeoIPMatcher(geoip) common.Must(err) b.ResetTimer() diff --git a/app/router/condition_test.go b/app/router/condition_test.go index 1272aef6..283e6725 100644 --- a/app/router/condition_test.go +++ b/app/router/condition_test.go @@ -1,20 +1,22 @@ package router_test import ( + "os" + "path/filepath" + "runtime" "strconv" "testing" + "github.com/xtls/xray-core/app/router" . "github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/common" - "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol/http" "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/features/routing" routing_session "github.com/xtls/xray-core/features/routing/session" - "google.golang.org/protobuf/proto" + "github.com/xtls/xray-core/infra/conf" ) func withBackground() routing.Context { @@ -300,32 +302,25 @@ func TestRoutingRule(t *testing.T) { } } -func loadGeoSite(country string) ([]*Domain, error) { - path, err := getAssetPath("geosite.dat") - if err != nil { - return nil, err - } - geositeBytes, err := filesystem.ReadFile(path) +func loadGeoSiteDomains(geo string) ([]*Domain, error) { + os.Setenv("XRAY_LOCATION_ASSET", filepath.Join("..", "..", "resources")) + + domains, err := conf.ParseDomainRule(geo) if err != nil { return nil, err } - var geositeList GeoSiteList - if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil { - return nil, err - } - - for _, site := range geositeList.Entry { - if site.CountryCode == country { - return site.Domain, nil + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { + domains, err = router.GetDomainList(domains) + if err != nil { + return nil, err } } - - return nil, errors.New("country not found: " + country) + return domains, nil } func TestChinaSites(t *testing.T) { - domains, err := loadGeoSite("CN") + domains, err := loadGeoSiteDomains("geosite:cn") common.Must(err) acMatcher, err := NewMphMatcherGroup(domains) @@ -366,8 +361,50 @@ func TestChinaSites(t *testing.T) { } } +func TestChinaSitesWithAttrs(t *testing.T) { + domains, err := loadGeoSiteDomains("geosite:google@cn") + common.Must(err) + + acMatcher, err := NewMphMatcherGroup(domains) + common.Must(err) + + type TestCase struct { + Domain string + Output bool + } + testCases := []TestCase{ + { + Domain: "google.cn", + Output: true, + }, + { + Domain: "recaptcha.net", + Output: true, + }, + { + Domain: "164.com", + Output: false, + }, + { + Domain: "164.com", + Output: false, + }, + } + + for i := 0; i < 1024; i++ { + testCases = append(testCases, TestCase{Domain: strconv.Itoa(i) + ".not-exists.com", Output: false}) + } + + for _, testCase := range testCases { + r := acMatcher.ApplyDomain(testCase.Domain) + if r != testCase.Output { + t.Error("ACDomainMatcher expected output ", testCase.Output, " for domain ", testCase.Domain, " but got ", r) + } + } +} + func BenchmarkMphDomainMatcher(b *testing.B) { - domains, err := loadGeoSite("CN") + domains, err := loadGeoSiteDomains("geosite:cn") common.Must(err) matcher, err := NewMphMatcherGroup(domains) @@ -412,11 +449,11 @@ func BenchmarkMultiGeoIPMatcher(b *testing.B) { var geoips []*GeoIP { - ips, err := loadGeoIP("CN") + ips, err := loadGeoIP("geoip:cn") common.Must(err) geoips = append(geoips, &GeoIP{ CountryCode: "CN", - Cidr: ips, + Cidr: ips.Cidr, }) } @@ -425,25 +462,25 @@ func BenchmarkMultiGeoIPMatcher(b *testing.B) { common.Must(err) geoips = append(geoips, &GeoIP{ CountryCode: "JP", - Cidr: ips, + Cidr: ips.Cidr, }) } { - ips, err := loadGeoIP("CA") + ips, err := loadGeoIP("geoip:ca") common.Must(err) geoips = append(geoips, &GeoIP{ CountryCode: "CA", - Cidr: ips, + Cidr: ips.Cidr, }) } { - ips, err := loadGeoIP("US") + ips, err := loadGeoIP("geoip:us") common.Must(err) geoips = append(geoips, &GeoIP{ CountryCode: "US", - Cidr: ips, + Cidr: ips.Cidr, }) } diff --git a/app/router/config.go b/app/router/config.go index 1566b560..5399589b 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -112,7 +112,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { domains := rr.Domain if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { var err error - domains, err = getDomainList(rr.Domain) + domains, err = GetDomainList(rr.Domain) if err != nil { return nil, errors.New("failed to build domains from mmap").Base(err) } @@ -122,7 +122,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { if err != nil { return nil, errors.New("failed to build domain condition with MphDomainMatcher").Base(err) } - errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)") + errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(domains), " domain rule(s)") conds.Add(matcher) } @@ -214,7 +214,7 @@ func GetGeoIPList(ips []*GeoIP) ([]*GeoIP, error) { } -func getDomainList(domains []*Domain) ([]*Domain, error) { +func GetDomainList(domains []*Domain) ([]*Domain, error) { domainList := []*Domain{} for _, domain := range domains { val := strings.Split(domain.Value, "_") diff --git a/common/platform/windows.go b/common/platform/windows.go index 684ddc9c..5bd15520 100644 --- a/common/platform/windows.go +++ b/common/platform/windows.go @@ -3,7 +3,9 @@ package platform -import "path/filepath" +import ( + "path/filepath" +) func LineSeparator() string { return "\r\n" @@ -12,6 +14,7 @@ func LineSeparator() string { // GetAssetLocation searches for `file` in the env dir and the executable dir func GetAssetLocation(file string) string { assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) + return filepath.Join(assetPath, file) } diff --git a/infra/conf/dns.go b/infra/conf/dns.go index 6ec307c6..f6d56913 100644 --- a/infra/conf/dns.go +++ b/infra/conf/dns.go @@ -89,7 +89,7 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) { var originalRules []*dns.NameServer_OriginalRule for _, rule := range c.Domains { - parsedDomain, err := parseDomainRule(rule) + parsedDomain, err := ParseDomainRule(rule) if err != nil { return nil, errors.New("invalid domain rule: ", rule).Base(err) } diff --git a/infra/conf/router.go b/infra/conf/router.go index 54320b3b..c9f5b43b 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -291,7 +291,7 @@ func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, er return filteredDomains, nil } -func parseDomainRule(domain string) ([]*router.Domain, error) { +func ParseDomainRule(domain string) ([]*router.Domain, error) { if strings.HasPrefix(domain, "geosite:") { country := strings.ToUpper(domain[8:]) domains, err := loadGeositeWithAttr("geosite.dat", country) @@ -489,7 +489,7 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) { if rawFieldRule.Domain != nil { for _, domain := range *rawFieldRule.Domain { - rules, err := parseDomainRule(domain) + rules, err := ParseDomainRule(domain) if err != nil { return nil, errors.New("failed to parse domain rule: ", domain).Base(err) } @@ -499,7 +499,7 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) { if rawFieldRule.Domains != nil { for _, domain := range *rawFieldRule.Domains { - rules, err := parseDomainRule(domain) + rules, err := ParseDomainRule(domain) if err != nil { return nil, errors.New("failed to parse domain rule: ", domain).Base(err) } From 0ca13452b8e99824e08c2c860dd0f84a4ae2859d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Fri, 9 Jan 2026 08:11:24 +0800 Subject: [PATCH 086/136] TLS config: Add `pinnedPeerCertSha256`; Remove `pinnedPeerCertificateChainSha256` and `pinnedPeerCertificatePublicKeySha256` (#5154) Usage: https://github.com/XTLS/Xray-core/pull/5507 --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- infra/conf/transport_internet.go | 68 +++---- main/commands/all/tls/certchainhash.go | 40 ---- main/commands/all/tls/leafcerthash.go | 44 +++++ main/commands/all/tls/ping.go | 12 +- main/commands/all/tls/tls.go | 2 +- testing/scenarios/tls_test.go | 48 ++--- transport/internet/tls/config.go | 99 ++++++---- transport/internet/tls/config.pb.go | 31 ++- transport/internet/tls/config.proto | 2 + transport/internet/tls/pin.go | 41 ++-- transport/internet/tls/pin_test.go | 254 ++++++++----------------- 11 files changed, 284 insertions(+), 357 deletions(-) delete mode 100644 main/commands/all/tls/certchainhash.go create mode 100644 main/commands/all/tls/leafcerthash.go diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 5a860b1c..2544c016 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -395,27 +395,26 @@ func (c *TLSCertConfig) Build() (*tls.Certificate, error) { } type TLSConfig struct { - Insecure bool `json:"allowInsecure"` - Certs []*TLSCertConfig `json:"certificates"` - ServerName string `json:"serverName"` - ALPN *StringList `json:"alpn"` - EnableSessionResumption bool `json:"enableSessionResumption"` - DisableSystemRoot bool `json:"disableSystemRoot"` - MinVersion string `json:"minVersion"` - MaxVersion string `json:"maxVersion"` - CipherSuites string `json:"cipherSuites"` - Fingerprint string `json:"fingerprint"` - RejectUnknownSNI bool `json:"rejectUnknownSni"` - PinnedPeerCertificateChainSha256 *[]string `json:"pinnedPeerCertificateChainSha256"` - PinnedPeerCertificatePublicKeySha256 *[]string `json:"pinnedPeerCertificatePublicKeySha256"` - CurvePreferences *StringList `json:"curvePreferences"` - MasterKeyLog string `json:"masterKeyLog"` - ServerNameToVerify string `json:"serverNameToVerify"` - VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` - ECHServerKeys string `json:"echServerKeys"` - ECHConfigList string `json:"echConfigList"` - ECHForceQuery string `json:"echForceQuery"` - ECHSocketSettings *SocketConfig `json:"echSockopt"` + Insecure bool `json:"allowInsecure"` + Certs []*TLSCertConfig `json:"certificates"` + ServerName string `json:"serverName"` + ALPN *StringList `json:"alpn"` + EnableSessionResumption bool `json:"enableSessionResumption"` + DisableSystemRoot bool `json:"disableSystemRoot"` + MinVersion string `json:"minVersion"` + MaxVersion string `json:"maxVersion"` + CipherSuites string `json:"cipherSuites"` + Fingerprint string `json:"fingerprint"` + RejectUnknownSNI bool `json:"rejectUnknownSni"` + PinnedPeerCertSha256 string `json:"pinnedPeerCertSha256"` + CurvePreferences *StringList `json:"curvePreferences"` + MasterKeyLog string `json:"masterKeyLog"` + ServerNameToVerify string `json:"serverNameToVerify"` + VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` + ECHServerKeys string `json:"echServerKeys"` + ECHConfigList string `json:"echConfigList"` + ECHForceQuery string `json:"echForceQuery"` + ECHSocketSettings *SocketConfig `json:"echSockopt"` } // Build implements Buildable. @@ -458,25 +457,20 @@ func (c *TLSConfig) Build() (proto.Message, error) { } config.RejectUnknownSni = c.RejectUnknownSNI - if c.PinnedPeerCertificateChainSha256 != nil { - config.PinnedPeerCertificateChainSha256 = [][]byte{} - for _, v := range *c.PinnedPeerCertificateChainSha256 { - hashValue, err := base64.StdEncoding.DecodeString(v) + if c.PinnedPeerCertSha256 != "" { + config.PinnedPeerCertSha256 = [][]byte{} + // Split by tilde separator + hashes := strings.Split(c.PinnedPeerCertSha256, "~") + for _, v := range hashes { + v = strings.TrimSpace(v) + if v == "" { + continue + } + hashValue, err := hex.DecodeString(v) if err != nil { return nil, err } - config.PinnedPeerCertificateChainSha256 = append(config.PinnedPeerCertificateChainSha256, hashValue) - } - } - - if c.PinnedPeerCertificatePublicKeySha256 != nil { - config.PinnedPeerCertificatePublicKeySha256 = [][]byte{} - for _, v := range *c.PinnedPeerCertificatePublicKeySha256 { - hashValue, err := base64.StdEncoding.DecodeString(v) - if err != nil { - return nil, err - } - config.PinnedPeerCertificatePublicKeySha256 = append(config.PinnedPeerCertificatePublicKeySha256, hashValue) + config.PinnedPeerCertSha256 = append(config.PinnedPeerCertSha256, hashValue) } } diff --git a/main/commands/all/tls/certchainhash.go b/main/commands/all/tls/certchainhash.go deleted file mode 100644 index 304f668d..00000000 --- a/main/commands/all/tls/certchainhash.go +++ /dev/null @@ -1,40 +0,0 @@ -package tls - -import ( - "flag" - "fmt" - "os" - - "github.com/xtls/xray-core/main/commands/base" - "github.com/xtls/xray-core/transport/internet/tls" -) - -var cmdCertChainHash = &base.Command{ - UsageLine: "{{.Exec}} certChainHash", - Short: "Calculate TLS certificates hash.", - Long: ` - xray tls certChainHash --cert - Calculate TLS certificate chain hash. - `, -} - -func init() { - cmdCertChainHash.Run = executeCertChainHash // break init loop -} - -var input = cmdCertChainHash.Flag.String("cert", "fullchain.pem", "The file path of the certificates chain") - -func executeCertChainHash(cmd *base.Command, args []string) { - fs := flag.NewFlagSet("certChainHash", flag.ContinueOnError) - if err := fs.Parse(args); err != nil { - fmt.Println(err) - return - } - certContent, err := os.ReadFile(*input) - if err != nil { - fmt.Println(err) - return - } - certChainHashB64 := tls.CalculatePEMCertChainSHA256Hash(certContent) - fmt.Println(certChainHashB64) -} diff --git a/main/commands/all/tls/leafcerthash.go b/main/commands/all/tls/leafcerthash.go new file mode 100644 index 00000000..06bbf5cd --- /dev/null +++ b/main/commands/all/tls/leafcerthash.go @@ -0,0 +1,44 @@ +package tls + +import ( + "flag" + "fmt" + "os" + + "github.com/xtls/xray-core/main/commands/base" + "github.com/xtls/xray-core/transport/internet/tls" +) + +var cmdLeafCertHash = &base.Command{ + UsageLine: "{{.Exec}} tls leafCertHash", + Short: "Calculate TLS leaf certificate hash.", + Long: ` + xray tls leafCertHash --cert + Calculate TLS leaf certificate hash. + `, +} + +func init() { + cmdLeafCertHash.Run = executeLeafCertHash // break init loop +} + +var input = cmdLeafCertHash.Flag.String("cert", "fullchain.pem", "The file path of the leaf certificate") + +func executeLeafCertHash(cmd *base.Command, args []string) { + fs := flag.NewFlagSet("leafCertHash", flag.ContinueOnError) + if err := fs.Parse(args); err != nil { + fmt.Println(err) + return + } + certContent, err := os.ReadFile(*input) + if err != nil { + fmt.Println(err) + return + } + certChainHashB64, err := tls.CalculatePEMLeafCertSHA256Hash(certContent) + if err != nil { + fmt.Println("failed to decode cert", err) + return + } + fmt.Println(certChainHashB64) +} diff --git a/main/commands/all/tls/ping.go b/main/commands/all/tls/ping.go index ccc5fe6b..6417b74c 100644 --- a/main/commands/all/tls/ping.go +++ b/main/commands/all/tls/ping.go @@ -3,7 +3,7 @@ package tls import ( gotls "crypto/tls" "crypto/x509" - "encoding/base64" + "encoding/hex" "fmt" "net" "strconv" @@ -156,8 +156,14 @@ func printTLSConnDetail(tlsConn *gotls.Conn) { func showCert() func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - hash := GenerateCertChainHash(rawCerts) - fmt.Println("Certificate Chain Hash: ", base64.StdEncoding.EncodeToString(hash)) + var hash []byte + for _, asn1Data := range rawCerts { + cert, _ := x509.ParseCertificate(asn1Data) + if cert.IsCA { + hash = GenerateCertHash(cert) + } + } + fmt.Println("Certificate Leaf Hash: ", hex.EncodeToString(hash)) return nil } } diff --git a/main/commands/all/tls/tls.go b/main/commands/all/tls/tls.go index a93da1c3..159bf769 100644 --- a/main/commands/all/tls/tls.go +++ b/main/commands/all/tls/tls.go @@ -13,7 +13,7 @@ var CmdTLS = &base.Command{ Commands: []*base.Command{ cmdCert, cmdPing, - cmdCertChainHash, + cmdLeafCertHash, cmdECH, }, } diff --git a/testing/scenarios/tls_test.go b/testing/scenarios/tls_test.go index 250e5d47..69b38288 100644 --- a/testing/scenarios/tls_test.go +++ b/testing/scenarios/tls_test.go @@ -92,7 +92,7 @@ func TestSimpleTLSConnection(t *testing.T) { Receiver: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vmess.Account{ Id: userID.String(), }), @@ -203,7 +203,7 @@ func TestAutoIssuingCertificate(t *testing.T) { Receiver: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vmess.Account{ Id: userID.String(), }), @@ -304,7 +304,7 @@ func TestTLSOverKCP(t *testing.T) { Receiver: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vmess.Account{ Id: userID.String(), }), @@ -400,7 +400,7 @@ func TestTLSOverWebSocket(t *testing.T) { Receiver: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vmess.Account{ Id: userID.String(), }), @@ -512,7 +512,7 @@ func TestGRPC(t *testing.T) { Receiver: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vmess.Account{ Id: userID.String(), }), @@ -624,7 +624,7 @@ func TestGRPCMultiMode(t *testing.T) { Receiver: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vmess.Account{ Id: userID.String(), }), @@ -674,7 +674,7 @@ func TestSimpleTLSConnectionPinned(t *testing.T) { defer tcpServer.Close() certificateDer := cert.MustGenerate(nil) certificate := tls.ParseCertificate(certificateDer) - certHash := tls.GenerateCertChainHash([][]byte{certificateDer.Certificate}) + certHash := tls.GenerateCertHash(certificateDer.Certificate) userID := protocol.NewID(uuid.New()) serverPort := tcp.PickPort() serverConfig := &core.Config{ @@ -731,7 +731,7 @@ func TestSimpleTLSConnectionPinned(t *testing.T) { Receiver: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vmess.Account{ Id: userID.String(), }), @@ -743,8 +743,8 @@ func TestSimpleTLSConnectionPinned(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - AllowInsecure: true, - PinnedPeerCertificateChainSha256: [][]byte{certHash}, + AllowInsecure: true, + PinnedPeerCertSha256: [][]byte{certHash}, }), }, }, @@ -771,7 +771,7 @@ func TestSimpleTLSConnectionPinnedWrongCert(t *testing.T) { defer tcpServer.Close() certificateDer := cert.MustGenerate(nil) certificate := tls.ParseCertificate(certificateDer) - certHash := tls.GenerateCertChainHash([][]byte{certificateDer.Certificate}) + certHash := tls.GenerateCertHash(certificateDer.Certificate) certHash[1] += 1 userID := protocol.NewID(uuid.New()) serverPort := tcp.PickPort() @@ -829,7 +829,7 @@ func TestSimpleTLSConnectionPinnedWrongCert(t *testing.T) { Receiver: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vmess.Account{ Id: userID.String(), }), @@ -841,8 +841,8 @@ func TestSimpleTLSConnectionPinnedWrongCert(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - AllowInsecure: true, - PinnedPeerCertificateChainSha256: [][]byte{certHash}, + AllowInsecure: true, + PinnedPeerCertSha256: [][]byte{certHash}, }), }, }, @@ -869,7 +869,7 @@ func TestUTLSConnectionPinned(t *testing.T) { defer tcpServer.Close() certificateDer := cert.MustGenerate(nil) certificate := tls.ParseCertificate(certificateDer) - certHash := tls.GenerateCertChainHash([][]byte{certificateDer.Certificate}) + certHash := tls.GenerateCertHash(certificateDer.Certificate) userID := protocol.NewID(uuid.New()) serverPort := tcp.PickPort() serverConfig := &core.Config{ @@ -926,7 +926,7 @@ func TestUTLSConnectionPinned(t *testing.T) { Receiver: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vmess.Account{ Id: userID.String(), }), @@ -938,9 +938,9 @@ func TestUTLSConnectionPinned(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - Fingerprint: "random", - AllowInsecure: true, - PinnedPeerCertificateChainSha256: [][]byte{certHash}, + Fingerprint: "random", + AllowInsecure: true, + PinnedPeerCertSha256: [][]byte{certHash}, }), }, }, @@ -967,7 +967,7 @@ func TestUTLSConnectionPinnedWrongCert(t *testing.T) { defer tcpServer.Close() certificateDer := cert.MustGenerate(nil) certificate := tls.ParseCertificate(certificateDer) - certHash := tls.GenerateCertChainHash([][]byte{certificateDer.Certificate}) + certHash := tls.GenerateCertHash(certificateDer.Certificate) certHash[1] += 1 userID := protocol.NewID(uuid.New()) serverPort := tcp.PickPort() @@ -1025,7 +1025,7 @@ func TestUTLSConnectionPinnedWrongCert(t *testing.T) { Receiver: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vmess.Account{ Id: userID.String(), }), @@ -1037,9 +1037,9 @@ func TestUTLSConnectionPinnedWrongCert(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - Fingerprint: "random", - AllowInsecure: true, - PinnedPeerCertificateChainSha256: [][]byte{certHash}, + Fingerprint: "random", + AllowInsecure: true, + PinnedPeerCertSha256: [][]byte{certHash}, }), }, }, diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index 90427b8d..a25ad6ca 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -7,7 +7,6 @@ import ( "crypto/rand" "crypto/tls" "crypto/x509" - "encoding/base64" "os" "slices" "strings" @@ -281,15 +280,35 @@ func (c *Config) parseServerName() string { return c.ServerName } -func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { +func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) (err error) { + // extract x509 certificates from rawCerts(verifiedChains will be nil if InsecureSkipVerify is true) + certs := make([]*x509.Certificate, len(rawCerts)) + for i, asn1Data := range rawCerts { + certs[i], _ = x509.ParseCertificate(asn1Data) + } + + // directly return success if pinned cert is leaf + // or add the CA to RootCAs if pinned cert is CA(and can be used in VerifyPeerCertInNames for Self signed CA) + RootCAs := r.RootCAs + if r.PinnedPeerCertSha256 != nil { + verifyResult, verifiedCert := verifyChain(certs, r.PinnedPeerCertSha256) + switch verifyResult { + case certNotFound: + return errors.New("peer cert is unrecognized") + case foundLeaf: + return nil + case foundCA: + RootCAs = x509.NewCertPool() + RootCAs.AddCert(verifiedCert) + default: + panic("impossible PinnedPeerCertificateSha256 verify result") + } + } + if r.VerifyPeerCertInNames != nil { if len(r.VerifyPeerCertInNames) > 0 { - certs := make([]*x509.Certificate, len(rawCerts)) - for i, asn1Data := range rawCerts { - certs[i], _ = x509.ParseCertificate(asn1Data) - } opts := x509.VerifyOptions{ - Roots: r.RootCAs, + Roots: RootCAs, CurrentTime: time.Now(), Intermediates: x509.NewCertPool(), } @@ -302,42 +321,14 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509 } } } - if r.PinnedPeerCertificateChainSha256 == nil { - return errors.New("peer cert is invalid.") - } - } - - if r.PinnedPeerCertificateChainSha256 != nil { - hashValue := GenerateCertChainHash(rawCerts) - for _, v := range r.PinnedPeerCertificateChainSha256 { - if hmac.Equal(hashValue, v) { - return nil - } - } - return errors.New("peer cert is unrecognized: ", base64.StdEncoding.EncodeToString(hashValue)) - } - - if r.PinnedPeerCertificatePublicKeySha256 != nil { - for _, v := range verifiedChains { - for _, cert := range v { - publicHash := GenerateCertPublicKeyHash(cert) - for _, c := range r.PinnedPeerCertificatePublicKeySha256 { - if hmac.Equal(publicHash, c) { - return nil - } - } - } - } - return errors.New("peer public key is unrecognized.") } return nil } type RandCarrier struct { - RootCAs *x509.CertPool - VerifyPeerCertInNames []string - PinnedPeerCertificateChainSha256 [][]byte - PinnedPeerCertificatePublicKeySha256 [][]byte + RootCAs *x509.CertPool + VerifyPeerCertInNames []string + PinnedPeerCertSha256 [][]byte } func (r *RandCarrier) Read(p []byte) (n int, err error) { @@ -362,10 +353,9 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { } randCarrier := &RandCarrier{ - RootCAs: root, - VerifyPeerCertInNames: slices.Clone(c.VerifyPeerCertInNames), - PinnedPeerCertificateChainSha256: c.PinnedPeerCertificateChainSha256, - PinnedPeerCertificatePublicKeySha256: c.PinnedPeerCertificatePublicKeySha256, + RootCAs: root, + VerifyPeerCertInNames: slices.Clone(c.VerifyPeerCertInNames), + PinnedPeerCertSha256: c.PinnedPeerCertSha256, } config := &tls.Config{ Rand: randCarrier, @@ -526,3 +516,28 @@ func ParseCurveName(curveNames []string) []tls.CurveID { func IsFromMitm(str string) bool { return strings.ToLower(str) == "frommitm" } + +type verifyResult int + +const ( + certNotFound verifyResult = iota + foundLeaf + foundCA +) + +func verifyChain(certs []*x509.Certificate, PinnedPeerCertificateSha256 [][]byte) (verifyResult, *x509.Certificate) { + for _, cert := range certs { + certHash := GenerateCertHash(cert) + for _, c := range PinnedPeerCertificateSha256 { + if hmac.Equal(certHash, c) { + if cert.IsCA { + return foundCA, cert + } else { + return foundLeaf, cert + } + + } + } + } + return certNotFound, nil +} diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index b93af678..2abd0c3e 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -222,6 +222,7 @@ type Config struct { EchConfigList string `protobuf:"bytes,19,opt,name=ech_config_list,json=echConfigList,proto3" json:"ech_config_list,omitempty"` EchForceQuery string `protobuf:"bytes,20,opt,name=ech_force_query,json=echForceQuery,proto3" json:"ech_force_query,omitempty"` EchSocketSettings *internet.SocketConfig `protobuf:"bytes,21,opt,name=ech_socket_settings,json=echSocketSettings,proto3" json:"ech_socket_settings,omitempty"` + PinnedPeerCertSha256 [][]byte `protobuf:"bytes,22,rep,name=pinned_peer_cert_sha256,json=pinnedPeerCertSha256,proto3" json:"pinned_peer_cert_sha256,omitempty"` } func (x *Config) Reset() { @@ -394,6 +395,13 @@ func (x *Config) GetEchSocketSettings() *internet.SocketConfig { return nil } +func (x *Config) GetPinnedPeerCertSha256() [][]byte { + if x != nil { + return x.PinnedPeerCertSha256 + } + return nil +} + var File_transport_internet_tls_config_proto protoreflect.FileDescriptor var file_transport_internet_tls_config_proto_rawDesc = []byte{ @@ -427,7 +435,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x45, 0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, - 0x59, 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xe9, 0x07, 0x0a, 0x06, 0x43, 0x6f, + 0x59, 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xa0, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, @@ -490,15 +498,18 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x65, 0x63, 0x68, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x53, 0x65, 0x74, - 0x74, 0x69, 0x6e, 0x67, 0x73, 0x42, 0x73, 0x0a, 0x1f, 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, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 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, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, - 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x35, 0x0a, 0x17, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x5f, + 0x70, 0x65, 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, + 0x18, 0x16, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x14, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x65, + 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x42, 0x73, 0x0a, 0x1f, + 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, 0x2e, 0x74, 0x6c, 0x73, 0x50, + 0x01, 0x5a, 0x30, 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, 0x2f, + 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, + 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index 6d39bc56..cb7a225e 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -101,4 +101,6 @@ message Config { string ech_force_query = 20; SocketConfig ech_socket_settings = 21; + + repeated bytes pinned_peer_cert_sha256 = 22; } diff --git a/transport/internet/tls/pin.go b/transport/internet/tls/pin.go index f561bfdf..060f5d73 100644 --- a/transport/internet/tls/pin.go +++ b/transport/internet/tls/pin.go @@ -3,40 +3,37 @@ package tls import ( "crypto/sha256" "crypto/x509" - "encoding/base64" + "encoding/hex" "encoding/pem" ) -func CalculatePEMCertChainSHA256Hash(certContent []byte) string { - var certChain [][]byte +func CalculatePEMLeafCertSHA256Hash(certContent []byte) (string, error) { + var leafCert *x509.Certificate for { + var err error block, remain := pem.Decode(certContent) if block == nil { break } - certChain = append(certChain, block.Bytes) + leafCert, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return "", err + } certContent = remain } - certChainHash := GenerateCertChainHash(certChain) - certChainHashB64 := base64.StdEncoding.EncodeToString(certChainHash) - return certChainHashB64 + certHash := GenerateCertHash(leafCert) + certHashHex := hex.EncodeToString(certHash) + return certHashHex, nil } -func GenerateCertChainHash(rawCerts [][]byte) []byte { - var hashValue []byte - for _, certValue := range rawCerts { - out := sha256.Sum256(certValue) - if hashValue == nil { - hashValue = out[:] - } else { - newHashValue := sha256.Sum256(append(hashValue, out[:]...)) - hashValue = newHashValue[:] - } +// []byte must be ASN.1 DER content +func GenerateCertHash[T *x509.Certificate | []byte](cert T) []byte { + var out [32]byte + switch v := any(cert).(type) { + case *x509.Certificate: + out = sha256.Sum256(v.Raw) + case []byte: + out = sha256.Sum256(v) } - return hashValue -} - -func GenerateCertPublicKeyHash(cert *x509.Certificate) []byte { - out := sha256.Sum256(cert.RawSubjectPublicKeyInfo) return out[:] } diff --git a/transport/internet/tls/pin_test.go b/transport/internet/tls/pin_test.go index cfc60e17..e0558c45 100644 --- a/transport/internet/tls/pin_test.go +++ b/transport/internet/tls/pin_test.go @@ -2,7 +2,7 @@ package tls import ( "crypto/x509" - "encoding/base64" + "encoding/hex" "encoding/pem" "testing" @@ -10,190 +10,88 @@ import ( ) func TestCalculateCertHash(t *testing.T) { - /* This is used to make sure that the hash signature generated is consistent - Do NOT change this test to suit your modification. - */ - const CertBundle = ` ------BEGIN CERTIFICATE----- -MIIFJjCCBA6gAwIBAgISBL8FgUdEcVYEjdMkTZPgC3ocMA0GCSqGSIb3DQEBCwUA -MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD -EwJSMzAeFw0yMTAzMjkwMTM2MzlaFw0yMTA2MjcwMTM2MzlaMBsxGTAXBgNVBAMT -EHNlY3VyZS5ra2Rldi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQDOF/j+s7rHaDMXdhYjffoOFjNZb7n3sCuvubI3qOcgJmr1WPlCEry50KoY8FaB -IF2HstMIZceN4NoUK7mr3WAvsQTA47uBfuhp+XQmAQW0T/fYD+XbvxtCENEin+xm -JsvTKZLTKbE08E964J4H+1sBmueP6rvy2Wt95z0XkqoQiikpmLE87WdltQcATvVX -qqrL64hV0nN4Hdi2Bv1cQ92aR7lZGj8jiQRtTj8y5Ah3Gk3fPoao+yI7gnzembqo -fddePzz/u8iEuvYAsIYZKn9bbS7rkYoJazL2/xiDZR7usn0SomzmM6lGXDD3FF4b -lyTkLYwgFVgbGWoz1+eOHD5BAgMBAAGjggJLMIICRzAOBgNVHQ8BAf8EBAMCBaAw -HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD -VR0OBBYEFOPRdApL8XENLXDuU3oPisykGyp+MB8GA1UdIwQYMBaAFBQusxe3WFbL -rlAJQOYfr52LFMLGMFUGCCsGAQUFBwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDov -L3IzLm8ubGVuY3Iub3JnMCIGCCsGAQUFBzAChhZodHRwOi8vcjMuaS5sZW5jci5v -cmcvMBsGA1UdEQQUMBKCEHNlY3VyZS5ra2Rldi5vcmcwTAYDVR0gBEUwQzAIBgZn -gQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5s -ZXRzZW5jcnlwdC5vcmcwggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdQD2XJQv0Xcw -IhRUGAgwlFaO400TGTO/3wwvIAvMTvFk4wAAAXh71yBGAAAEAwBGMEQCIDmziDOn -ehPY2KoAFX8fPWiCm4EBTbGJXBWF1LCotPJBAiBLSCg+krXvbyoABnTm8knv0hbG -/ZOk8LV6qpw9VoQwGwB3AG9Tdqwx8DEZ2JkApFEV/3cVHBHZAsEAKQaNsgiaN9kT -AAABeHvXIIAAAAQDAEgwRgIhAOkeKc52wR3n5QWZfa3zbbicMMSQrTGbQ+1fHNs7 -SsRvAiEAqbflDx1nZRsTA22FfNYfgF6v5Z3/svjiTleWSQad4WswDQYJKoZIhvcN -AQELBQADggEBAEj8tg+Agf5sNBM9CvjeXbA0fkpGDaQzXEkwefAea5vPgKoGiWSN -pHDkyr0i7+mqa7cMXhmmo20V0/+QDv0nrsCw8pgJuviC3GvK6agT6WfkXM2djJuo -osPeXOL9KEF/ATv0EyM5tr9TIoRSSYQoRhuqEdg3Dz9Ii8GXR5HhbYr6Cd7gwNHS -kYeivKDmgv31GHb4twPSE9LZ/U+56lNVvSbJ4csupIF3GnxnxrFSmijYNOPoM6mj -tzY45d4mjPs0fKCFKSsVM6YT0tX4NwIKsOaeQg30WLtRyDwYm6ma/a/UUUS0FloZ -2/p85glOgzownfoRjzTbqHu8ewtMd6Apc0E= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEZTCCA02gAwIBAgIQQAF1BIMUpMghjISpDBbN3zANBgkqhkiG9w0BAQsFADA/ -MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT -DkRTVCBSb290IENBIFgzMB4XDTIwMTAwNzE5MjE0MFoXDTIxMDkyOTE5MjE0MFow -MjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxCzAJBgNVBAMT -AlIzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuwIVKMz2oJTTDxLs -jVWSw/iC8ZmmekKIp10mqrUrucVMsa+Oa/l1yKPXD0eUFFU1V4yeqKI5GfWCPEKp -Tm71O8Mu243AsFzzWTjn7c9p8FoLG77AlCQlh/o3cbMT5xys4Zvv2+Q7RVJFlqnB -U840yFLuta7tj95gcOKlVKu2bQ6XpUA0ayvTvGbrZjR8+muLj1cpmfgwF126cm/7 -gcWt0oZYPRfH5wm78Sv3htzB2nFd1EbjzK0lwYi8YGd1ZrPxGPeiXOZT/zqItkel -/xMY6pgJdz+dU/nPAeX1pnAXFK9jpP+Zs5Od3FOnBv5IhR2haa4ldbsTzFID9e1R -oYvbFQIDAQABo4IBaDCCAWQwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8E -BAMCAYYwSwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5p -ZGVudHJ1c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTE -p7Gkeyxx+tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEE -AYLfEwEBATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2Vu -Y3J5cHQub3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0 -LmNvbS9EU1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYf -r52LFMLGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0B -AQsFAAOCAQEA2UzgyfWEiDcx27sT4rP8i2tiEmxYt0l+PAK3qB8oYevO4C5z70kH -ejWEHx2taPDY/laBL21/WKZuNTYQHHPD5b1tXgHXbnL7KqC401dk5VvCadTQsvd8 -S8MXjohyc9z9/G2948kLjmE6Flh9dDYrVYA9x2O+hEPGOaEOa1eePynBgPayvUfL -qjBstzLhWVQLGAkXXmNs+5ZnPBxzDJOLxhF2JIbeQAcH5H0tZrUlo5ZYyOqA7s9p -O5b85o3AM/OJ+CktFBQtfvBhcJVd9wvlwPsk+uyOy2HI7mNxKKgsBTt375teA2Tw -UdHkhVNcsAKX1H7GNNLOEADksd86wuoXvg== ------END CERTIFICATE----- -` - t.Run("bundle", func(t *testing.T) { - hash := CalculatePEMCertChainSHA256Hash([]byte(CertBundle)) - assert.Equal(t, "WF65fBkgltadMnXryOMZ6TEYeV4d5Q0uu4SGXGZ0RjI=", hash) - }) const Single = `-----BEGIN CERTIFICATE----- -MIIFJjCCBA6gAwIBAgISBL8FgUdEcVYEjdMkTZPgC3ocMA0GCSqGSIb3DQEBCwUA -MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD -EwJSMzAeFw0yMTAzMjkwMTM2MzlaFw0yMTA2MjcwMTM2MzlaMBsxGTAXBgNVBAMT -EHNlY3VyZS5ra2Rldi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQDOF/j+s7rHaDMXdhYjffoOFjNZb7n3sCuvubI3qOcgJmr1WPlCEry50KoY8FaB -IF2HstMIZceN4NoUK7mr3WAvsQTA47uBfuhp+XQmAQW0T/fYD+XbvxtCENEin+xm -JsvTKZLTKbE08E964J4H+1sBmueP6rvy2Wt95z0XkqoQiikpmLE87WdltQcATvVX -qqrL64hV0nN4Hdi2Bv1cQ92aR7lZGj8jiQRtTj8y5Ah3Gk3fPoao+yI7gnzembqo -fddePzz/u8iEuvYAsIYZKn9bbS7rkYoJazL2/xiDZR7usn0SomzmM6lGXDD3FF4b -lyTkLYwgFVgbGWoz1+eOHD5BAgMBAAGjggJLMIICRzAOBgNVHQ8BAf8EBAMCBaAw -HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD -VR0OBBYEFOPRdApL8XENLXDuU3oPisykGyp+MB8GA1UdIwQYMBaAFBQusxe3WFbL -rlAJQOYfr52LFMLGMFUGCCsGAQUFBwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDov -L3IzLm8ubGVuY3Iub3JnMCIGCCsGAQUFBzAChhZodHRwOi8vcjMuaS5sZW5jci5v -cmcvMBsGA1UdEQQUMBKCEHNlY3VyZS5ra2Rldi5vcmcwTAYDVR0gBEUwQzAIBgZn -gQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5s -ZXRzZW5jcnlwdC5vcmcwggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdQD2XJQv0Xcw -IhRUGAgwlFaO400TGTO/3wwvIAvMTvFk4wAAAXh71yBGAAAEAwBGMEQCIDmziDOn -ehPY2KoAFX8fPWiCm4EBTbGJXBWF1LCotPJBAiBLSCg+krXvbyoABnTm8knv0hbG -/ZOk8LV6qpw9VoQwGwB3AG9Tdqwx8DEZ2JkApFEV/3cVHBHZAsEAKQaNsgiaN9kT -AAABeHvXIIAAAAQDAEgwRgIhAOkeKc52wR3n5QWZfa3zbbicMMSQrTGbQ+1fHNs7 -SsRvAiEAqbflDx1nZRsTA22FfNYfgF6v5Z3/svjiTleWSQad4WswDQYJKoZIhvcN -AQELBQADggEBAEj8tg+Agf5sNBM9CvjeXbA0fkpGDaQzXEkwefAea5vPgKoGiWSN -pHDkyr0i7+mqa7cMXhmmo20V0/+QDv0nrsCw8pgJuviC3GvK6agT6WfkXM2djJuo -osPeXOL9KEF/ATv0EyM5tr9TIoRSSYQoRhuqEdg3Dz9Ii8GXR5HhbYr6Cd7gwNHS -kYeivKDmgv31GHb4twPSE9LZ/U+56lNVvSbJ4csupIF3GnxnxrFSmijYNOPoM6mj -tzY45d4mjPs0fKCFKSsVM6YT0tX4NwIKsOaeQg30WLtRyDwYm6ma/a/UUUS0FloZ -2/p85glOgzownfoRjzTbqHu8ewtMd6Apc0E= +MIINWzCCC0OgAwIBAgITMwK6ajqdrV0tahuIrQAAArpqOjANBgkqhkiG9w0BAQwF +ADBdMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MS4wLAYDVQQDEyVNaWNyb3NvZnQgQXp1cmUgUlNBIFRMUyBJc3N1aW5nIENBIDA0 +MB4XDTI1MDkwOTEwMzE1NloXDTI2MDMwODEwMzE1NlowYzELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAldBMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv +ZnQgQ29ycG9yYXRpb24xFTATBgNVBAMTDHd3dy5iaW5nLmNvbTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMBflymLifrVkjp8K4/XrHSt+/xDrrZIJyTI +JOhIGZJZ88sNjo4OChQWV8O3CTQwrbKJDd6KjZFFc6BPKpEJZ891w2zkymMbE7wh +vQVviSCIVCO+49pLrEvfh5ZvdbXhtNzm/ZRvkoI8h4ZKPBRNmX5sGpSQ9p0loJBj +Jk1HbzLv0vRk5bLb/J6x7YexaAu86C9TjqnC4irO+AZZNI/0S70ZHxX+ETZVV0EX +QU8UmqV68e4YhAQwiLYdAQw125n2hGWoLokQSZTyEiIIoubB00pE5zf0Qaq6Q4s8 +Go5Ukw1A4HjWMisHVKq369pgI8VDZtMzOhS+O0DEQZLwOFETZxECAwEAAaOCCQww +ggkIMIIBgAYKKwYBBAHWeQIEAgSCAXAEggFsAWoAdgCWl2S/VViXrfdDh2g3CEJ3 +6fA61fak8zZuRqQ/D8qpxgAAAZkuEXLdAAAEAwBHMEUCIBLzX4AJgVJdQshSMBLS +hBMQX8zgRm2U3IXjLk37JM3QAiEAkVrmCFx0+BM3NOoCAXBU1WzVuniPxJP3Ysbd +OO3dkEAAdwBkEcRspBLsp4kcogIuALyrTygH1B41J6vq/tUDyX3N8AAAAZkuEXKd +AAAEAwBIMEYCIQCCO1ys+tlI8Fhp4J/Dqk3VVtSi408Nuw8T6YciDL6LPgIhAPjp +fm/gMkASgNimNuMFH8oiJbqeQ/yo2zQfub894iMuAHcAVmzVo3a+g9/jQrZ1xJwj +JJinabrDgsurSaOHfZqzLQEAAAGZLhFy2QAABAMASDBGAiEA/93O6XiiYhfeANHh +0n2nJyVvFAc6sBNT2S7WOR28vR0CIQC7i+leDRRIeY2BYJwaRlAqHlSyU4DZu5IG +caxiWFeavzAnBgkrBgEEAYI3FQoEGjAYMAoGCCsGAQUFBwMCMAoGCCsGAQUFBwMB +MDwGCSsGAQQBgjcVBwQvMC0GJSsGAQQBgjcVCIe91xuB5+tGgoGdLo7QDIfw2h1d +gqvnMIft8R8CAWQCAS0wgbQGCCsGAQUFBwEBBIGnMIGkMHMGCCsGAQUFBzAChmdo +dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUy +MEF6dXJlJTIwUlNBJTIwVExTJTIwSXNzdWluZyUyMENBJTIwMDQlMjAtJTIweHNp +Z24uY3J0MC0GCCsGAQUFBzABhiFodHRwOi8vb25lb2NzcC5taWNyb3NvZnQuY29t +L29jc3AwHQYDVR0OBBYEFAsWImxddBew8yEv3yGDsmy90FzPMA4GA1UdDwEB/wQE +AwIFoDCCBREGA1UdEQSCBQgwggUEghMqLnBsYXRmb3JtLmJpbmcuY29tggoqLmJp +bmcuY29tgghiaW5nLmNvbYIWaWVvbmxpbmUubWljcm9zb2Z0LmNvbYITKi53aW5k +b3dzc2VhcmNoLmNvbYIZY24uaWVvbmxpbmUubWljcm9zb2Z0LmNvbYIRKi5vcmln +aW4uYmluZy5jb22CDSoubW0uYmluZy5uZXSCDiouYXBpLmJpbmcuY29tgg0qLmNu +LmJpbmcubmV0gg0qLmNuLmJpbmcuY29tghBzc2wtYXBpLmJpbmcuY29tghBzc2wt +YXBpLmJpbmcubmV0gg4qLmFwaS5iaW5nLm5ldIIOKi5iaW5nYXBpcy5jb22CD2Jp +bmdzYW5kYm94LmNvbYIWZmVlZGJhY2subWljcm9zb2Z0LmNvbYIbaW5zZXJ0bWVk +aWEuYmluZy5vZmZpY2UubmV0gg5yLmJhdC5iaW5nLmNvbYIQKi5yLmJhdC5iaW5n +LmNvbYIPKi5kaWN0LmJpbmcuY29tgg4qLnNzbC5iaW5nLmNvbYIQKi5hcHBleC5i +aW5nLmNvbYIWKi5wbGF0Zm9ybS5jbi5iaW5nLmNvbYINd3AubS5iaW5nLmNvbYIM +Ki5tLmJpbmcuY29tgg9nbG9iYWwuYmluZy5jb22CEXdpbmRvd3NzZWFyY2guY29t +gg5zZWFyY2gubXNuLmNvbYIRKi5iaW5nc2FuZGJveC5jb22CGSouYXBpLnRpbGVz +LmRpdHUubGl2ZS5jb22CGCoudDAudGlsZXMuZGl0dS5saXZlLmNvbYIYKi50MS50 +aWxlcy5kaXR1LmxpdmUuY29tghgqLnQyLnRpbGVzLmRpdHUubGl2ZS5jb22CGCou +dDMudGlsZXMuZGl0dS5saXZlLmNvbYILM2QubGl2ZS5jb22CE2FwaS5zZWFyY2gu +bGl2ZS5jb22CFGJldGEuc2VhcmNoLmxpdmUuY29tghVjbndlYi5zZWFyY2gubGl2 +ZS5jb22CDWRpdHUubGl2ZS5jb22CEWZhcmVjYXN0LmxpdmUuY29tgg5pbWFnZS5s +aXZlLmNvbYIPaW1hZ2VzLmxpdmUuY29tghFsb2NhbC5saXZlLmNvbS5hdYIUbG9j +YWxzZWFyY2gubGl2ZS5jb22CFGxzNGQuc2VhcmNoLmxpdmUuY29tgg1tYWlsLmxp +dmUuY29tghFtYXBpbmRpYS5saXZlLmNvbYIObG9jYWwubGl2ZS5jb22CDW1hcHMu +bGl2ZS5jb22CEG1hcHMubGl2ZS5jb20uYXWCD21pbmRpYS5saXZlLmNvbYINbmV3 +cy5saXZlLmNvbYIcb3JpZ2luLmNud2ViLnNlYXJjaC5saXZlLmNvbYIWcHJldmll +dy5sb2NhbC5saXZlLmNvbYIPc2VhcmNoLmxpdmUuY29tghJ0ZXN0Lm1hcHMubGl2 +ZS5jb22CDnZpZGVvLmxpdmUuY29tgg92aWRlb3MubGl2ZS5jb22CFXZpcnR1YWxl +YXJ0aC5saXZlLmNvbYIMd2FwLmxpdmUuY29tghJ3ZWJtYXN0ZXIubGl2ZS5jb22C +FXd3dy5sb2NhbC5saXZlLmNvbS5hdYIUd3d3Lm1hcHMubGl2ZS5jb20uYXWCE3dl +Ym1hc3RlcnMubGl2ZS5jb22CGGVjbi5kZXYudmlydHVhbGVhcnRoLm5ldIIMd3d3 +LmJpbmcuY29tMAwGA1UdEwEB/wQCMAAwagYDVR0fBGMwYTBfoF2gW4ZZaHR0cDov +L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwQXp1cmUl +MjBSU0ElMjBUTFMlMjBJc3N1aW5nJTIwQ0ElMjAwNC5jcmwwZgYDVR0gBF8wXTBR +BgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3Nv +ZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMAgGBmeBDAECAjAfBgNV +HSMEGDAWgBQ7cNFT6XYlnWCoymYPxpuub1QWajAdBgNVHSUEFjAUBggrBgEFBQcD +AgYIKwYBBQUHAwEwDQYJKoZIhvcNAQEMBQADggIBAEQCoppNllgoHtfLJt2m7cVL +AILYFxJdi9qc4LUBfaQEdUwAfsC1pSk5YFB0aGcmVFKMvMMOeENOrWgNJVTLYI05 +8mu6XmbiqUeIu1Rlye/yNirYm33Js2f3VXYp6HSzisF5cWq4QwYqA6XIMfDl61/y +IXVb5l5eTfproM2grn3RcVVbk5DuEUfyDPzYYNm8elxzac4RrbkDif/b+tVFxmrJ +CUx1o3VLiVVzbIFCDc5r6pPArm1EdgseJ7pRdXzg6flwA0INRpeLCpjtvkHeZCh7 +GS2JUBhFv7M+lneJljNU/trTkYiho+ZRW9AgLcN73c4+1wHttPHk+w19m5Ge182V +HzCQdO27IGovKN8jkprGafGxYhyCn4KdSYbRrG7fjkckzpJrjCpF2/bJJ+o4Zi9P +rJIKHzY5lIMXcD7wwwT2WwlKXoTDrgm4QKN18V+kZaoOILdKyMlEww4jPFUqk6j1 +0Qeod55F5h4tCq2lmwDIa/jyWTGgqTr4UESqj46NB5+JkGYl0O1PPbS1nUm9sN1l +hkY45iskXVXqLl6AVVcXyxMTefD43M81tFVuJJgpdD/BaMaXAuBdNDfTQcJwhP99 +uI6HqHFD3iEct8fBkYfQiwH2e1eu9OwgujiWHsutyK8VvzVB3/YnhQ/TzciRjPqz +7ykUutQNUALq8dQwoTnK -----END CERTIFICATE----- -` - t.Run("single", func(t *testing.T) { - hash := CalculatePEMCertChainSHA256Hash([]byte(Single)) - assert.Equal(t, "FW3SVMCL6um2wVltOdgJ3DpI82aredw83YoCblkMkVM=", hash) - }) -} -func TestCalculateCertPublicKeyHash(t *testing.T) { - const Single = `-----BEGIN CERTIFICATE----- -MIINWTCCC0GgAwIBAgITLQAxbA/A+lw/1sLDAAAAADFsDzANBgkqhkiG9w0BAQsF -ADBPMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u -MSAwHgYDVQQDExdNaWNyb3NvZnQgUlNBIFRMUyBDQSAwMjAeFw0yMjExMjUwMDU2 -NTZaFw0yMzA1MjUwMDU2NTZaMBcxFTATBgNVBAMTDHd3dy5iaW5nLmNvbTCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOH89lKmtkDnClFiQwfZofZO4h8C -Ye/+ChI67pEw5Q6/MxJzHiMKe8f1WaNuc+wkdHdct+BmQ+AftozIJt+eSN6IF7eY -dsutBvR87GNLFe40MBvfyvTQVM9Ulv04JxOpKTYnsf2wmktEI3y7FCgfm9RT71n+ -Zef8Z8fa4By7aGfbbCQ0DsHl5P9o3ug/eLQODzK9NuQlwcVBHD2Zvgo+K7WOsjgE -k8JnOr+2zc0WWT4OrWSDJE/3l+jvhxmZkrwgmks4m9zUZvAnYAz/xxVCJRqbI3Ou -S5fkJJ3f6IxPbS2i8OWz6tma1aIkgQaFNJQuYOJa1esfQcEzs6kb/Xx5DXUCAwEA -AaOCCWQwgglgMIIBfQYKKwYBBAHWeQIEAgSCAW0EggFpAWcAdgCt9776fP8QyIud -PZwePhhqtGcpXc+xDCTKhYY069yCigAAAYSsUtxtAAAEAwBHMEUCIQCP/Jpp337p -cKITqS/kNlA4bNY6TK1Ad0VlsdkzQU+oZgIgFZb2AcsyT1UKCmM3ziGsLdvS9MAT -D1g/kztyDXhkA70AdgBVgdTCFpA2AUrqC5tXPFPwwOQ4eHAlCBcvo6odBxPTDAAA -AYSsUtsZAAAEAwBHMEUCIQDvlqXrdA440PW6b+JLj4F0ZVQNKHcv1lub0FhQqHgR -wAIgAtC7eXvXXhVBuO+Bd3fkDI0aGQM+pcvIesBoygzStjQAdQB6MoxU2LcttiDq -OOBSHumEFnAyE4VNO9IrwTpXo1LrUgAAAYSsUtmfAAAEAwBGMEQCIDgjSYt6e/h8 -dv2KGEL3AJZUBH2gp1AA5saH8o3OyMJhAiBOCzo3oWlVFeF/8c0fxIIs9Fj4w8BY -INo0jNP/k7apgTAnBgkrBgEEAYI3FQoEGjAYMAoGCCsGAQUFBwMBMAoGCCsGAQUF -BwMCMD4GCSsGAQQBgjcVBwQxMC8GJysGAQQBgjcVCIfahnWD7tkBgsmFG4G1nmGF -9OtggV2Fho5Bh8KYUAIBZAIBJzCBhwYIKwYBBQUHAQEEezB5MFMGCCsGAQUFBzAC -hkdodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL21zY29ycC9NaWNyb3NvZnQl -MjBSU0ElMjBUTFMlMjBDQSUyMDAyLmNydDAiBggrBgEFBQcwAYYWaHR0cDovL29j -c3AubXNvY3NwLmNvbTAdBgNVHQ4EFgQUpuSPPchFlPGu8FTbzPhJTFxQ7RowDgYD -VR0PAQH/BAQDAgSwMIIFbQYDVR0RBIIFZDCCBWCCDHd3dy5iaW5nLmNvbYIQZGlj -dC5iaW5nLmNvbS5jboITKi5wbGF0Zm9ybS5iaW5nLmNvbYIKKi5iaW5nLmNvbYII -YmluZy5jb22CFmllb25saW5lLm1pY3Jvc29mdC5jb22CEyoud2luZG93c3NlYXJj -aC5jb22CGWNuLmllb25saW5lLm1pY3Jvc29mdC5jb22CESoub3JpZ2luLmJpbmcu -Y29tgg0qLm1tLmJpbmcubmV0gg4qLmFwaS5iaW5nLmNvbYIYZWNuLmRldi52aXJ0 -dWFsZWFydGgubmV0gg0qLmNuLmJpbmcubmV0gg0qLmNuLmJpbmcuY29tghBzc2wt -YXBpLmJpbmcuY29tghBzc2wtYXBpLmJpbmcubmV0gg4qLmFwaS5iaW5nLm5ldIIO -Ki5iaW5nYXBpcy5jb22CD2JpbmdzYW5kYm94LmNvbYIWZmVlZGJhY2subWljcm9z -b2Z0LmNvbYIbaW5zZXJ0bWVkaWEuYmluZy5vZmZpY2UubmV0gg5yLmJhdC5iaW5n -LmNvbYIQKi5yLmJhdC5iaW5nLmNvbYISKi5kaWN0LmJpbmcuY29tLmNugg8qLmRp -Y3QuYmluZy5jb22CDiouc3NsLmJpbmcuY29tghAqLmFwcGV4LmJpbmcuY29tghYq -LnBsYXRmb3JtLmNuLmJpbmcuY29tgg13cC5tLmJpbmcuY29tggwqLm0uYmluZy5j -b22CD2dsb2JhbC5iaW5nLmNvbYIRd2luZG93c3NlYXJjaC5jb22CDnNlYXJjaC5t -c24uY29tghEqLmJpbmdzYW5kYm94LmNvbYIZKi5hcGkudGlsZXMuZGl0dS5saXZl -LmNvbYIPKi5kaXR1LmxpdmUuY29tghgqLnQwLnRpbGVzLmRpdHUubGl2ZS5jb22C -GCoudDEudGlsZXMuZGl0dS5saXZlLmNvbYIYKi50Mi50aWxlcy5kaXR1LmxpdmUu -Y29tghgqLnQzLnRpbGVzLmRpdHUubGl2ZS5jb22CFSoudGlsZXMuZGl0dS5saXZl -LmNvbYILM2QubGl2ZS5jb22CE2FwaS5zZWFyY2gubGl2ZS5jb22CFGJldGEuc2Vh -cmNoLmxpdmUuY29tghVjbndlYi5zZWFyY2gubGl2ZS5jb22CDGRldi5saXZlLmNv -bYINZGl0dS5saXZlLmNvbYIRZmFyZWNhc3QubGl2ZS5jb22CDmltYWdlLmxpdmUu -Y29tgg9pbWFnZXMubGl2ZS5jb22CEWxvY2FsLmxpdmUuY29tLmF1ghRsb2NhbHNl -YXJjaC5saXZlLmNvbYIUbHM0ZC5zZWFyY2gubGl2ZS5jb22CDW1haWwubGl2ZS5j -b22CEW1hcGluZGlhLmxpdmUuY29tgg5sb2NhbC5saXZlLmNvbYINbWFwcy5saXZl -LmNvbYIQbWFwcy5saXZlLmNvbS5hdYIPbWluZGlhLmxpdmUuY29tgg1uZXdzLmxp -dmUuY29tghxvcmlnaW4uY253ZWIuc2VhcmNoLmxpdmUuY29tghZwcmV2aWV3Lmxv -Y2FsLmxpdmUuY29tgg9zZWFyY2gubGl2ZS5jb22CEnRlc3QubWFwcy5saXZlLmNv -bYIOdmlkZW8ubGl2ZS5jb22CD3ZpZGVvcy5saXZlLmNvbYIVdmlydHVhbGVhcnRo -LmxpdmUuY29tggx3YXAubGl2ZS5jb22CEndlYm1hc3Rlci5saXZlLmNvbYITd2Vi -bWFzdGVycy5saXZlLmNvbYIVd3d3LmxvY2FsLmxpdmUuY29tLmF1ghR3d3cubWFw -cy5saXZlLmNvbS5hdTCBsAYDVR0fBIGoMIGlMIGioIGfoIGchk1odHRwOi8vbXNj -cmwubWljcm9zb2Z0LmNvbS9wa2kvbXNjb3JwL2NybC9NaWNyb3NvZnQlMjBSU0El -MjBUTFMlMjBDQSUyMDAyLmNybIZLaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3Br -aS9tc2NvcnAvY3JsL01pY3Jvc29mdCUyMFJTQSUyMFRMUyUyMENBJTIwMDIuY3Js -MFcGA1UdIARQME4wQgYJKwYBBAGCNyoBMDUwMwYIKwYBBQUHAgEWJ2h0dHA6Ly93 -d3cubWljcm9zb2Z0LmNvbS9wa2kvbXNjb3JwL2NwczAIBgZngQwBAgEwHwYDVR0j -BBgwFoAU/y9/4Qb0OPMt7SWNmML+DvZs/PowHQYDVR0lBBYwFAYIKwYBBQUHAwEG -CCsGAQUFBwMCMA0GCSqGSIb3DQEBCwUAA4ICAQB4OIB/EHxpF64iFZME7XkJjZYn -ZiYIfOfHs6EGDNn7fxvpZS9HVy1jOWv/RvzEbMuSV3b/fItaJN/zATBg5/6hb5Jq -HGIcnKmb+tYrKlYhSOngHSu/8/OYP1dFFIqcVe0769kwXaKUzLh6UVRaS+mB7GFc -sXmPMbv5NM7mCUEdMkOaoSmubfw/WzmmRGrcSmtCxtIwMcp8Jf13Esunq//4+9w3 -M/JXa8ubmXyrY63zt/Oz/NkVJvja89ueovscy6s5sw2r+Su4bRsJjmxwCbakp56K -rbh7z417LzW88MMuATvOyk/O8Rbw2KYVSEiQgO54kHI0YkHkJ/6IoeAT1pmCfHUE -Rd+Ec8T+/lE2BPLVqp8SjogDYiybb0IR5Gn2vYyUdzsS2h/C5qGNd2t5ehxfjQoL -G6Y3GJZQRxkSX6TLPYU0U63wWb4yeSxabpBlARaZMaAoqDa3cX53WCnrAXDz8vuH -yAtX2/Jq7IpybFK5kFzbxfI02Ik0aCWJUnXPL8L6esTskwvkzX8rSI/bjPrzcJL5 -B9pONLy6wc8/Arfu2eNlMbs8s/g8c5zkEc3fBZ9tJ1dqlnMAVgB2+fwI3aK4F34N -uyfZW7Xu65KkPhbMnO0GVGM7X4Lkyjm4ysQ9PIRV3MwMfXH+RBSXlIayLTcYG4gl -XF1a/qnao6nMjyTIyQ== ------END CERTIFICATE----- ` t.Run("singlepublickey", func(t *testing.T) { block, _ := pem.Decode([]byte(Single)) cert, err := x509.ParseCertificate(block.Bytes) assert.Equal(t, err, nil) - hash := GenerateCertPublicKeyHash(cert) - hashstr := base64.StdEncoding.EncodeToString(hash) - assert.Equal(t, "xI/4mNm8xF9uDT4vA9G1+aKAaybwNlkRECnN8vGAHTM=", hashstr) + hash := GenerateCertHash(cert) + fingerprint, _ := hex.DecodeString("ae243d668ec9c7f74a0dcd1ad21c6676b4efe30c39728934b362093af886bf77") + assert.Equal(t, fingerprint, hash) }) } From 07a0dafa413d8656e6e3b5c9732b62952f840665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Fri, 9 Jan 2026 22:22:07 +0800 Subject: [PATCH 087/136] DNS: Check err for UDP dns.PackMessage(req.msg) (#5512) Fixes https://github.com/XTLS/Xray-core/issues/5506 --- app/dns/nameserver_udp.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/dns/nameserver_udp.go b/app/dns/nameserver_udp.go index f263a0f8..55b825f7 100644 --- a/app/dns/nameserver_udp.go +++ b/app/dns/nameserver_udp.go @@ -160,7 +160,7 @@ func (s *ClassicNameServer) getCacheController() *CacheController { } // sendQuery implements CachedNameserver. -func (s *ClassicNameServer) sendQuery(ctx context.Context, _ chan<- error, fqdn string, option dns_feature.IPOption) { +func (s *ClassicNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn) reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) @@ -171,7 +171,14 @@ func (s *ClassicNameServer) sendQuery(ctx context.Context, _ chan<- error, fqdn ctx: ctx, } s.addPendingRequest(udpReq) - b, _ := dns.PackMessage(req.msg) + b, err := dns.PackMessage(req.msg) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to pack dns query") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } copyDest := net.UDPDestination(s.address.Address, s.address.Port) b.UDP = ©Dest s.udpServer.Dispatch(toDnsContext(ctx, s.address.String()), *s.address, b) From 14e171ac8e87d3151c0533db5016a5b27009a606 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:26:45 +0000 Subject: [PATCH 088/136] TUN inbound: Implement UDP FullCone NAT (#5509) https://github.com/XTLS/Xray-core/pull/5509#issuecomment-3732898130 --------- Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> Co-authored-by: Fangliding <45535409+Fangliding@users.noreply.github.com> Co-authored-by: Owersun <4807375+Owersun@users.noreply.github.com> --- common/xudp/xudp.go | 2 +- go.mod | 2 +- proxy/tun/stack_gvisor.go | 52 ++++----- proxy/tun/udp_fullcone.go | 216 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 246 insertions(+), 26 deletions(-) create mode 100644 proxy/tun/udp_fullcone.go diff --git a/common/xudp/xudp.go b/common/xudp/xudp.go index 1dfff16a..e5da49f4 100644 --- a/common/xudp/xudp.go +++ b/common/xudp/xudp.go @@ -52,7 +52,7 @@ func GetGlobalID(ctx context.Context) (globalID [8]byte) { return } if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP && - (inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks") { + (inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun") { h := blake3.New(8, BaseKey) h.Write([]byte(inbound.Source.String())) copy(globalID[:], h.Sum(nil)) diff --git a/go.mod b/go.mod index f0797bc0..a39162cc 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( golang.org/x/net v0.48.0 golang.org/x/sync v0.19.0 golang.org/x/sys v0.39.0 + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 google.golang.org/grpc v1.78.0 google.golang.org/protobuf v1.36.11 @@ -50,7 +51,6 @@ require ( golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.39.0 // indirect - golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/proxy/tun/stack_gvisor.go b/proxy/tun/stack_gvisor.go index 81f30784..d062c3d0 100644 --- a/proxy/tun/stack_gvisor.go +++ b/proxy/tun/stack_gvisor.go @@ -6,6 +6,7 @@ import ( "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" + "gvisor.dev/gvisor/pkg/buffer" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" "gvisor.dev/gvisor/pkg/tcpip/header" @@ -100,32 +101,35 @@ func (t *stackGVisor) Start() error { }) ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) - udpForwarder := udp.NewForwarder(ipStack, func(r *udp.ForwarderRequest) { - go func(r *udp.ForwarderRequest) { - var wq waiter.Queue - var id = r.ID() + // Use custom UDP packet handler, instead of strict gVisor forwarder, for FullCone NAT support + udpForwarder := newUdpConnectionHandler(t.ctx, t.handler, func(p []byte) { + // extract network protocol from the packet + var networkProtocol tcpip.NetworkProtocolNumber + switch header.IPVersion(p) { + case header.IPv4Version: + networkProtocol = header.IPv4ProtocolNumber + case header.IPv6Version: + networkProtocol = header.IPv6ProtocolNumber + default: + // discard packet with unknown network version + return + } - ep, err := r.CreateEndpoint(&wq) - if err != nil { - errors.LogError(t.ctx, err.String()) - return - } - - options := ep.SocketOptions() - options.SetReuseAddress(true) - options.SetReusePort(true) - - t.handler.HandleConnection( - gonet.NewUDPConn(&wq, ep), - // local address on the gVisor side is connection destination - net.UDPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)), - ) - - // close the socket - ep.Close() - }(r) + ipStack.WriteRawPacket(defaultNIC, networkProtocol, buffer.MakeWithData(p)) + }) + ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { + data := pkt.Data().AsRange().ToSlice() + if len(data) == 0 { + return false + } + // source/destination of the packet we process as incoming, on gVisor side are Remote/Local + // in other terms, src is the side behind tun, dst is the side behind gVisor + // this function handle packets passing from the tun to the gVisor, therefore the src/dst assignement + src := net.UDPDestination(net.IPAddress(id.RemoteAddress.AsSlice()), net.Port(id.RemotePort)) + dst := net.UDPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)) + + return udpForwarder.HandlePacket(src, dst, data) }) - ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) t.stack = ipStack t.endpoint = linkEndpoint diff --git a/proxy/tun/udp_fullcone.go b/proxy/tun/udp_fullcone.go new file mode 100644 index 00000000..bc50a7f3 --- /dev/null +++ b/proxy/tun/udp_fullcone.go @@ -0,0 +1,216 @@ +package tun + +import ( + "context" + "sync" + "sync/atomic" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + c "github.com/xtls/xray-core/common/ctx" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/pipe" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/checksum" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +// udp connection abstraction +type udpConn struct { + lastActive atomic.Int64 + reader buf.Reader + writer buf.Writer + done *done.Instance + cancel context.CancelFunc +} + +// sub-handler specifically for udp connections under main handler +type udpConnectionHandler struct { + sync.Mutex + ctx context.Context + handler *Handler + udpConns map[net.Destination]*udpConn + udpChecker *task.Periodic + writePacket func(p []byte) +} + +func newUdpConnectionHandler(ctx context.Context, h *Handler, writePacket func(p []byte)) *udpConnectionHandler { + handler := &udpConnectionHandler{ + ctx: ctx, + handler: h, + udpConns: make(map[net.Destination]*udpConn), + writePacket: writePacket, + } + + handler.udpChecker = &task.Periodic{Interval: time.Minute, Execute: handler.cleanupUDP} + handler.udpChecker.Start() + + return handler +} + +func (u *udpConnectionHandler) cleanupUDP() error { + u.Lock() + defer u.Unlock() + if len(u.udpConns) == 0 { + return errors.New("no connections") + } + now := time.Now().Unix() + for src, conn := range u.udpConns { + if now-conn.lastActive.Load() > 300 { + conn.cancel() + common.Must(conn.done.Close()) + common.Must(common.Close(conn.writer)) + delete(u.udpConns, src) + } + } + return nil +} + +// HandlePacket handles UDP packets coming from tun, to forward to the dispatcher +// this custom handler support FullCone NAT of returning packets, binding connection only by the source port +func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destination, data []byte) bool { + u.Lock() + conn, found := u.udpConns[src] + if !found { + reader, writer := pipe.New(pipe.DiscardOverflow(), pipe.WithSizeLimit(16*1024)) + conn = &udpConn{reader: reader, writer: writer, done: done.New()} + u.udpConns[src] = conn + u.Unlock() + + go func() { + ctx, cancel := context.WithCancel(u.ctx) + conn.cancel = cancel + defer func() { + cancel() + u.Lock() + delete(u.udpConns, src) + u.Unlock() + common.Must(conn.done.Close()) + common.Must(common.Close(conn.writer)) + }() + + inbound := &session.Inbound{ + Name: "tun", + Source: src, + CanSpliceCopy: 1, + User: &protocol.MemoryUser{Level: u.handler.config.UserLevel}, + } + ctx = session.ContextWithInbound(c.ContextWithID(ctx, session.NewID()), inbound) + ctx = session.SubContextFromMuxInbound(ctx) + link := &transport.Link{ + Reader: &buf.TimeoutWrapperReader{Reader: conn.reader}, + // reverse source and destination, indicating the packets to write are going in the other + // direction (written back to tun) and should have reversed addressing + Writer: &udpWriter{handler: u, src: dst, dst: src}, + } + _ = u.handler.dispatcher.DispatchLink(ctx, dst, link) + }() + } else { + conn.lastActive.Store(time.Now().Unix()) + u.Unlock() + } + + b := buf.New() + b.Write(data) + b.UDP = &dst + conn.writer.WriteMultiBuffer(buf.MultiBuffer{b}) + + return true +} + +type udpWriter struct { + handler *udpConnectionHandler + // address in the side of stack, where packet will be coming from + src net.Destination + // address on the side of tun, where packet will be destined to + dst net.Destination +} + +func (w *udpWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + for _, b := range mb { + // use captured in the dispatched packet source address b.UDP as source, if available, + // otherwise use captured in the writer source w.src + srcAddr := w.src + if b.UDP != nil { + srcAddr = *b.UDP + } + + // validate address family matches + if srcAddr.Address.Family() != w.src.Address.Family() { + errors.LogWarning(context.Background(), "UDP return packet address family mismatch: expected ", w.src.Address.Family(), ", got ", srcAddr.Address.Family()) + b.Release() + continue + } + + payload := b.Bytes() + udpLen := header.UDPMinimumSize + len(payload) + srcIP := tcpip.AddrFromSlice(srcAddr.Address.IP()) + dstIP := tcpip.AddrFromSlice(w.dst.Address.IP()) + + // build packet with appropriate IP header size + isIPv4 := srcAddr.Address.Family().IsIPv4() + ipHdrSize := header.IPv6MinimumSize + if isIPv4 { + ipHdrSize = header.IPv4MinimumSize + } + + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + ReserveHeaderBytes: ipHdrSize + header.UDPMinimumSize, + Payload: buffer.MakeWithData(payload), + }) + + // Build UDP header + udpHdr := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize)) + udpHdr.Encode(&header.UDPFields{ + SrcPort: uint16(srcAddr.Port), + DstPort: uint16(w.dst.Port), + Length: uint16(udpLen), + }) + + // Calculate and set UDP checksum + xsum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, srcIP, dstIP, uint16(udpLen)) + udpHdr.SetChecksum(^udpHdr.CalculateChecksum(checksum.Checksum(payload, xsum))) + + // Build IP header + if isIPv4 { + ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize)) + ipHdr.Encode(&header.IPv4Fields{ + TotalLength: uint16(header.IPv4MinimumSize + udpLen), + TTL: 64, + Protocol: uint8(header.UDPProtocolNumber), + SrcAddr: srcIP, + DstAddr: dstIP, + }) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + } else { + ipHdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize)) + ipHdr.Encode(&header.IPv6Fields{ + PayloadLength: uint16(udpLen), + TransportProtocol: header.UDPProtocolNumber, + HopLimit: 64, + SrcAddr: srcIP, + DstAddr: dstIP, + }) + } + + // Write raw packet to network stack + views := pkt.AsSlices() + var data []byte + for _, view := range views { + data = append(data, view...) + } + w.handler.writePacket(data) + pkt.DecRef() + b.Release() + } + return nil +} From 0c09f4342b5cf3b351f6c700f38bd002b2f968f6 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:01:27 +0000 Subject: [PATCH 089/136] TUN inbound: Fix log, CanSpliceCopy, tag, sniffing, and port config issues (#5522) Fixes https://github.com/XTLS/Xray-core/pull/5509#issuecomment-3732488294 & https://github.com/XTLS/Xray-core/pull/5509#issuecomment-3732740897 --------- Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- app/proxyman/inbound/always.go | 15 ++++++++++ infra/conf/xray.go | 5 +++- proxy/tun/handler.go | 50 ++++++++++++++++++++++++---------- proxy/tun/udp_fullcone.go | 14 +++++++++- 4 files changed, 67 insertions(+), 17 deletions(-) diff --git a/app/proxyman/inbound/always.go b/app/proxyman/inbound/always.go index 6fb3ba43..a1cb7b7c 100644 --- a/app/proxyman/inbound/always.go +++ b/app/proxyman/inbound/always.go @@ -9,6 +9,7 @@ import ( "github.com/xtls/xray-core/common/mux" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/features/policy" "github.com/xtls/xray-core/features/stats" @@ -52,6 +53,20 @@ type AlwaysOnInboundHandler struct { } func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *proxyman.ReceiverConfig, proxyConfig interface{}) (*AlwaysOnInboundHandler, error) { + // Set tag and sniffing config in context before creating proxy + // This allows proxies like TUN to access these settings + ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: tag}) + if receiverConfig.SniffingSettings != nil { + ctx = session.ContextWithContent(ctx, &session.Content{ + SniffingRequest: session.SniffingRequest{ + Enabled: receiverConfig.SniffingSettings.Enabled, + OverrideDestinationForProtocol: receiverConfig.SniffingSettings.DestinationOverride, + ExcludeForDomain: receiverConfig.SniffingSettings.DomainsExcluded, + MetadataOnly: receiverConfig.SniffingSettings.MetadataOnly, + RouteOnly: receiverConfig.SniffingSettings.RouteOnly, + }, + }) + } rawProxy, err := common.CreateObject(ctx, proxyConfig) if err != nil { return nil, err diff --git a/infra/conf/xray.go b/infra/conf/xray.go index 2080bd8c..eff6b8a9 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -130,7 +130,10 @@ type InboundDetourConfig struct { func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) { receiverSettings := &proxyman.ReceiverConfig{} - if c.ListenOn == nil { + // TUN inbound doesn't need port configuration as it uses network interface instead + if strings.ToLower(c.Protocol) == "tun" { + // Skip port validation for TUN + } else if c.ListenOn == nil { // Listen on anyip, must set PortList if c.PortList == nil { return nil, errors.New("Listen on AnyIP but no Port(s) set in InboundDetour.") diff --git a/proxy/tun/handler.go b/proxy/tun/handler.go index 760d872e..f830a4ea 100644 --- a/proxy/tun/handler.go +++ b/proxy/tun/handler.go @@ -7,6 +7,7 @@ import ( "github.com/xtls/xray-core/common/buf" c "github.com/xtls/xray-core/common/ctx" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/session" @@ -19,11 +20,13 @@ import ( // Handler is managing object that tie together tun interface, ip stack and dispatch connections to the routing type Handler struct { - ctx context.Context - config *Config - stack Stack - policyManager policy.Manager - dispatcher routing.Dispatcher + ctx context.Context + config *Config + stack Stack + policyManager policy.Manager + dispatcher routing.Dispatcher + tag string + sniffingRequest session.SniffingRequest } // ConnectionHandler interface with the only method that stack is going to push new connections to @@ -43,6 +46,14 @@ func (t *Handler) policy() policy.Session { func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routing.Dispatcher) error { var err error + // Retrieve tag and sniffing config from context (set by AlwaysOnInboundHandler) + if inbound := session.InboundFromContext(ctx); inbound != nil { + t.tag = inbound.Tag + } + if content := session.ContentFromContext(ctx); content != nil { + t.sniffingRequest = content.SniffingRequest + } + t.ctx = core.ToBackgroundDetachedContext(ctx) t.policyManager = pm t.dispatcher = dispatcher @@ -93,29 +104,38 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) { sid := session.NewID() ctx := c.ContextWithID(t.ctx, sid) - errors.LogInfo(ctx, "processing connection from: ", conn.RemoteAddr()) - inbound := session.Inbound{} - inbound.Name = "tun" - inbound.CanSpliceCopy = 1 - inbound.Source = net.DestinationFromAddr(conn.RemoteAddr()) - inbound.User = &protocol.MemoryUser{ - Level: t.config.UserLevel, + inbound := session.Inbound{ + Name: "tun", + Tag: t.tag, + CanSpliceCopy: 3, + Source: net.DestinationFromAddr(conn.RemoteAddr()), + User: &protocol.MemoryUser{ + Level: t.config.UserLevel, + }, } ctx = session.ContextWithInbound(ctx, &inbound) + ctx = session.ContextWithContent(ctx, &session.Content{ + SniffingRequest: t.sniffingRequest, + }) ctx = session.SubContextFromMuxInbound(ctx) + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: inbound.Source, + To: destination, + Status: log.AccessAccepted, + Reason: "", + }) + errors.LogInfo(ctx, "processing TCP from ", conn.RemoteAddr(), " to ", destination) + link := &transport.Link{ Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)}, Writer: buf.NewWriter(conn), } if err := t.dispatcher.DispatchLink(ctx, destination, link); err != nil { errors.LogError(ctx, errors.New("connection closed").Base(err)) - return } - - errors.LogInfo(ctx, "connection completed") } // Network implements proxy.Inbound diff --git a/proxy/tun/udp_fullcone.go b/proxy/tun/udp_fullcone.go index bc50a7f3..d2920b34 100644 --- a/proxy/tun/udp_fullcone.go +++ b/proxy/tun/udp_fullcone.go @@ -10,6 +10,7 @@ import ( "github.com/xtls/xray-core/common/buf" c "github.com/xtls/xray-core/common/ctx" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/session" @@ -100,12 +101,23 @@ func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destina inbound := &session.Inbound{ Name: "tun", + Tag: u.handler.tag, Source: src, - CanSpliceCopy: 1, + CanSpliceCopy: 3, User: &protocol.MemoryUser{Level: u.handler.config.UserLevel}, } ctx = session.ContextWithInbound(c.ContextWithID(ctx, session.NewID()), inbound) + ctx = session.ContextWithContent(ctx, &session.Content{ + SniffingRequest: u.handler.sniffingRequest, + }) ctx = session.SubContextFromMuxInbound(ctx) + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: src, + To: dst, + Status: log.AccessAccepted, + Reason: "", + }) + errors.LogInfo(ctx, "processing UDP from ", src, " to ", dst) link := &transport.Link{ Reader: &buf.TimeoutWrapperReader{Reader: conn.reader}, // reverse source and destination, indicating the packets to write are going in the other From 7726fbece0d061c1cf1c04a9255ea4dc5359777b Mon Sep 17 00:00:00 2001 From: Owersun <4807375+Owersun@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:34:09 +0100 Subject: [PATCH 090/136] TUN inbound: Make udp_fullcone pure side effect free udp connection (#5526) https://github.com/XTLS/Xray-core/pull/5526#issue-3804306341 --------- Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- proxy/tun/handler.go | 5 +- proxy/tun/stack_gvisor.go | 80 ++++++++--- proxy/tun/udp_fullcone.go | 270 +++++++++++++------------------------- 3 files changed, 156 insertions(+), 199 deletions(-) diff --git a/proxy/tun/handler.go b/proxy/tun/handler.go index f830a4ea..2b73aad9 100644 --- a/proxy/tun/handler.go +++ b/proxy/tun/handler.go @@ -105,11 +105,12 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) { sid := session.NewID() ctx := c.ContextWithID(t.ctx, sid) + source := net.DestinationFromAddr(conn.RemoteAddr()) inbound := session.Inbound{ Name: "tun", Tag: t.tag, CanSpliceCopy: 3, - Source: net.DestinationFromAddr(conn.RemoteAddr()), + Source: source, User: &protocol.MemoryUser{ Level: t.config.UserLevel, }, @@ -127,7 +128,7 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) { Status: log.AccessAccepted, Reason: "", }) - errors.LogInfo(ctx, "processing TCP from ", conn.RemoteAddr(), " to ", destination) + errors.LogInfo(ctx, "processing from ", source, " to ", destination) link := &transport.Link{ Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)}, diff --git a/proxy/tun/stack_gvisor.go b/proxy/tun/stack_gvisor.go index d062c3d0..952150a8 100644 --- a/proxy/tun/stack_gvisor.go +++ b/proxy/tun/stack_gvisor.go @@ -9,6 +9,7 @@ import ( "gvisor.dev/gvisor/pkg/buffer" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/checksum" "gvisor.dev/gvisor/pkg/tcpip/header" "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" @@ -102,21 +103,7 @@ func (t *stackGVisor) Start() error { ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) // Use custom UDP packet handler, instead of strict gVisor forwarder, for FullCone NAT support - udpForwarder := newUdpConnectionHandler(t.ctx, t.handler, func(p []byte) { - // extract network protocol from the packet - var networkProtocol tcpip.NetworkProtocolNumber - switch header.IPVersion(p) { - case header.IPv4Version: - networkProtocol = header.IPv4ProtocolNumber - case header.IPv6Version: - networkProtocol = header.IPv6ProtocolNumber - default: - // discard packet with unknown network version - return - } - - ipStack.WriteRawPacket(defaultNIC, networkProtocol, buffer.MakeWithData(p)) - }) + udpForwarder := newUdpConnectionHandler(t.handler.HandleConnection, t.writeRawUDPPacket) ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { data := pkt.Data().AsRange().ToSlice() if len(data) == 0 { @@ -137,6 +124,69 @@ func (t *stackGVisor) Start() error { return nil } +func (t *stackGVisor) writeRawUDPPacket(payload []byte, src net.Destination, dst net.Destination) error { + udpLen := header.UDPMinimumSize + len(payload) + srcIP := tcpip.AddrFromSlice(src.Address.IP()) + dstIP := tcpip.AddrFromSlice(dst.Address.IP()) + + // build packet with appropriate IP header size + isIPv4 := dst.Address.Family().IsIPv4() + ipHdrSize := header.IPv6MinimumSize + ipProtocol := header.IPv6ProtocolNumber + if isIPv4 { + ipHdrSize = header.IPv4MinimumSize + ipProtocol = header.IPv4ProtocolNumber + } + + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + ReserveHeaderBytes: ipHdrSize + header.UDPMinimumSize, + Payload: buffer.MakeWithData(payload), + }) + defer pkt.DecRef() + + // Build UDP header + udpHdr := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize)) + udpHdr.Encode(&header.UDPFields{ + SrcPort: uint16(src.Port), + DstPort: uint16(dst.Port), + Length: uint16(udpLen), + }) + + // Calculate and set UDP checksum + xsum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, srcIP, dstIP, uint16(udpLen)) + udpHdr.SetChecksum(^udpHdr.CalculateChecksum(checksum.Checksum(payload, xsum))) + + // Build IP header + if isIPv4 { + ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize)) + ipHdr.Encode(&header.IPv4Fields{ + TotalLength: uint16(header.IPv4MinimumSize + udpLen), + TTL: 64, + Protocol: uint8(header.UDPProtocolNumber), + SrcAddr: srcIP, + DstAddr: dstIP, + }) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + } else { + ipHdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize)) + ipHdr.Encode(&header.IPv6Fields{ + PayloadLength: uint16(udpLen), + TransportProtocol: header.UDPProtocolNumber, + HopLimit: 64, + SrcAddr: srcIP, + DstAddr: dstIP, + }) + } + + // dispatch the packet + err := t.stack.WriteRawPacket(defaultNIC, ipProtocol, buffer.MakeWithView(pkt.ToView())) + if err != nil { + return errors.New("failed to write raw udp packet back to stack", err) + } + + return nil +} + // Close is called by Handler to shut down the stack func (t *stackGVisor) Close() error { if t.stack == nil { diff --git a/proxy/tun/udp_fullcone.go b/proxy/tun/udp_fullcone.go index d2920b34..df58ce4e 100644 --- a/proxy/tun/udp_fullcone.go +++ b/proxy/tun/udp_fullcone.go @@ -1,228 +1,134 @@ package tun import ( - "context" + "io" "sync" - "sync/atomic" - "time" - "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" - c "github.com/xtls/xray-core/common/ctx" - "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/common/protocol" - "github.com/xtls/xray-core/common/session" - "github.com/xtls/xray-core/common/signal/done" - "github.com/xtls/xray-core/common/task" - "github.com/xtls/xray-core/transport" - "github.com/xtls/xray-core/transport/pipe" - "gvisor.dev/gvisor/pkg/buffer" - "gvisor.dev/gvisor/pkg/tcpip" - "gvisor.dev/gvisor/pkg/tcpip/checksum" - "gvisor.dev/gvisor/pkg/tcpip/header" - "gvisor.dev/gvisor/pkg/tcpip/stack" ) -// udp connection abstraction -type udpConn struct { - lastActive atomic.Int64 - reader buf.Reader - writer buf.Writer - done *done.Instance - cancel context.CancelFunc -} - // sub-handler specifically for udp connections under main handler type udpConnectionHandler struct { sync.Mutex - ctx context.Context - handler *Handler - udpConns map[net.Destination]*udpConn - udpChecker *task.Periodic - writePacket func(p []byte) + + udpConns map[net.Destination]*udpConn + + handleConnection func(conn net.Conn, dest net.Destination) + writePacket func(data []byte, src net.Destination, dst net.Destination) error } -func newUdpConnectionHandler(ctx context.Context, h *Handler, writePacket func(p []byte)) *udpConnectionHandler { +func newUdpConnectionHandler(handleConnection func(conn net.Conn, dest net.Destination), writePacket func(data []byte, src net.Destination, dst net.Destination) error) *udpConnectionHandler { handler := &udpConnectionHandler{ - ctx: ctx, - handler: h, - udpConns: make(map[net.Destination]*udpConn), - writePacket: writePacket, + udpConns: make(map[net.Destination]*udpConn), + handleConnection: handleConnection, + writePacket: writePacket, } - handler.udpChecker = &task.Periodic{Interval: time.Minute, Execute: handler.cleanupUDP} - handler.udpChecker.Start() - return handler } -func (u *udpConnectionHandler) cleanupUDP() error { - u.Lock() - defer u.Unlock() - if len(u.udpConns) == 0 { - return errors.New("no connections") - } - now := time.Now().Unix() - for src, conn := range u.udpConns { - if now-conn.lastActive.Load() > 300 { - conn.cancel() - common.Must(conn.done.Close()) - common.Must(common.Close(conn.writer)) - delete(u.udpConns, src) - } - } - return nil -} - // HandlePacket handles UDP packets coming from tun, to forward to the dispatcher -// this custom handler support FullCone NAT of returning packets, binding connection only by the source port +// this custom handler support FullCone NAT of returning packets, binding connection only by the source addr:port func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destination, data []byte) bool { u.Lock() conn, found := u.udpConns[src] if !found { - reader, writer := pipe.New(pipe.DiscardOverflow(), pipe.WithSizeLimit(16*1024)) - conn = &udpConn{reader: reader, writer: writer, done: done.New()} + egress := make(chan []byte, 16) + conn = &udpConn{handler: u, egress: egress, src: src, dst: dst} u.udpConns[src] = conn - u.Unlock() - go func() { - ctx, cancel := context.WithCancel(u.ctx) - conn.cancel = cancel - defer func() { - cancel() - u.Lock() - delete(u.udpConns, src) - u.Unlock() - common.Must(conn.done.Close()) - common.Must(common.Close(conn.writer)) - }() - - inbound := &session.Inbound{ - Name: "tun", - Tag: u.handler.tag, - Source: src, - CanSpliceCopy: 3, - User: &protocol.MemoryUser{Level: u.handler.config.UserLevel}, - } - ctx = session.ContextWithInbound(c.ContextWithID(ctx, session.NewID()), inbound) - ctx = session.ContextWithContent(ctx, &session.Content{ - SniffingRequest: u.handler.sniffingRequest, - }) - ctx = session.SubContextFromMuxInbound(ctx) - ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ - From: src, - To: dst, - Status: log.AccessAccepted, - Reason: "", - }) - errors.LogInfo(ctx, "processing UDP from ", src, " to ", dst) - link := &transport.Link{ - Reader: &buf.TimeoutWrapperReader{Reader: conn.reader}, - // reverse source and destination, indicating the packets to write are going in the other - // direction (written back to tun) and should have reversed addressing - Writer: &udpWriter{handler: u, src: dst, dst: src}, - } - _ = u.handler.dispatcher.DispatchLink(ctx, dst, link) - }() - } else { - conn.lastActive.Store(time.Now().Unix()) - u.Unlock() + go u.handleConnection(conn, dst) } + u.Unlock() - b := buf.New() - b.Write(data) - b.UDP = &dst - conn.writer.WriteMultiBuffer(buf.MultiBuffer{b}) + // send packet data to the egress channel, if it has buffer, or discard + select { + case conn.egress <- data: + default: + } return true } -type udpWriter struct { - handler *udpConnectionHandler - // address in the side of stack, where packet will be coming from - src net.Destination - // address on the side of tun, where packet will be destined to - dst net.Destination +func (u *udpConnectionHandler) connectionFinished(src net.Destination) { + u.Lock() + conn, found := u.udpConns[src] + if found { + delete(u.udpConns, src) + close(conn.egress) + } + u.Unlock() } -func (w *udpWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { +// udp connection abstraction +type udpConn struct { + net.Conn + buf.Writer + + handler *udpConnectionHandler + + egress chan []byte + src net.Destination + dst net.Destination +} + +// Read packets from the connection +func (c *udpConn) Read(p []byte) (int, error) { + data, ok := <-c.egress + if !ok { + return 0, io.EOF + } + + n := copy(p, data) + return n, nil +} + +// Write returning packets back +func (c *udpConn) Write(p []byte) (int, error) { + // sending packets back mean sending payload with source/destination reversed + err := c.handler.writePacket(p, c.dst, c.src) + if err != nil { + return 0, nil + } + + return len(p), nil +} + +func (c *udpConn) Close() error { + c.handler.connectionFinished(c.src) + + return nil +} + +func (c *udpConn) LocalAddr() net.Addr { + return &net.UDPAddr{IP: c.dst.Address.IP(), Port: int(c.dst.Port.Value())} +} + +func (c *udpConn) RemoteAddr() net.Addr { + return &net.UDPAddr{IP: c.src.Address.IP(), Port: int(c.src.Port.Value())} +} + +// Write returning packets back +func (c *udpConn) WriteMultiBuffer(mb buf.MultiBuffer) error { for _, b := range mb { - // use captured in the dispatched packet source address b.UDP as source, if available, - // otherwise use captured in the writer source w.src - srcAddr := w.src + dst := c.dst if b.UDP != nil { - srcAddr = *b.UDP + dst = *b.UDP } - // validate address family matches - if srcAddr.Address.Family() != w.src.Address.Family() { - errors.LogWarning(context.Background(), "UDP return packet address family mismatch: expected ", w.src.Address.Family(), ", got ", srcAddr.Address.Family()) - b.Release() + // validate address family matches between buffer packet and the connection + if dst.Address.Family() != c.dst.Address.Family() { continue } - payload := b.Bytes() - udpLen := header.UDPMinimumSize + len(payload) - srcIP := tcpip.AddrFromSlice(srcAddr.Address.IP()) - dstIP := tcpip.AddrFromSlice(w.dst.Address.IP()) - - // build packet with appropriate IP header size - isIPv4 := srcAddr.Address.Family().IsIPv4() - ipHdrSize := header.IPv6MinimumSize - if isIPv4 { - ipHdrSize = header.IPv4MinimumSize + // sending packets back mean sending payload with source/destination reversed + err := c.handler.writePacket(b.Bytes(), dst, c.src) + if err != nil { + // udp doesn't guarantee delivery, so in any failure we just continue to the next packet + continue } - - pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ - ReserveHeaderBytes: ipHdrSize + header.UDPMinimumSize, - Payload: buffer.MakeWithData(payload), - }) - - // Build UDP header - udpHdr := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize)) - udpHdr.Encode(&header.UDPFields{ - SrcPort: uint16(srcAddr.Port), - DstPort: uint16(w.dst.Port), - Length: uint16(udpLen), - }) - - // Calculate and set UDP checksum - xsum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, srcIP, dstIP, uint16(udpLen)) - udpHdr.SetChecksum(^udpHdr.CalculateChecksum(checksum.Checksum(payload, xsum))) - - // Build IP header - if isIPv4 { - ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize)) - ipHdr.Encode(&header.IPv4Fields{ - TotalLength: uint16(header.IPv4MinimumSize + udpLen), - TTL: 64, - Protocol: uint8(header.UDPProtocolNumber), - SrcAddr: srcIP, - DstAddr: dstIP, - }) - ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) - } else { - ipHdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize)) - ipHdr.Encode(&header.IPv6Fields{ - PayloadLength: uint16(udpLen), - TransportProtocol: header.UDPProtocolNumber, - HopLimit: 64, - SrcAddr: srcIP, - DstAddr: dstIP, - }) - } - - // Write raw packet to network stack - views := pkt.AsSlices() - var data []byte - for _, view := range views { - data = append(data, view...) - } - w.handler.writePacket(data) - pkt.DecRef() - b.Release() } + return nil } From e742e84ded3e3c57a811fc9d8fa3b68660503660 Mon Sep 17 00:00:00 2001 From: Owersun <4807375+Owersun@users.noreply.github.com> Date: Mon, 12 Jan 2026 19:18:02 +0100 Subject: [PATCH 091/136] Upgrade gVisor to latest version v0.0.0-20260109181451-4be7c433dae2 (#5527) https://github.com/XTLS/Xray-core/pull/5526#issuecomment-3738638586 --- go.mod | 4 ++-- go.sum | 4 ++-- proxy/wireguard/tun.go | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index a39162cc..2f9892aa 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/xtls/xray-core -go 1.25 +go 1.25.5 require ( github.com/cloudflare/circl v1.6.2 @@ -29,7 +29,7 @@ require ( golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 google.golang.org/grpc v1.78.0 google.golang.org/protobuf v1.36.11 - gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 + gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2 h12.io/socks v1.0.3 lukechampine.com/blake3 v1.4.1 ) diff --git a/go.sum b/go.sum index 03f6eae5..f72bba31 100644 --- a/go.sum +++ b/go.sum @@ -156,8 +156,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h1:sfK5nHuG7lRFZ2FdTT3RimOqWBg8IrVm+/Vko1FVOsk= -gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= +gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2 h1:fr6L00yGG2RP5NMea6njWpdC+bm+cMdFClrSpaicp1c= +gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo= h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= diff --git a/proxy/wireguard/tun.go b/proxy/wireguard/tun.go index bd20fab2..f6437910 100644 --- a/proxy/wireguard/tun.go +++ b/proxy/wireguard/tun.go @@ -173,7 +173,7 @@ func createGVisorTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo }) stack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) - udpForwarder := udp.NewForwarder(stack, func(r *udp.ForwarderRequest) { + udpForwarder := udp.NewForwarder(stack, func(r *udp.ForwarderRequest) bool { go func(r *udp.ForwarderRequest) { var ( wq waiter.Queue @@ -195,6 +195,8 @@ func createGVisorTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo handler(net.UDPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)), gonet.NewUDPConn(&wq, ep)) }(r) + + return true }) stack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) } From de6be7c5a919e799a0405b4aa90d53f97074693a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 09:49:58 +0000 Subject: [PATCH 092/136] Bump golang.org/x/net from 0.48.0 to 0.49.0 (#5530) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.48.0 to 0.49.0. - [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.49.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 2f9892aa..b92482d2 100644 --- a/go.mod +++ b/go.mod @@ -21,10 +21,10 @@ require ( github.com/vishvananda/netlink v1.3.1 github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.46.0 - golang.org/x/net v0.48.0 + golang.org/x/crypto v0.47.0 + golang.org/x/net v0.49.0 golang.org/x/sync v0.19.0 - golang.org/x/sys v0.39.0 + golang.org/x/sys v0.40.0 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 google.golang.org/grpc v1.78.0 @@ -47,10 +47,10 @@ require ( github.com/quic-go/qpack v0.6.0 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/vishvananda/netns v0.0.5 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.40.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index f72bba31..fbf5fcf2 100644 --- a/go.sum +++ b/go.sum @@ -95,16 +95,16 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 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-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= @@ -116,21 +116,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 8a9dbd407f5c98b09f0d2f044c1b65928496aa87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 09:55:42 +0000 Subject: [PATCH 093/136] Bump github.com/miekg/dns from 1.1.69 to 1.1.70 (#5528) Bumps [github.com/miekg/dns](https://github.com/miekg/dns) from 1.1.69 to 1.1.70. - [Commits](https://github.com/miekg/dns/compare/v1.1.69...v1.1.70) --- updated-dependencies: - dependency-name: github.com/miekg/dns dependency-version: 1.1.70 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b92482d2..c52fc6d6 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/golang/mock v1.7.0-rc.1 github.com/google/go-cmp v0.7.0 github.com/gorilla/websocket v1.5.3 - github.com/miekg/dns v1.1.69 + github.com/miekg/dns v1.1.70 github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 github.com/quic-go/quic-go v0.58.0 diff --git a/go.sum b/go.sum index fbf5fcf2..5f8367d2 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= -github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= +github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA= +github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= From 92ada2dd1d5159bb0ed954298d9f4662def23d45 Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Tue, 13 Jan 2026 21:31:51 +0800 Subject: [PATCH 094/136] Proxy: Add Hysteria outbound & transport (version 2, udphop) and Salamander udpmask (#5508) https://github.com/XTLS/Xray-core/issues/3547#issuecomment-3549896520 https://github.com/XTLS/Xray-core/issues/2635#issuecomment-3570871754 --- app/dns/nameserver_quic.go | 2 +- common/net/port.go | 10 + common/protocol/quic/sniff.go | 2 +- go.mod | 3 +- go.sum | 6 +- infra/conf/hysteria.go | 23 + infra/conf/transport_internet.go | 224 ++++ infra/conf/xray.go | 15 +- proxy/hysteria/client.go | 263 +++++ proxy/hysteria/config.go | 10 + proxy/hysteria/config.pb.go | 126 +++ proxy/hysteria/config.proto | 13 + proxy/hysteria/frag.go | 73 ++ proxy/hysteria/protocol.go | 204 ++++ transport/internet/config.go | 4 +- transport/internet/config.pb.go | 382 +++---- transport/internet/config.proto | 3 + transport/internet/finalmask/finalmask.go | 127 +++ .../internet/finalmask/salamander/config.go | 42 + .../finalmask/salamander/config.pb.go | 123 +++ .../finalmask/salamander/config.proto | 12 + .../finalmask/salamander/obfs/conn.go | 121 +++ .../finalmask/salamander/obfs/salamander.go | 71 ++ .../salamander/obfs/salamander_test.go | 45 + transport/internet/hysteria/config.go | 47 + transport/internet/hysteria/config.pb.go | 232 +++++ transport/internet/hysteria/config.proto | 25 + .../hysteria/congestion/bbr/bandwidth.go | 27 + .../congestion/bbr/bandwidth_sampler.go | 874 ++++++++++++++++ .../hysteria/congestion/bbr/bbr_sender.go | 980 ++++++++++++++++++ .../internet/hysteria/congestion/bbr/clock.go | 18 + .../bbr/packet_number_indexed_queue.go | 199 ++++ .../hysteria/congestion/bbr/ringbuffer.go | 118 +++ .../congestion/bbr/windowed_filter.go | 162 +++ .../hysteria/congestion/brutal/brutal.go | 185 ++++ .../hysteria/congestion/common/pacer.go | 79 ++ .../internet/hysteria/congestion/utils.go | 18 + transport/internet/hysteria/conn.go | 101 ++ transport/internet/hysteria/dialer.go | 410 ++++++++ .../internet/hysteria/padding/padding.go | 24 + transport/internet/hysteria/udphop/addr.go | 65 ++ transport/internet/hysteria/udphop/conn.go | 297 ++++++ .../internet/hysteria/utils/portunion.go | 107 ++ .../internet/hysteria/utils/portunion_test.go | 150 +++ transport/internet/memory_settings.go | 31 +- transport/internet/splithttp/dialer.go | 4 +- transport/internet/splithttp/hub.go | 4 +- 47 files changed, 5818 insertions(+), 243 deletions(-) create mode 100644 infra/conf/hysteria.go create mode 100644 proxy/hysteria/client.go create mode 100644 proxy/hysteria/config.go create mode 100644 proxy/hysteria/config.pb.go create mode 100644 proxy/hysteria/config.proto create mode 100644 proxy/hysteria/frag.go create mode 100644 proxy/hysteria/protocol.go create mode 100644 transport/internet/finalmask/finalmask.go create mode 100644 transport/internet/finalmask/salamander/config.go create mode 100644 transport/internet/finalmask/salamander/config.pb.go create mode 100644 transport/internet/finalmask/salamander/config.proto create mode 100644 transport/internet/finalmask/salamander/obfs/conn.go create mode 100644 transport/internet/finalmask/salamander/obfs/salamander.go create mode 100644 transport/internet/finalmask/salamander/obfs/salamander_test.go create mode 100644 transport/internet/hysteria/config.go create mode 100644 transport/internet/hysteria/config.pb.go create mode 100644 transport/internet/hysteria/config.proto create mode 100644 transport/internet/hysteria/congestion/bbr/bandwidth.go create mode 100644 transport/internet/hysteria/congestion/bbr/bandwidth_sampler.go create mode 100644 transport/internet/hysteria/congestion/bbr/bbr_sender.go create mode 100644 transport/internet/hysteria/congestion/bbr/clock.go create mode 100644 transport/internet/hysteria/congestion/bbr/packet_number_indexed_queue.go create mode 100644 transport/internet/hysteria/congestion/bbr/ringbuffer.go create mode 100644 transport/internet/hysteria/congestion/bbr/windowed_filter.go create mode 100644 transport/internet/hysteria/congestion/brutal/brutal.go create mode 100644 transport/internet/hysteria/congestion/common/pacer.go create mode 100644 transport/internet/hysteria/congestion/utils.go create mode 100644 transport/internet/hysteria/conn.go create mode 100644 transport/internet/hysteria/dialer.go create mode 100644 transport/internet/hysteria/padding/padding.go create mode 100644 transport/internet/hysteria/udphop/addr.go create mode 100644 transport/internet/hysteria/udphop/conn.go create mode 100644 transport/internet/hysteria/utils/portunion.go create mode 100644 transport/internet/hysteria/utils/portunion_test.go diff --git a/app/dns/nameserver_quic.go b/app/dns/nameserver_quic.go index 4c7ac032..bd0da170 100644 --- a/app/dns/nameserver_quic.go +++ b/app/dns/nameserver_quic.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/quic-go/quic-go" + "github.com/apernet/quic-go" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/log" diff --git a/common/net/port.go b/common/net/port.go index d4a6514c..26f5e3e2 100644 --- a/common/net/port.go +++ b/common/net/port.go @@ -87,6 +87,16 @@ func PortListFromProto(l *PortList) MemoryPortList { return mpl } +func (l *PortList) Ports() []uint32 { + var ports []uint32 + for _, r := range l.Range { + for i := uint32(r.From); i <= uint32(r.To); i++ { + ports = append(ports, i) + } + } + return ports +} + func (mpl MemoryPortList) Contains(port Port) bool { for _, pr := range mpl { if pr.Contains(port) { diff --git a/common/protocol/quic/sniff.go b/common/protocol/quic/sniff.go index 0691bad6..5b29d6ff 100644 --- a/common/protocol/quic/sniff.go +++ b/common/protocol/quic/sniff.go @@ -7,7 +7,7 @@ import ( "encoding/binary" "io" - "github.com/quic-go/quic-go/quicvarint" + "github.com/apernet/quic-go/quicvarint" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" diff --git a/go.mod b/go.mod index c52fc6d6..f6bfcb79 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/xtls/xray-core go 1.25.5 require ( + github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 github.com/cloudflare/circl v1.6.2 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/golang/mock v1.7.0-rc.1 @@ -11,7 +12,6 @@ require ( github.com/miekg/dns v1.1.70 github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 - github.com/quic-go/quic-go v0.58.0 github.com/refraction-networking/utls v1.8.1 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 @@ -22,6 +22,7 @@ require ( github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.47.0 + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/net v0.49.0 golang.org/x/sync v0.19.0 golang.org/x/sys v0.40.0 diff --git a/go.sum b/go.sum index 5f8367d2..0f8e1428 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 h1:bSq8n+gX4oO/qnM3MKf4kroW75n+phO9Qp6nigJKZ1E= +github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178/go.mod h1:N1WIjPphkqs4efXWuyDNQ6OjjIK04vM3h+bEgwV+eVU= github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -50,8 +52,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug= -github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= @@ -97,6 +97,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +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/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= diff --git a/infra/conf/hysteria.go b/infra/conf/hysteria.go new file mode 100644 index 00000000..6512d105 --- /dev/null +++ b/infra/conf/hysteria.go @@ -0,0 +1,23 @@ +package conf + +import ( + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/proxy/hysteria" + "google.golang.org/protobuf/proto" +) + +type HysteriaClientConfig struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` +} + +func (c *HysteriaClientConfig) Build() (proto.Message, error) { + config := new(hysteria.ClientConfig) + + config.Server = &protocol.ServerEndpoint{ + Address: c.Address.Build(), + Port: uint32(c.Port), + } + + return config, nil +} diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 2544c016..742c7df5 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -16,7 +16,9 @@ import ( "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/finalmask/salamander" "github.com/xtls/xray-core/transport/internet/httpupgrade" + "github.com/xtls/xray-core/transport/internet/hysteria" "github.com/xtls/xray-core/transport/internet/kcp" "github.com/xtls/xray-core/transport/internet/reality" "github.com/xtls/xray-core/transport/internet/splithttp" @@ -332,6 +334,161 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { return config, nil } +const ( + Byte = 1 + Kilobyte = 1024 * Byte + Megabyte = 1024 * Kilobyte + Gigabyte = 1024 * Megabyte + Terabyte = 1024 * Gigabyte +) + +type Bandwidth string + +func (b Bandwidth) Bps() (uint64, error) { + s := strings.TrimSpace(strings.ToLower(string(b))) + if s == "" { + return 0, nil + } + + idx := len(s) + for i, c := range s { + if (c < '0' || c > '9') && c != '.' { + idx = i + break + } + } + + numStr := s[:idx] + unit := strings.TrimSpace(s[idx:]) + + val, err := strconv.ParseFloat(numStr, 64) + if err != nil { + return 0, err + } + + mul := uint64(1) + switch unit { + case "", "b", "bps": + mul = Byte + case "k", "kb", "kbps": + mul = Kilobyte + case "m", "mb", "mbps": + mul = Megabyte + case "g", "gb", "gbps": + mul = Gigabyte + case "t", "tb", "tbps": + mul = Terabyte + default: + return 0, errors.New("unsupported unit: " + unit) + } + + return uint64(val*float64(mul)) / 8, nil +} + +type UdpHop struct { + PortList json.RawMessage `json:"port"` + Interval int64 `json:"interval"` +} + +type HysteriaConfig struct { + Version int32 `json:"version"` + Auth string `json:"auth"` + Up Bandwidth `json:"up"` + Down Bandwidth `json:"down"` + UdpHop UdpHop `json:"udphop"` + + InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"` + MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"` + InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"` + MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"` + MaxIdleTimeout int64 `json:"maxIdleTimeout"` + KeepAlivePeriod int64 `json:"keepAlivePeriod"` + DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"` +} + +func (c *HysteriaConfig) Build() (proto.Message, error) { + if c.Version != 2 { + return nil, errors.New("version != 2") + } + up, err := c.Up.Bps() + if err != nil { + return nil, err + } + down, err := c.Down.Bps() + if err != nil { + return nil, err + } + var hop *PortList + if err := json.Unmarshal(c.UdpHop.PortList, &hop); err != nil { + hop = &PortList{} + } + + if up > 0 && up < 65536 { + return nil, errors.New("Up must be at least 65536 Bps") + } + if down > 0 && down < 65536 { + return nil, errors.New("Down must be at least 65536 Bps") + } + if c.UdpHop.Interval != 0 && c.UdpHop.Interval < 5 { + return nil, errors.New("Interval must be at least 5") + } + + if c.InitStreamReceiveWindow > 0 && c.InitStreamReceiveWindow < 16384 { + return nil, errors.New("InitStreamReceiveWindow must be at least 16384") + } + if c.MaxStreamReceiveWindow > 0 && c.MaxStreamReceiveWindow < 16384 { + return nil, errors.New("MaxStreamReceiveWindow must be at least 16384") + } + if c.InitConnectionReceiveWindow > 0 && c.InitConnectionReceiveWindow < 16384 { + return nil, errors.New("InitConnectionReceiveWindow must be at least 16384") + } + if c.MaxConnectionReceiveWindow > 0 && c.MaxConnectionReceiveWindow < 16384 { + return nil, errors.New("MaxConnectionReceiveWindow must be at least 16384") + } + if c.MaxIdleTimeout != 0 && (c.MaxIdleTimeout < 4 || c.MaxIdleTimeout > 120) { + return nil, errors.New("MaxIdleTimeout must be between 4 and 120") + } + if c.KeepAlivePeriod != 0 && (c.KeepAlivePeriod < 2 || c.KeepAlivePeriod > 60) { + return nil, errors.New("KeepAlivePeriod must be between 2 and 60") + } + + config := &hysteria.Config{} + config.Version = int32(c.Version) + config.Auth = c.Auth + config.Up = up + config.Down = down + config.Ports = hop.Build().Ports() + config.Interval = c.UdpHop.Interval + config.InitStreamReceiveWindow = c.InitStreamReceiveWindow + config.MaxStreamReceiveWindow = c.MaxStreamReceiveWindow + config.InitConnReceiveWindow = c.InitConnectionReceiveWindow + config.MaxConnReceiveWindow = c.MaxConnectionReceiveWindow + config.MaxIdleTimeout = c.MaxIdleTimeout + config.KeepAlivePeriod = c.KeepAlivePeriod + config.DisablePathMtuDiscovery = c.DisablePathMTUDiscovery + + if config.InitStreamReceiveWindow == 0 { + config.InitStreamReceiveWindow = 8388608 + } + if config.MaxStreamReceiveWindow == 0 { + config.MaxStreamReceiveWindow = 8388608 + } + if config.InitConnReceiveWindow == 0 { + config.InitConnReceiveWindow = 8388608 * 5 / 2 + } + if config.MaxConnReceiveWindow == 0 { + config.MaxConnReceiveWindow = 8388608 * 5 / 2 + } + if config.MaxIdleTimeout == 0 { + config.MaxIdleTimeout = 30 + } + // if config.KeepAlivePeriod == 0 { + // config.KeepAlivePeriod = 10 + // } + + return config, nil +} + func readFileOrString(f string, s []string) ([]byte, error) { if len(f) > 0 { return filesystem.ReadCert(f) @@ -746,6 +903,8 @@ func (p TransportProtocol) Build() (string, error) { return "", errors.PrintRemovedFeatureError("HTTP transport (without header padding, etc.)", "XHTTP stream-one H2 & H3") case "quic": return "", errors.PrintRemovedFeatureError("QUIC transport (without web service, etc.)", "XHTTP stream-one H3") + case "hysteria": + return "hysteria", nil default: return "", errors.New("Config: unknown transport protocol: ", p) } @@ -928,11 +1087,54 @@ func (c *SocketConfig) Build() (*internet.SocketConfig, error) { }, nil } +var ( + udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "salamander": func() interface{} { return new(Salamander) }, + }, "type", "settings") +) + +type Salamander struct { + Password string `json:"password"` +} + +func (c *Salamander) Build() (proto.Message, error) { + config := &salamander.Config{} + config.Password = c.Password + return config, nil +} + +type FinalMask struct { + Type string `json:"type"` + Settings *json.RawMessage `json:"settings"` +} + +func (c *FinalMask) Build(tcpmaskLoader bool) (proto.Message, error) { + loader := udpmaskLoader + if tcpmaskLoader { + return nil, errors.New("") + } + + settings := []byte("{}") + if c.Settings != nil { + settings = ([]byte)(*c.Settings) + } + rawConfig, err := loader.LoadWithID(settings, c.Type) + if err != nil { + return nil, err + } + ts, err := rawConfig.(Buildable).Build() + if err != nil { + return nil, err + } + return ts, nil +} + type StreamConfig struct { Address *Address `json:"address"` Port uint16 `json:"port"` Network *TransportProtocol `json:"network"` Security string `json:"security"` + Udpmasks []*FinalMask `json:"udpmasks"` TLSSettings *TLSConfig `json:"tlsSettings"` REALITYSettings *REALITYConfig `json:"realitySettings"` RAWSettings *TCPConfig `json:"rawSettings"` @@ -943,6 +1145,7 @@ type StreamConfig struct { GRPCSettings *GRPCConfig `json:"grpcSettings"` WSSettings *WebSocketConfig `json:"wsSettings"` HTTPUPGRADESettings *HttpUpgradeConfig `json:"httpupgradeSettings"` + HysteriaSettings *HysteriaConfig `json:"hysteriaSettings"` SocketSettings *SocketConfig `json:"sockopt"` } @@ -962,6 +1165,7 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) { } config.ProtocolName = protocol } + switch strings.ToLower(c.Security) { case "", "none": case "tls": @@ -995,6 +1199,7 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) { default: return nil, errors.New(`Unknown security "` + c.Security + `".`) } + if c.RAWSettings != nil { c.TCPSettings = c.RAWSettings } @@ -1061,6 +1266,16 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) { Settings: serial.ToTypedMessage(hs), }) } + if c.HysteriaSettings != nil { + hs, err := c.HysteriaSettings.Build() + if err != nil { + return nil, errors.New("Failed to build Hysteria config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "hysteria", + Settings: serial.ToTypedMessage(hs), + }) + } if c.SocketSettings != nil { ss, err := c.SocketSettings.Build() if err != nil { @@ -1068,6 +1283,15 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) { } config.SocketSettings = ss } + + for _, mask := range c.Udpmasks { + u, err := mask.Build(false) + if err != nil { + return nil, errors.New("failed to build mask with type ", mask.Type).Base(err) + } + config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(u)) + } + return config, nil } diff --git a/infra/conf/xray.go b/infra/conf/xray.go index eff6b8a9..9e5c1394 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -43,6 +43,7 @@ var ( "vless": func() interface{} { return new(VLessOutboundConfig) }, "vmess": func() interface{} { return new(VMessOutboundConfig) }, "trojan": func() interface{} { return new(TrojanClientConfig) }, + "hysteria": func() interface{} { return new(HysteriaClientConfig) }, "dns": func() interface{} { return new(DNSOutboundConfig) }, "wireguard": func() interface{} { return &WireGuardConfig{IsClient: true} }, }, "protocol", "settings") @@ -117,13 +118,13 @@ func (m *MuxConfig) Build() (*proxyman.MultiplexingConfig, error) { } type InboundDetourConfig struct { - Protocol string `json:"protocol"` - PortList *PortList `json:"port"` - ListenOn *Address `json:"listen"` - Settings *json.RawMessage `json:"settings"` - Tag string `json:"tag"` - StreamSetting *StreamConfig `json:"streamSettings"` - SniffingConfig *SniffingConfig `json:"sniffing"` + Protocol string `json:"protocol"` + PortList *PortList `json:"port"` + ListenOn *Address `json:"listen"` + Settings *json.RawMessage `json:"settings"` + Tag string `json:"tag"` + StreamSetting *StreamConfig `json:"streamSettings"` + SniffingConfig *SniffingConfig `json:"sniffing"` } // Build implements Buildable. diff --git a/proxy/hysteria/client.go b/proxy/hysteria/client.go new file mode 100644 index 00000000..4e985443 --- /dev/null +++ b/proxy/hysteria/client.go @@ -0,0 +1,263 @@ +package hysteria + +import ( + "context" + go_errors "errors" + "io" + "math/rand" + + "github.com/apernet/quic-go" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/hysteria" + "github.com/xtls/xray-core/transport/internet/stat" +) + +type Client struct { + server *protocol.ServerSpec + policyManager policy.Manager +} + +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + if config.Server == nil { + return nil, errors.New(`no target server found`) + } + server, err := protocol.NewServerSpecFromPB(config.Server) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err) + } + + v := core.MustFromContext(ctx) + client := &Client{ + server: server, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + return client, nil +} + +func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() { + return errors.New("target not specified") + } + ob.Name = "hysteria" + ob.CanSpliceCopy = 3 + target := ob.Target + + conn, err := dialer.Dial(ctx, c.server.Destination) + if err != nil { + return errors.New("failed to find an available destination").AtWarning().Base(err) + } + defer conn.Close() + errors.LogInfo(ctx, "tunneling request to ", target, " via ", target.Network, ":", c.server.Destination.NetAddr()) + + var newCtx context.Context + var newCancel context.CancelFunc + if session.TimeoutOnlyFromContext(ctx) { + newCtx, newCancel = context.WithCancel(context.Background()) + } + + sessionPolicy := c.policyManager.ForLevel(0) + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, func() { + cancel() + if newCancel != nil { + newCancel() + } + }, sessionPolicy.Timeouts.ConnectionIdle) + + if newCtx != nil { + ctx = newCtx + } + + if target.Network == net.Network_TCP { + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + bufferedWriter := buf.NewBufferedWriter(buf.NewWriter(conn)) + err := WriteTCPRequest(bufferedWriter, target.NetAddr()) + if err != nil { + return errors.New("failed to write request").Base(err) + } + if err := bufferedWriter.SetBuffered(false); err != nil { + return err + } + return buf.Copy(link.Reader, bufferedWriter, buf.UpdateActivity(timer)) + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + ok, msg, err := ReadTCPResponse(conn) + if err != nil { + return err + } + if !ok { + return errors.New(msg) + } + return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer)) + } + + responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil { + return errors.New("connection ends").Base(err) + } + + return nil + } + + if target.Network == net.Network_UDP { + iConn := stat.TryUnwrapStatsConn(conn) + _, ok := iConn.(*hysteria.InterUdpConn) + if !ok { + return errors.New("udp requires hysteria udp transport") + } + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + writer := &UDPWriter{ + Writer: conn, + buf: make([]byte, MaxUDPSize), + addr: target.NetAddr(), + } + + if err := buf.Copy(link.Reader, writer, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to transport all UDP request").Base(err) + } + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + reader := &UDPReader{ + Reader: conn, + df: &Defragger{}, + } + + if err := buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to transport all UDP response").Base(err) + } + return nil + } + + responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil { + return errors.New("connection ends").Base(err) + } + + return nil + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} + +type UDPWriter struct { + Writer io.Writer + buf []byte + addr string +} + +func (w *UDPWriter) sendMsg(msg *UDPMessage) error { + msgN := msg.Serialize(w.buf) + if msgN < 0 { + // Message larger than buffer, silent drop + return nil + } + _, err := w.Writer.Write(w.buf[:msgN]) + return err +} + +func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + for { + mb2, b := buf.SplitFirst(mb) + mb = mb2 + if b == nil { + break + } + addr := w.addr + if b.UDP != nil { + addr = b.UDP.NetAddr() + } + msg := &UDPMessage{ + SessionID: 0, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: addr, + Data: b.Bytes(), + } + if err := w.sendMsg(msg); err != nil { + var errTooLarge *quic.DatagramTooLargeError + if go_errors.As(err, &errTooLarge) { + msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1 + fMsgs := FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize)) + for _, fMsg := range fMsgs { + err := w.sendMsg(&fMsg) + if err != nil { + b.Release() + buf.ReleaseMulti(mb) + return err + } + } + } else { + b.Release() + buf.ReleaseMulti(mb) + return err + } + } + b.Release() + } + return nil +} + +type UDPReader struct { + Reader io.Reader + df *Defragger +} + +func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + for { + b := buf.New() + _, err := b.ReadFrom(r.Reader) + if err != nil { + b.Release() + return nil, err + } + + msg, err := ParseUDPMessage(b.Bytes()) + if err != nil { + b.Release() + continue + } + + dfMsg := r.df.Feed(msg) + if dfMsg == nil { + continue + } + + dest, _ := net.ParseDestination("udp:" + dfMsg.Addr) + + buffer := buf.New() + buffer.Write(dfMsg.Data) + buffer.UDP = &dest + + return buf.MultiBuffer{buffer}, nil + } +} diff --git a/proxy/hysteria/config.go b/proxy/hysteria/config.go new file mode 100644 index 00000000..2650d856 --- /dev/null +++ b/proxy/hysteria/config.go @@ -0,0 +1,10 @@ +package hysteria + +import ( + "github.com/xtls/xray-core/transport/internet/hysteria/padding" +) + +var ( + tcpRequestPadding = padding.Padding{Min: 64, Max: 512} + // tcpResponsePadding = padding.Padding{Min: 128, Max: 1024} +) diff --git a/proxy/hysteria/config.pb.go b/proxy/hysteria/config.pb.go new file mode 100644 index 00000000..d58022cc --- /dev/null +++ b/proxy/hysteria/config.pb.go @@ -0,0 +1,126 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v6.33.1 +// source: proxy/hysteria/config.proto + +package hysteria + +import ( + protocol "github.com/xtls/xray-core/common/protocol" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ClientConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + mi := &file_proxy_hysteria_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_hysteria_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 ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_hysteria_config_proto_rawDescGZIP(), []int{0} +} + +func (x *ClientConfig) GetServer() *protocol.ServerEndpoint { + if x != nil { + return x.Server + } + return nil +} + +var File_proxy_hysteria_config_proto protoreflect.FileDescriptor + +const file_proxy_hysteria_config_proto_rawDesc = "" + + "\n" + + "\x1bproxy/hysteria/config.proto\x12\x13xray.proxy.hysteria\x1a!common/protocol/server_spec.proto\"L\n" + + "\fClientConfig\x12<\n" + + "\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06serverB[\n" + + "\x17com.xray.proxy.hysteriaP\x01Z(github.com/xtls/xray-core/proxy/hysteria\xaa\x02\x13Xray.Proxy.Hysteriab\x06proto3" + +var ( + file_proxy_hysteria_config_proto_rawDescOnce sync.Once + file_proxy_hysteria_config_proto_rawDescData []byte +) + +func file_proxy_hysteria_config_proto_rawDescGZIP() []byte { + file_proxy_hysteria_config_proto_rawDescOnce.Do(func() { + file_proxy_hysteria_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_hysteria_config_proto_rawDesc), len(file_proxy_hysteria_config_proto_rawDesc))) + }) + return file_proxy_hysteria_config_proto_rawDescData +} + +var file_proxy_hysteria_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_hysteria_config_proto_goTypes = []any{ + (*ClientConfig)(nil), // 0: xray.proxy.hysteria.ClientConfig + (*protocol.ServerEndpoint)(nil), // 1: xray.common.protocol.ServerEndpoint +} +var file_proxy_hysteria_config_proto_depIdxs = []int32{ + 1, // 0: xray.proxy.hysteria.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_proxy_hysteria_config_proto_init() } +func file_proxy_hysteria_config_proto_init() { + if File_proxy_hysteria_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_hysteria_config_proto_rawDesc), len(file_proxy_hysteria_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_hysteria_config_proto_goTypes, + DependencyIndexes: file_proxy_hysteria_config_proto_depIdxs, + MessageInfos: file_proxy_hysteria_config_proto_msgTypes, + }.Build() + File_proxy_hysteria_config_proto = out.File + file_proxy_hysteria_config_proto_goTypes = nil + file_proxy_hysteria_config_proto_depIdxs = nil +} diff --git a/proxy/hysteria/config.proto b/proxy/hysteria/config.proto new file mode 100644 index 00000000..c54c0ead --- /dev/null +++ b/proxy/hysteria/config.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package xray.proxy.hysteria; +option csharp_namespace = "Xray.Proxy.Hysteria"; +option go_package = "github.com/xtls/xray-core/proxy/hysteria"; +option java_package = "com.xray.proxy.hysteria"; +option java_multiple_files = true; + +import "common/protocol/server_spec.proto"; + +message ClientConfig { + xray.common.protocol.ServerEndpoint server = 1; +} diff --git a/proxy/hysteria/frag.go b/proxy/hysteria/frag.go new file mode 100644 index 00000000..64a6b0e1 --- /dev/null +++ b/proxy/hysteria/frag.go @@ -0,0 +1,73 @@ +package hysteria + +func FragUDPMessage(m *UDPMessage, maxSize int) []UDPMessage { + if m.Size() <= maxSize { + return []UDPMessage{*m} + } + fullPayload := m.Data + maxPayloadSize := maxSize - m.HeaderSize() + off := 0 + fragID := uint8(0) + fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up + frags := make([]UDPMessage, fragCount) + for off < len(fullPayload) { + payloadSize := len(fullPayload) - off + if payloadSize > maxPayloadSize { + payloadSize = maxPayloadSize + } + frag := *m + frag.FragID = fragID + frag.FragCount = fragCount + frag.Data = fullPayload[off : off+payloadSize] + frags[fragID] = frag + off += payloadSize + fragID++ + } + return frags +} + +// Defragger handles the defragmentation of UDP messages. +// The current implementation can only handle one packet ID at a time. +// If another packet arrives before a packet has received all fragments +// in their entirety, any previous state is discarded. +type Defragger struct { + pktID uint16 + frags []*UDPMessage + count uint8 + size int // data size +} + +func (d *Defragger) Feed(m *UDPMessage) *UDPMessage { + if m.FragCount <= 1 { + return m + } + if m.FragID >= m.FragCount { + // wtf is this? + return nil + } + if m.PacketID != d.pktID || m.FragCount != uint8(len(d.frags)) { + // new message, clear previous state + d.pktID = m.PacketID + d.frags = make([]*UDPMessage, m.FragCount) + d.frags[m.FragID] = m + d.count = 1 + d.size = len(m.Data) + } else if d.frags[m.FragID] == nil { + d.frags[m.FragID] = m + d.count++ + d.size += len(m.Data) + if int(d.count) == len(d.frags) { + // all fragments received, assemble + data := make([]byte, d.size) + off := 0 + for _, frag := range d.frags { + off += copy(data[off:], frag.Data) + } + m.Data = data + m.FragID = 0 + m.FragCount = 1 + return m + } + } + return nil +} diff --git a/proxy/hysteria/protocol.go b/proxy/hysteria/protocol.go new file mode 100644 index 00000000..ee4834a0 --- /dev/null +++ b/proxy/hysteria/protocol.go @@ -0,0 +1,204 @@ +package hysteria + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + "github.com/apernet/quic-go/quicvarint" + "github.com/xtls/xray-core/common/errors" +) + +const ( + FrameTypeTCPRequest = 0x401 + + // Max length values are for preventing DoS attacks + + MaxAddressLength = 2048 + MaxMessageLength = 2048 + MaxPaddingLength = 4096 + + MaxUDPSize = 4096 + + maxVarInt1 = 63 + maxVarInt2 = 16383 + maxVarInt4 = 1073741823 + maxVarInt8 = 4611686018427387903 +) + +// TCPRequest format: +// 0x401 (QUIC varint) +// Address length (QUIC varint) +// Address (bytes) +// Padding length (QUIC varint) +// Padding (bytes) + +func WriteTCPRequest(w io.Writer, addr string) error { + padding := tcpRequestPadding.String() + paddingLen := len(padding) + addrLen := len(addr) + sz := int(quicvarint.Len(FrameTypeTCPRequest)) + + int(quicvarint.Len(uint64(addrLen))) + addrLen + + int(quicvarint.Len(uint64(paddingLen))) + paddingLen + buf := make([]byte, sz) + i := varintPut(buf, FrameTypeTCPRequest) + i += varintPut(buf[i:], uint64(addrLen)) + i += copy(buf[i:], addr) + i += varintPut(buf[i:], uint64(paddingLen)) + copy(buf[i:], padding) + _, err := w.Write(buf) + return err +} + +// TCPResponse format: +// Status (byte, 0=ok, 1=error) +// Message length (QUIC varint) +// Message (bytes) +// Padding length (QUIC varint) +// Padding (bytes) + +func ReadTCPResponse(r io.Reader) (bool, string, error) { + var status [1]byte + if _, err := io.ReadFull(r, status[:]); err != nil { + return false, "", err + } + bReader := quicvarint.NewReader(r) + msgLen, err := quicvarint.Read(bReader) + if err != nil { + return false, "", err + } + if msgLen > MaxMessageLength { + return false, "", errors.New("invalid message length") + } + var msgBuf []byte + // No message is fine + if msgLen > 0 { + msgBuf = make([]byte, msgLen) + _, err = io.ReadFull(r, msgBuf) + if err != nil { + return false, "", err + } + } + paddingLen, err := quicvarint.Read(bReader) + if err != nil { + return false, "", err + } + if paddingLen > MaxPaddingLength { + return false, "", errors.New("invalid padding length") + } + if paddingLen > 0 { + _, err = io.CopyN(io.Discard, r, int64(paddingLen)) + if err != nil { + return false, "", err + } + } + return status[0] == 0, string(msgBuf), nil +} + +// UDPMessage format: +// Session ID (uint32 BE) +// Packet ID (uint16 BE) +// Fragment ID (uint8) +// Fragment count (uint8) +// Address length (QUIC varint) +// Address (bytes) +// Data... + +type UDPMessage struct { + SessionID uint32 // 4 + PacketID uint16 // 2 + FragID uint8 // 1 + FragCount uint8 // 1 + Addr string // varint + bytes + Data []byte +} + +func (m *UDPMessage) HeaderSize() int { + lAddr := len(m.Addr) + return 4 + 2 + 1 + 1 + int(quicvarint.Len(uint64(lAddr))) + lAddr +} + +func (m *UDPMessage) Size() int { + return m.HeaderSize() + len(m.Data) +} + +func (m *UDPMessage) Serialize(buf []byte) int { + // Make sure the buffer is big enough + if len(buf) < m.Size() { + return -1 + } + // binary.BigEndian.PutUint32(buf, m.SessionID) + binary.BigEndian.PutUint16(buf[4:], m.PacketID) + buf[6] = m.FragID + buf[7] = m.FragCount + i := varintPut(buf[8:], uint64(len(m.Addr))) + i += copy(buf[8+i:], m.Addr) + i += copy(buf[8+i:], m.Data) + return 8 + i +} + +func ParseUDPMessage(msg []byte) (*UDPMessage, error) { + m := &UDPMessage{} + buf := bytes.NewBuffer(msg) + if err := binary.Read(buf, binary.BigEndian, &m.SessionID); err != nil { + return nil, err + } + if err := binary.Read(buf, binary.BigEndian, &m.PacketID); err != nil { + return nil, err + } + if err := binary.Read(buf, binary.BigEndian, &m.FragID); err != nil { + return nil, err + } + if err := binary.Read(buf, binary.BigEndian, &m.FragCount); err != nil { + return nil, err + } + lAddr, err := quicvarint.Read(buf) + if err != nil { + return nil, err + } + if lAddr == 0 || lAddr > MaxMessageLength { + return nil, errors.New("invalid address length") + } + bs := buf.Bytes() + if len(bs) <= int(lAddr) { + // We use <= instead of < here as we expect at least one byte of data after the address + return nil, errors.New("invalid message length") + } + m.Addr = string(bs[:lAddr]) + m.Data = bs[lAddr:] + return m, nil +} + +// varintPut is like quicvarint.Append, but instead of appending to a slice, +// it writes to a fixed-size buffer. Returns the number of bytes written. +func varintPut(b []byte, i uint64) int { + if i <= maxVarInt1 { + b[0] = uint8(i) + return 1 + } + if i <= maxVarInt2 { + b[0] = uint8(i>>8) | 0x40 + b[1] = uint8(i) + return 2 + } + if i <= maxVarInt4 { + b[0] = uint8(i>>24) | 0x80 + b[1] = uint8(i >> 16) + b[2] = uint8(i >> 8) + b[3] = uint8(i) + return 4 + } + if i <= maxVarInt8 { + b[0] = uint8(i>>56) | 0xc0 + b[1] = uint8(i >> 48) + b[2] = uint8(i >> 40) + b[3] = uint8(i >> 32) + b[4] = uint8(i >> 24) + b[5] = uint8(i >> 16) + b[6] = uint8(i >> 8) + b[7] = uint8(i) + return 8 + } + panic(fmt.Sprintf("%#x doesn't fit into 62 bits", i)) +} diff --git a/transport/internet/config.go b/transport/internet/config.go index bd6dfac4..0b6f9324 100644 --- a/transport/internet/config.go +++ b/transport/internet/config.go @@ -90,7 +90,7 @@ func (c *StreamConfig) GetEffectiveSecuritySettings() (interface{}, error) { } func (c *StreamConfig) HasSecuritySettings() bool { - return len(c.SecurityType) > 0 + return len(c.SecuritySettings) > 0 } func (c *ProxyConfig) HasTag() bool { @@ -130,7 +130,7 @@ func (s DomainStrategy) FallbackIP6() bool { } func (s DomainStrategy) GetDynamicStrategy(addrFamily net.AddressFamily) DomainStrategy { - if addrFamily.IsDomain(){ + if addrFamily.IsDomain() { return s } switch s { diff --git a/transport/internet/config.pb.go b/transport/internet/config.pb.go index 41ce3294..89f83a12 100644 --- a/transport/internet/config.pb.go +++ b/transport/internet/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.2 +// protoc-gen-go v1.36.10 +// protoc v6.33.1 // source: transport/internet/config.proto package internet @@ -13,6 +13,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -209,14 +210,13 @@ func (SocketConfig_TProxyMode) EnumDescriptor() ([]byte, []int) { } type TransportConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Transport protocol name. ProtocolName string `protobuf:"bytes,3,opt,name=protocol_name,json=protocolName,proto3" json:"protocol_name,omitempty"` // Specific transport protocol settings. - Settings *serial.TypedMessage `protobuf:"bytes,2,opt,name=settings,proto3" json:"settings,omitempty"` + Settings *serial.TypedMessage `protobuf:"bytes,2,opt,name=settings,proto3" json:"settings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *TransportConfig) Reset() { @@ -264,12 +264,9 @@ func (x *TransportConfig) GetSettings() *serial.TypedMessage { } type StreamConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Address *net.IPOrDomain `protobuf:"bytes,8,opt,name=address,proto3" json:"address,omitempty"` - Port uint32 `protobuf:"varint,9,opt,name=port,proto3" json:"port,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Address *net.IPOrDomain `protobuf:"bytes,8,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,9,opt,name=port,proto3" json:"port,omitempty"` // Effective network. ProtocolName string `protobuf:"bytes,5,opt,name=protocol_name,json=protocolName,proto3" json:"protocol_name,omitempty"` TransportSettings []*TransportConfig `protobuf:"bytes,2,rep,name=transport_settings,json=transportSettings,proto3" json:"transport_settings,omitempty"` @@ -277,7 +274,11 @@ type StreamConfig struct { SecurityType string `protobuf:"bytes,3,opt,name=security_type,json=securityType,proto3" json:"security_type,omitempty"` // Transport security settings. They can be either TLS or REALITY. SecuritySettings []*serial.TypedMessage `protobuf:"bytes,4,rep,name=security_settings,json=securitySettings,proto3" json:"security_settings,omitempty"` + Udpmasks []*serial.TypedMessage `protobuf:"bytes,10,rep,name=udpmasks,proto3" json:"udpmasks,omitempty"` + Tcpmasks []*serial.TypedMessage `protobuf:"bytes,11,rep,name=tcpmasks,proto3" json:"tcpmasks,omitempty"` SocketSettings *SocketConfig `protobuf:"bytes,6,opt,name=socket_settings,json=socketSettings,proto3" json:"socket_settings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StreamConfig) Reset() { @@ -352,6 +353,20 @@ func (x *StreamConfig) GetSecuritySettings() []*serial.TypedMessage { return nil } +func (x *StreamConfig) GetUdpmasks() []*serial.TypedMessage { + if x != nil { + return x.Udpmasks + } + return nil +} + +func (x *StreamConfig) GetTcpmasks() []*serial.TypedMessage { + if x != nil { + return x.Tcpmasks + } + return nil +} + func (x *StreamConfig) GetSocketSettings() *SocketConfig { if x != nil { return x.SocketSettings @@ -360,12 +375,11 @@ func (x *StreamConfig) GetSocketSettings() *SocketConfig { } type ProxyConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` - TransportLayerProxy bool `protobuf:"varint,2,opt,name=transportLayerProxy,proto3" json:"transportLayerProxy,omitempty"` + 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() { @@ -413,16 +427,15 @@ func (x *ProxyConfig) GetTransportLayerProxy() bool { } type CustomSockopt struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + System string `protobuf:"bytes,1,opt,name=system,proto3" json:"system,omitempty"` + Network string `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"` + Level string `protobuf:"bytes,3,opt,name=level,proto3" json:"level,omitempty"` + Opt string `protobuf:"bytes,4,opt,name=opt,proto3" json:"opt,omitempty"` + Value string `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"` + Type string `protobuf:"bytes,6,opt,name=type,proto3" json:"type,omitempty"` unknownFields protoimpl.UnknownFields - - System string `protobuf:"bytes,1,opt,name=system,proto3" json:"system,omitempty"` - Network string `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"` - Level string `protobuf:"bytes,3,opt,name=level,proto3" json:"level,omitempty"` - Opt string `protobuf:"bytes,4,opt,name=opt,proto3" json:"opt,omitempty"` - Value string `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"` - Type string `protobuf:"bytes,6,opt,name=type,proto3" json:"type,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CustomSockopt) Reset() { @@ -499,10 +512,7 @@ func (x *CustomSockopt) GetType() string { // SocketConfig is options to be applied on network sockets. type SocketConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Mark of the connection. If non-zero, the value will be set to SO_MARK. Mark int32 `protobuf:"varint,1,opt,name=mark,proto3" json:"mark,omitempty"` // TFO is the state of TFO settings. @@ -531,6 +541,8 @@ type SocketConfig struct { 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"` TrustedXForwardedFor []string `protobuf:"bytes,23,rep,name=trusted_x_forwarded_for,json=trustedXForwardedFor,proto3" json:"trusted_x_forwarded_for,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SocketConfig) Reset() { @@ -725,14 +737,13 @@ func (x *SocketConfig) GetTrustedXForwardedFor() []string { } type HappyEyeballsConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - PrioritizeIpv6 bool `protobuf:"varint,1,opt,name=prioritize_ipv6,json=prioritizeIpv6,proto3" json:"prioritize_ipv6,omitempty"` - Interleave uint32 `protobuf:"varint,2,opt,name=interleave,proto3" json:"interleave,omitempty"` - TryDelayMs uint64 `protobuf:"varint,3,opt,name=try_delayMs,json=tryDelayMs,proto3" json:"try_delayMs,omitempty"` - MaxConcurrentTry uint32 `protobuf:"varint,4,opt,name=max_concurrent_try,json=maxConcurrentTry,proto3" json:"max_concurrent_try,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + PrioritizeIpv6 bool `protobuf:"varint,1,opt,name=prioritize_ipv6,json=prioritizeIpv6,proto3" json:"prioritize_ipv6,omitempty"` + Interleave uint32 `protobuf:"varint,2,opt,name=interleave,proto3" json:"interleave,omitempty"` + TryDelayMs uint64 `protobuf:"varint,3,opt,name=try_delayMs,json=tryDelayMs,proto3" json:"try_delayMs,omitempty"` + MaxConcurrentTry uint32 `protobuf:"varint,4,opt,name=max_concurrent_try,json=maxConcurrentTry,proto3" json:"max_concurrent_try,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *HappyEyeballsConfig) Reset() { @@ -795,184 +806,106 @@ func (x *HappyEyeballsConfig) GetMaxConcurrentTry() uint32 { var File_transport_internet_config_proto protoreflect.FileDescriptor -var file_transport_internet_config_proto_rawDesc = []byte{ - 0x0a, 0x1f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x17, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, - 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x1a, 0x21, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x18, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74, 0x2f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x74, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x6e, 0x73, - 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x3c, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x9b, 0x03, - 0x0a, 0x0c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x35, - 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, - 0x74, 0x2e, 0x49, 0x50, 0x4f, 0x72, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x07, 0x61, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x57, - 0x0a, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x74, - 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 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, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x53, - 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x63, 0x75, 0x72, - 0x69, 0x74, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x4d, 0x0a, 0x11, - 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, - 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, - 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x10, 0x73, 0x65, 0x63, 0x75, 0x72, - 0x69, 0x74, 0x79, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x4e, 0x0a, 0x0f, 0x73, - 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 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, 0x53, - 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x73, 0x6f, 0x63, - 0x6b, 0x65, 0x74, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x51, 0x0a, 0x0b, 0x50, - 0x72, 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, - 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x30, 0x0a, 0x13, - 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x50, 0x72, - 0x6f, 0x78, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x74, 0x72, 0x61, 0x6e, 0x73, - 0x70, 0x6f, 0x72, 0x74, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x22, 0x93, - 0x01, 0x0a, 0x0d, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x53, 0x6f, 0x63, 0x6b, 0x6f, 0x70, 0x74, - 0x12, 0x16, 0x0a, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x18, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x6f, 0x70, 0x74, 0x18, - 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, 0x89, 0x09, 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, - 0x70, 0x72, 0x6f, 0x78, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 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, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x2e, 0x54, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x06, 0x74, - 0x70, 0x72, 0x6f, 0x78, 0x79, 0x12, 0x41, 0x0a, 0x1d, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, - 0x5f, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x5f, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x65, - 0x63, 0x65, 0x69, 0x76, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x65, 0x73, - 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x69, 0x6e, 0x64, - 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, - 0x62, 0x69, 0x6e, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x62, - 0x69, 0x6e, 0x64, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, - 0x62, 0x69, 0x6e, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x61, 0x63, 0x63, 0x65, - 0x70, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x50, - 0x72, 0x6f, 0x78, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x50, 0x0a, 0x0f, - 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 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, - 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0e, - 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x21, - 0x0a, 0x0c, 0x64, 0x69, 0x61, 0x6c, 0x65, 0x72, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x6c, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x78, - 0x79, 0x12, 0x35, 0x0a, 0x17, 0x74, 0x63, 0x70, 0x5f, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x61, 0x6c, - 0x69, 0x76, 0x65, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x14, 0x74, 0x63, 0x70, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, - 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x13, 0x74, 0x63, 0x70, 0x5f, - 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x5f, 0x69, 0x64, 0x6c, 0x65, 0x18, - 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x74, 0x63, 0x70, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, - 0x69, 0x76, 0x65, 0x49, 0x64, 0x6c, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x63, 0x70, 0x5f, 0x63, - 0x6f, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x74, 0x63, 0x70, 0x43, 0x6f, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, - 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, - 0x76, 0x36, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x76, 0x36, - 0x6f, 0x6e, 0x6c, 0x79, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x63, 0x70, 0x5f, 0x77, 0x69, 0x6e, 0x64, - 0x6f, 0x77, 0x5f, 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, - 0x74, 0x63, 0x70, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x43, 0x6c, 0x61, 0x6d, 0x70, 0x12, 0x28, - 0x0a, 0x10, 0x74, 0x63, 0x70, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6f, - 0x75, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x74, 0x63, 0x70, 0x55, 0x73, 0x65, - 0x72, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x1e, 0x0a, 0x0b, 0x74, 0x63, 0x70, 0x5f, - 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x65, 0x67, 0x18, 0x11, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, - 0x63, 0x70, 0x4d, 0x61, 0x78, 0x53, 0x65, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x65, 0x6e, 0x65, - 0x74, 0x72, 0x61, 0x74, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x70, 0x65, 0x6e, - 0x65, 0x74, 0x72, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x6d, 0x70, - 0x74, 0x63, 0x70, 0x18, 0x13, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x74, 0x63, 0x70, 0x4d, 0x70, - 0x74, 0x63, 0x70, 0x12, 0x4c, 0x0a, 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x53, 0x6f, 0x63, - 0x6b, 0x6f, 0x70, 0x74, 0x18, 0x14, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 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, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x53, 0x6f, 0x63, 0x6b, 0x6f, - 0x70, 0x74, 0x52, 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x53, 0x6f, 0x63, 0x6b, 0x6f, 0x70, - 0x74, 0x12, 0x60, 0x0a, 0x15, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x70, 0x6f, 0x72, - 0x74, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0e, - 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, 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, 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, 0x12, 0x35, 0x0a, 0x17, 0x74, 0x72, 0x75, 0x73, - 0x74, 0x65, 0x64, 0x5f, 0x78, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x5f, - 0x66, 0x6f, 0x72, 0x18, 0x17, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x74, 0x72, 0x75, 0x73, 0x74, - 0x65, 0x64, 0x58, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x46, 0x6f, 0x72, 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, 0xad, 0x01, 0x0a, 0x13, 0x48, 0x61, 0x70, 0x70, 0x79, 0x45, 0x79, 0x65, 0x62, 0x61, 0x6c, - 0x6c, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x6f, - 0x72, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x18, 0x01, 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, - 0x02, 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, 0x03, 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, 0x04, 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, -} +const file_transport_internet_config_proto_rawDesc = "" + + "\n" + + "\x1ftransport/internet/config.proto\x12\x17xray.transport.internet\x1a!common/serial/typed_message.proto\x1a\x18common/net/address.proto\"t\n" + + "\x0fTransportConfig\x12#\n" + + "\rprotocol_name\x18\x03 \x01(\tR\fprotocolName\x12<\n" + + "\bsettings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\bsettings\"\x97\x04\n" + + "\fStreamConfig\x125\n" + + "\aaddress\x18\b \x01(\v2\x1b.xray.common.net.IPOrDomainR\aaddress\x12\x12\n" + + "\x04port\x18\t \x01(\rR\x04port\x12#\n" + + "\rprotocol_name\x18\x05 \x01(\tR\fprotocolName\x12W\n" + + "\x12transport_settings\x18\x02 \x03(\v2(.xray.transport.internet.TransportConfigR\x11transportSettings\x12#\n" + + "\rsecurity_type\x18\x03 \x01(\tR\fsecurityType\x12M\n" + + "\x11security_settings\x18\x04 \x03(\v2 .xray.common.serial.TypedMessageR\x10securitySettings\x12<\n" + + "\budpmasks\x18\n" + + " \x03(\v2 .xray.common.serial.TypedMessageR\budpmasks\x12<\n" + + "\btcpmasks\x18\v \x03(\v2 .xray.common.serial.TypedMessageR\btcpmasks\x12N\n" + + "\x0fsocket_settings\x18\x06 \x01(\v2%.xray.transport.internet.SocketConfigR\x0esocketSettings\"Q\n" + + "\vProxyConfig\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x120\n" + + "\x13transportLayerProxy\x18\x02 \x01(\bR\x13transportLayerProxy\"\x93\x01\n" + + "\rCustomSockopt\x12\x16\n" + + "\x06system\x18\x01 \x01(\tR\x06system\x12\x18\n" + + "\anetwork\x18\x02 \x01(\tR\anetwork\x12\x14\n" + + "\x05level\x18\x03 \x01(\tR\x05level\x12\x10\n" + + "\x03opt\x18\x04 \x01(\tR\x03opt\x12\x14\n" + + "\x05value\x18\x05 \x01(\tR\x05value\x12\x12\n" + + "\x04type\x18\x06 \x01(\tR\x04type\"\x89\t\n" + + "\fSocketConfig\x12\x12\n" + + "\x04mark\x18\x01 \x01(\x05R\x04mark\x12\x10\n" + + "\x03tfo\x18\x02 \x01(\x05R\x03tfo\x12H\n" + + "\x06tproxy\x18\x03 \x01(\x0e20.xray.transport.internet.SocketConfig.TProxyModeR\x06tproxy\x12A\n" + + "\x1dreceive_original_dest_address\x18\x04 \x01(\bR\x1areceiveOriginalDestAddress\x12!\n" + + "\fbind_address\x18\x05 \x01(\fR\vbindAddress\x12\x1b\n" + + "\tbind_port\x18\x06 \x01(\rR\bbindPort\x122\n" + + "\x15accept_proxy_protocol\x18\a \x01(\bR\x13acceptProxyProtocol\x12P\n" + + "\x0fdomain_strategy\x18\b \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0edomainStrategy\x12!\n" + + "\fdialer_proxy\x18\t \x01(\tR\vdialerProxy\x125\n" + + "\x17tcp_keep_alive_interval\x18\n" + + " \x01(\x05R\x14tcpKeepAliveInterval\x12-\n" + + "\x13tcp_keep_alive_idle\x18\v \x01(\x05R\x10tcpKeepAliveIdle\x12%\n" + + "\x0etcp_congestion\x18\f \x01(\tR\rtcpCongestion\x12\x1c\n" + + "\tinterface\x18\r \x01(\tR\tinterface\x12\x16\n" + + "\x06v6only\x18\x0e \x01(\bR\x06v6only\x12(\n" + + "\x10tcp_window_clamp\x18\x0f \x01(\x05R\x0etcpWindowClamp\x12(\n" + + "\x10tcp_user_timeout\x18\x10 \x01(\x05R\x0etcpUserTimeout\x12\x1e\n" + + "\vtcp_max_seg\x18\x11 \x01(\x05R\ttcpMaxSeg\x12\x1c\n" + + "\tpenetrate\x18\x12 \x01(\bR\tpenetrate\x12\x1b\n" + + "\ttcp_mptcp\x18\x13 \x01(\bR\btcpMptcp\x12L\n" + + "\rcustomSockopt\x18\x14 \x03(\v2&.xray.transport.internet.CustomSockoptR\rcustomSockopt\x12`\n" + + "\x15address_port_strategy\x18\x15 \x01(\x0e2,.xray.transport.internet.AddressPortStrategyR\x13addressPortStrategy\x12S\n" + + "\x0ehappy_eyeballs\x18\x16 \x01(\v2,.xray.transport.internet.HappyEyeballsConfigR\rhappyEyeballs\x125\n" + + "\x17trusted_x_forwarded_for\x18\x17 \x03(\tR\x14trustedXForwardedFor\"/\n" + + "\n" + + "TProxyMode\x12\a\n" + + "\x03Off\x10\x00\x12\n" + + "\n" + + "\x06TProxy\x10\x01\x12\f\n" + + "\bRedirect\x10\x02\"\xad\x01\n" + + "\x13HappyEyeballsConfig\x12'\n" + + "\x0fprioritize_ipv6\x18\x01 \x01(\bR\x0eprioritizeIpv6\x12\x1e\n" + + "\n" + + "interleave\x18\x02 \x01(\rR\n" + + "interleave\x12\x1f\n" + + "\vtry_delayMs\x18\x03 \x01(\x04R\n" + + "tryDelayMs\x12,\n" + + "\x12max_concurrent_try\x18\x04 \x01(\rR\x10maxConcurrentTry*\xa9\x01\n" + + "\x0eDomainStrategy\x12\t\n" + + "\x05AS_IS\x10\x00\x12\n" + + "\n" + + "\x06USE_IP\x10\x01\x12\v\n" + + "\aUSE_IP4\x10\x02\x12\v\n" + + "\aUSE_IP6\x10\x03\x12\f\n" + + "\bUSE_IP46\x10\x04\x12\f\n" + + "\bUSE_IP64\x10\x05\x12\f\n" + + "\bFORCE_IP\x10\x06\x12\r\n" + + "\tFORCE_IP4\x10\a\x12\r\n" + + "\tFORCE_IP6\x10\b\x12\x0e\n" + + "\n" + + "FORCE_IP46\x10\t\x12\x0e\n" + + "\n" + + "FORCE_IP64\x10\n" + + "*\x97\x01\n" + + "\x13AddressPortStrategy\x12\b\n" + + "\x04None\x10\x00\x12\x0f\n" + + "\vSrvPortOnly\x10\x01\x12\x12\n" + + "\x0eSrvAddressOnly\x10\x02\x12\x15\n" + + "\x11SrvPortAndAddress\x10\x03\x12\x0f\n" + + "\vTxtPortOnly\x10\x04\x12\x12\n" + + "\x0eTxtAddressOnly\x10\x05\x12\x15\n" + + "\x11TxtPortAndAddress\x10\x06Bg\n" + + "\x1bcom.xray.transport.internetP\x01Z,github.com/xtls/xray-core/transport/internet\xaa\x02\x17Xray.Transport.Internetb\x06proto3" var ( file_transport_internet_config_proto_rawDescOnce sync.Once - file_transport_internet_config_proto_rawDescData = file_transport_internet_config_proto_rawDesc + file_transport_internet_config_proto_rawDescData []byte ) func file_transport_internet_config_proto_rawDescGZIP() []byte { file_transport_internet_config_proto_rawDescOnce.Do(func() { - file_transport_internet_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_config_proto_rawDescData) + file_transport_internet_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_config_proto_rawDesc), len(file_transport_internet_config_proto_rawDesc))) }) return file_transport_internet_config_proto_rawDescData } @@ -997,17 +930,19 @@ var file_transport_internet_config_proto_depIdxs = []int32{ 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 + 9, // 4: xray.transport.internet.StreamConfig.udpmasks:type_name -> xray.common.serial.TypedMessage + 9, // 5: xray.transport.internet.StreamConfig.tcpmasks:type_name -> xray.common.serial.TypedMessage + 7, // 6: xray.transport.internet.StreamConfig.socket_settings:type_name -> xray.transport.internet.SocketConfig + 2, // 7: xray.transport.internet.SocketConfig.tproxy:type_name -> xray.transport.internet.SocketConfig.TProxyMode + 0, // 8: xray.transport.internet.SocketConfig.domain_strategy:type_name -> xray.transport.internet.DomainStrategy + 6, // 9: xray.transport.internet.SocketConfig.customSockopt:type_name -> xray.transport.internet.CustomSockopt + 1, // 10: xray.transport.internet.SocketConfig.address_port_strategy:type_name -> xray.transport.internet.AddressPortStrategy + 8, // 11: xray.transport.internet.SocketConfig.happy_eyeballs:type_name -> xray.transport.internet.HappyEyeballsConfig + 12, // [12:12] is the sub-list for method output_type + 12, // [12:12] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_transport_internet_config_proto_init() } @@ -1019,7 +954,7 @@ func file_transport_internet_config_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: 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, NumMessages: 6, NumExtensions: 0, @@ -1031,7 +966,6 @@ func file_transport_internet_config_proto_init() { MessageInfos: file_transport_internet_config_proto_msgTypes, }.Build() File_transport_internet_config_proto = out.File - file_transport_internet_config_proto_rawDesc = nil file_transport_internet_config_proto_goTypes = nil file_transport_internet_config_proto_depIdxs = nil } diff --git a/transport/internet/config.proto b/transport/internet/config.proto index e1655fdd..8b1bb23e 100644 --- a/transport/internet/config.proto +++ b/transport/internet/config.proto @@ -56,6 +56,9 @@ message StreamConfig { // Transport security settings. They can be either TLS or REALITY. repeated xray.common.serial.TypedMessage security_settings = 4; + repeated xray.common.serial.TypedMessage udpmasks = 10; + repeated xray.common.serial.TypedMessage tcpmasks = 11; + SocketConfig socket_settings = 6; } diff --git a/transport/internet/finalmask/finalmask.go b/transport/internet/finalmask/finalmask.go new file mode 100644 index 00000000..7ce4d4f3 --- /dev/null +++ b/transport/internet/finalmask/finalmask.go @@ -0,0 +1,127 @@ +package finalmask + +import ( + "net" +) + +type Udpmask interface { + UDP() + + WrapConnClient(net.Conn) (net.Conn, error) + WrapConnServer(net.Conn) (net.Conn, error) + + WrapPacketConnClient(net.PacketConn) (net.PacketConn, error) + WrapPacketConnServer(net.PacketConn) (net.PacketConn, error) + + Size() int + Serialize([]byte) +} + +type UdpmaskManager struct { + udpmasks []Udpmask +} + +func NewUdpmaskManager(udpmasks []Udpmask) *UdpmaskManager { + return &UdpmaskManager{ + udpmasks: udpmasks, + } +} + +func (m *UdpmaskManager) WrapConnClient(raw net.Conn) (net.Conn, error) { + var err error + for _, mask := range m.udpmasks { + raw, err = mask.WrapConnClient(raw) + if err != nil { + return nil, err + } + } + return raw, nil +} + +func (m *UdpmaskManager) WrapConnServer(raw net.Conn) (net.Conn, error) { + var err error + for _, mask := range m.udpmasks { + raw, err = mask.WrapConnServer(raw) + if err != nil { + return nil, err + } + } + return raw, nil +} + +func (m *UdpmaskManager) WrapPacketConnClient(raw net.PacketConn) (net.PacketConn, error) { + var err error + for _, mask := range m.udpmasks { + raw, err = mask.WrapPacketConnClient(raw) + if err != nil { + return nil, err + } + } + return raw, nil +} + +func (m *UdpmaskManager) WrapPacketConnServer(raw net.PacketConn) (net.PacketConn, error) { + var err error + for _, mask := range m.udpmasks { + raw, err = mask.WrapPacketConnServer(raw) + if err != nil { + return nil, err + } + } + return raw, nil +} + +func (m *UdpmaskManager) Size() int { + size := 0 + for _, mask := range m.udpmasks { + size += mask.Size() + } + return size +} + +func (m *UdpmaskManager) Serialize(b []byte) { + index := 0 + for _, mask := range m.udpmasks { + mask.Serialize(b[index:]) + index += mask.Size() + } +} + +type Tcpmask interface { + TCP() + + WrapConnClient(net.Conn) (net.Conn, error) + WrapConnServer(net.Conn) (net.Conn, error) +} + +type TcpmaskManager struct { + tcpmasks []Tcpmask +} + +func NewTcpmaskManager(tcpmasks []Tcpmask) *TcpmaskManager { + return &TcpmaskManager{ + tcpmasks: tcpmasks, + } +} + +func (m *TcpmaskManager) WrapConnClient(raw net.Conn) (net.Conn, error) { + var err error + for _, mask := range m.tcpmasks { + raw, err = mask.WrapConnClient(raw) + if err != nil { + return nil, err + } + } + return raw, nil +} + +func (m *TcpmaskManager) WrapConnServer(raw net.Conn) (net.Conn, error) { + var err error + for _, mask := range m.tcpmasks { + raw, err = mask.WrapConnServer(raw) + if err != nil { + return nil, err + } + } + return raw, nil +} diff --git a/transport/internet/finalmask/salamander/config.go b/transport/internet/finalmask/salamander/config.go new file mode 100644 index 00000000..aca35b4d --- /dev/null +++ b/transport/internet/finalmask/salamander/config.go @@ -0,0 +1,42 @@ +package salamander + +import ( + "net" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/transport/internet/finalmask/salamander/obfs" +) + +func (c *Config) UDP() { +} + +func (c *Config) WrapConnClient(raw net.Conn) (net.Conn, error) { + return raw, nil +} + +func (c *Config) WrapConnServer(raw net.Conn) (net.Conn, error) { + return raw, nil +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn) (net.PacketConn, error) { + ob, err := obfs.NewSalamanderObfuscator([]byte(c.Password)) + if err != nil { + return nil, errors.New("salamander err").Base(err) + } + return obfs.WrapPacketConn(raw, ob), nil +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn) (net.PacketConn, error) { + ob, err := obfs.NewSalamanderObfuscator([]byte(c.Password)) + if err != nil { + return nil, errors.New("salamander err").Base(err) + } + return obfs.WrapPacketConn(raw, ob), nil +} + +func (c *Config) Size() int { + return 0 +} + +func (c *Config) Serialize([]byte) { +} diff --git a/transport/internet/finalmask/salamander/config.pb.go b/transport/internet/finalmask/salamander/config.pb.go new file mode 100644 index 00000000..7d572b42 --- /dev/null +++ b/transport/internet/finalmask/salamander/config.pb.go @@ -0,0 +1,123 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v6.33.1 +// source: transport/internet/udpmask/salamander/config.proto + +package salamander + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_udpmask_salamander_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_udpmask_salamander_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 Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_udpmask_salamander_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +var File_transport_internet_udpmask_salamander_config_proto protoreflect.FileDescriptor + +const file_transport_internet_udpmask_salamander_config_proto_rawDesc = "" + + "\n" + + "2transport/internet/udpmask/salamander/config.proto\x12*xray.transport.internet.udpmask.salamander\"$\n" + + "\x06Config\x12\x1a\n" + + "\bpassword\x18\x01 \x01(\tR\bpasswordB\xa0\x01\n" + + ".com.xray.transport.internet.udpmask.salamanderP\x01Z?github.com/xtls/xray-core/transport/internet/udpmask/salamander\xaa\x02*Xray.Transport.Internet.Udpmask.Salamanderb\x06proto3" + +var ( + file_transport_internet_udpmask_salamander_config_proto_rawDescOnce sync.Once + file_transport_internet_udpmask_salamander_config_proto_rawDescData []byte +) + +func file_transport_internet_udpmask_salamander_config_proto_rawDescGZIP() []byte { + file_transport_internet_udpmask_salamander_config_proto_rawDescOnce.Do(func() { + file_transport_internet_udpmask_salamander_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_udpmask_salamander_config_proto_rawDesc), len(file_transport_internet_udpmask_salamander_config_proto_rawDesc))) + }) + return file_transport_internet_udpmask_salamander_config_proto_rawDescData +} + +var file_transport_internet_udpmask_salamander_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_udpmask_salamander_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.udpmask.salamander.Config +} +var file_transport_internet_udpmask_salamander_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_udpmask_salamander_config_proto_init() } +func file_transport_internet_udpmask_salamander_config_proto_init() { + if File_transport_internet_udpmask_salamander_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_udpmask_salamander_config_proto_rawDesc), len(file_transport_internet_udpmask_salamander_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_udpmask_salamander_config_proto_goTypes, + DependencyIndexes: file_transport_internet_udpmask_salamander_config_proto_depIdxs, + MessageInfos: file_transport_internet_udpmask_salamander_config_proto_msgTypes, + }.Build() + File_transport_internet_udpmask_salamander_config_proto = out.File + file_transport_internet_udpmask_salamander_config_proto_goTypes = nil + file_transport_internet_udpmask_salamander_config_proto_depIdxs = nil +} diff --git a/transport/internet/finalmask/salamander/config.proto b/transport/internet/finalmask/salamander/config.proto new file mode 100644 index 00000000..bed943ab --- /dev/null +++ b/transport/internet/finalmask/salamander/config.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package xray.transport.internet.udpmask.salamander; +option csharp_namespace = "Xray.Transport.Internet.Udpmask.Salamander"; +option go_package = "github.com/xtls/xray-core/transport/internet/udpmask/salamander"; +option java_package = "com.xray.transport.internet.udpmask.salamander"; +option java_multiple_files = true; + +message Config { + string password = 1; +} + diff --git a/transport/internet/finalmask/salamander/obfs/conn.go b/transport/internet/finalmask/salamander/obfs/conn.go new file mode 100644 index 00000000..6b97592e --- /dev/null +++ b/transport/internet/finalmask/salamander/obfs/conn.go @@ -0,0 +1,121 @@ +package obfs + +import ( + "net" + "sync" + "syscall" + "time" +) + +const udpBufferSize = 2048 // QUIC packets are at most 1500 bytes long, so 2k should be more than enough + +// Obfuscator is the interface that wraps the Obfuscate and Deobfuscate methods. +// Both methods return the number of bytes written to out. +// If a packet is not valid, the methods should return 0. +type Obfuscator interface { + Obfuscate(in, out []byte) int + Deobfuscate(in, out []byte) int +} + +var _ net.PacketConn = (*obfsPacketConn)(nil) + +type obfsPacketConn struct { + Conn net.PacketConn + Obfs Obfuscator + + readBuf []byte + readMutex sync.Mutex + writeBuf []byte + writeMutex sync.Mutex +} + +// obfsPacketConnUDP is a special case of obfsPacketConn that uses a UDPConn +// as the underlying connection. We pass additional methods to quic-go to +// enable UDP-specific optimizations. +type obfsPacketConnUDP struct { + *obfsPacketConn + UDPConn *net.UDPConn +} + +// WrapPacketConn enables obfuscation on a net.PacketConn. +// The obfuscation is transparent to the caller - the n bytes returned by +// ReadFrom and WriteTo are the number of original bytes, not after +// obfuscation/deobfuscation. +func WrapPacketConn(conn net.PacketConn, obfs Obfuscator) net.PacketConn { + opc := &obfsPacketConn{ + Conn: conn, + Obfs: obfs, + readBuf: make([]byte, udpBufferSize), + writeBuf: make([]byte, udpBufferSize), + } + if udpConn, ok := conn.(*net.UDPConn); ok { + return &obfsPacketConnUDP{ + obfsPacketConn: opc, + UDPConn: udpConn, + } + } else { + return opc + } +} + +func (c *obfsPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + for { + c.readMutex.Lock() + n, addr, err = c.Conn.ReadFrom(c.readBuf) + if n <= 0 { + c.readMutex.Unlock() + return n, addr, err + } + n = c.Obfs.Deobfuscate(c.readBuf[:n], p) + c.readMutex.Unlock() + if n > 0 || err != nil { + return n, addr, err + } + // Invalid packet, try again + } +} + +func (c *obfsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.writeMutex.Lock() + nn := c.Obfs.Obfuscate(p, c.writeBuf) + _, err = c.Conn.WriteTo(c.writeBuf[:nn], addr) + c.writeMutex.Unlock() + if err == nil { + n = len(p) + } + return n, err +} + +func (c *obfsPacketConn) Close() error { + return c.Conn.Close() +} + +func (c *obfsPacketConn) LocalAddr() net.Addr { + return c.Conn.LocalAddr() +} + +func (c *obfsPacketConn) SetDeadline(t time.Time) error { + return c.Conn.SetDeadline(t) +} + +func (c *obfsPacketConn) SetReadDeadline(t time.Time) error { + return c.Conn.SetReadDeadline(t) +} + +func (c *obfsPacketConn) SetWriteDeadline(t time.Time) error { + return c.Conn.SetWriteDeadline(t) +} + +// UDP-specific methods below + +func (c *obfsPacketConnUDP) SetReadBuffer(bytes int) error { + return c.UDPConn.SetReadBuffer(bytes) +} + +func (c *obfsPacketConnUDP) SetWriteBuffer(bytes int) error { + return c.UDPConn.SetWriteBuffer(bytes) +} + +func (c *obfsPacketConnUDP) SyscallConn() (syscall.RawConn, error) { + return c.UDPConn.SyscallConn() +} diff --git a/transport/internet/finalmask/salamander/obfs/salamander.go b/transport/internet/finalmask/salamander/obfs/salamander.go new file mode 100644 index 00000000..50a3ce26 --- /dev/null +++ b/transport/internet/finalmask/salamander/obfs/salamander.go @@ -0,0 +1,71 @@ +package obfs + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "golang.org/x/crypto/blake2b" +) + +const ( + smPSKMinLen = 4 + smSaltLen = 8 + smKeyLen = blake2b.Size256 +) + +var _ Obfuscator = (*SalamanderObfuscator)(nil) + +var ErrPSKTooShort = fmt.Errorf("PSK must be at least %d bytes", smPSKMinLen) + +// SalamanderObfuscator is an obfuscator that obfuscates each packet with +// the BLAKE2b-256 hash of a pre-shared key combined with a random salt. +// Packet format: [8-byte salt][payload] +type SalamanderObfuscator struct { + PSK []byte + RandSrc *rand.Rand + + lk sync.Mutex +} + +func NewSalamanderObfuscator(psk []byte) (*SalamanderObfuscator, error) { + if len(psk) < smPSKMinLen { + return nil, ErrPSKTooShort + } + return &SalamanderObfuscator{ + PSK: psk, + RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())), + }, nil +} + +func (o *SalamanderObfuscator) Obfuscate(in, out []byte) int { + outLen := len(in) + smSaltLen + if len(out) < outLen { + return 0 + } + o.lk.Lock() + _, _ = o.RandSrc.Read(out[:smSaltLen]) + o.lk.Unlock() + key := o.key(out[:smSaltLen]) + for i, c := range in { + out[i+smSaltLen] = c ^ key[i%smKeyLen] + } + return outLen +} + +func (o *SalamanderObfuscator) Deobfuscate(in, out []byte) int { + outLen := len(in) - smSaltLen + if outLen <= 0 || len(out) < outLen { + return 0 + } + key := o.key(in[:smSaltLen]) + for i, c := range in[smSaltLen:] { + out[i] = c ^ key[i%smKeyLen] + } + return outLen +} + +func (o *SalamanderObfuscator) key(salt []byte) [smKeyLen]byte { + return blake2b.Sum256(append(o.PSK, salt...)) +} diff --git a/transport/internet/finalmask/salamander/obfs/salamander_test.go b/transport/internet/finalmask/salamander/obfs/salamander_test.go new file mode 100644 index 00000000..85eafdcc --- /dev/null +++ b/transport/internet/finalmask/salamander/obfs/salamander_test.go @@ -0,0 +1,45 @@ +package obfs + +import ( + "crypto/rand" + "testing" + + "github.com/stretchr/testify/assert" +) + +func BenchmarkSalamanderObfuscator_Obfuscate(b *testing.B) { + o, _ := NewSalamanderObfuscator([]byte("average_password")) + in := make([]byte, 1200) + _, _ = rand.Read(in) + out := make([]byte, 2048) + b.ResetTimer() + for i := 0; i < b.N; i++ { + o.Obfuscate(in, out) + } +} + +func BenchmarkSalamanderObfuscator_Deobfuscate(b *testing.B) { + o, _ := NewSalamanderObfuscator([]byte("average_password")) + in := make([]byte, 1200) + _, _ = rand.Read(in) + out := make([]byte, 2048) + b.ResetTimer() + for i := 0; i < b.N; i++ { + o.Deobfuscate(in, out) + } +} + +func TestSalamanderObfuscator(t *testing.T) { + o, _ := NewSalamanderObfuscator([]byte("average_password")) + in := make([]byte, 1200) + oOut := make([]byte, 2048) + dOut := make([]byte, 2048) + for i := 0; i < 1000; i++ { + _, _ = rand.Read(in) + n := o.Obfuscate(in, oOut) + assert.Equal(t, len(in)+smSaltLen, n) + n = o.Deobfuscate(oOut[:n], dOut) + assert.Equal(t, len(in), n) + assert.Equal(t, in, dOut[:n]) + } +} diff --git a/transport/internet/hysteria/config.go b/transport/internet/hysteria/config.go new file mode 100644 index 00000000..fd7a4bb4 --- /dev/null +++ b/transport/internet/hysteria/config.go @@ -0,0 +1,47 @@ +package hysteria + +import ( + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/hysteria/padding" +) + +const ( + closeErrCodeOK = 0x100 // HTTP3 ErrCodeNoError + closeErrCodeProtocolError = 0x101 // HTTP3 ErrCodeGeneralProtocolError + + MaxDatagramFrameSize = 1200 + + URLHost = "hysteria" + URLPath = "/auth" + + RequestHeaderAuth = "Hysteria-Auth" + ResponseHeaderUDPEnabled = "Hysteria-UDP" + CommonHeaderCCRX = "Hysteria-CC-RX" + CommonHeaderPadding = "Hysteria-Padding" + + StatusAuthOK = 233 + + udpMessageChanSize = 1024 +) + +var ( + authRequestPadding = padding.Padding{Min: 256, Max: 2048} + // authResponsePadding = padding.Padding{Min: 256, Max: 2048} +) + +type Status int + +const ( + StatusUnknown Status = iota + StatusActive + StatusInactive +) + +const protocolName = "hysteria" + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/transport/internet/hysteria/config.pb.go b/transport/internet/hysteria/config.pb.go new file mode 100644 index 00000000..5e453c30 --- /dev/null +++ b/transport/internet/hysteria/config.pb.go @@ -0,0 +1,232 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v6.33.1 +// source: transport/internet/hysteria/config.proto + +package hysteria + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Auth string `protobuf:"bytes,2,opt,name=auth,proto3" json:"auth,omitempty"` + Up uint64 `protobuf:"varint,3,opt,name=up,proto3" json:"up,omitempty"` + Down uint64 `protobuf:"varint,4,opt,name=down,proto3" json:"down,omitempty"` + Ports []uint32 `protobuf:"varint,5,rep,packed,name=ports,proto3" json:"ports,omitempty"` + Interval int64 `protobuf:"varint,6,opt,name=interval,proto3" json:"interval,omitempty"` + InitStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"` + MaxStreamReceiveWindow uint64 `protobuf:"varint,8,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"` + InitConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"` + MaxConnReceiveWindow uint64 `protobuf:"varint,10,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"` + MaxIdleTimeout int64 `protobuf:"varint,11,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"` + KeepAlivePeriod int64 `protobuf:"varint,12,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"` + DisablePathMtuDiscovery bool `protobuf:"varint,13,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_hysteria_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_hysteria_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 Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_hysteria_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Config) GetAuth() string { + if x != nil { + return x.Auth + } + return "" +} + +func (x *Config) GetUp() uint64 { + if x != nil { + return x.Up + } + return 0 +} + +func (x *Config) GetDown() uint64 { + if x != nil { + return x.Down + } + return 0 +} + +func (x *Config) GetPorts() []uint32 { + if x != nil { + return x.Ports + } + return nil +} + +func (x *Config) GetInterval() int64 { + if x != nil { + return x.Interval + } + return 0 +} + +func (x *Config) GetInitStreamReceiveWindow() uint64 { + if x != nil { + return x.InitStreamReceiveWindow + } + return 0 +} + +func (x *Config) GetMaxStreamReceiveWindow() uint64 { + if x != nil { + return x.MaxStreamReceiveWindow + } + return 0 +} + +func (x *Config) GetInitConnReceiveWindow() uint64 { + if x != nil { + return x.InitConnReceiveWindow + } + return 0 +} + +func (x *Config) GetMaxConnReceiveWindow() uint64 { + if x != nil { + return x.MaxConnReceiveWindow + } + return 0 +} + +func (x *Config) GetMaxIdleTimeout() int64 { + if x != nil { + return x.MaxIdleTimeout + } + return 0 +} + +func (x *Config) GetKeepAlivePeriod() int64 { + if x != nil { + return x.KeepAlivePeriod + } + return 0 +} + +func (x *Config) GetDisablePathMtuDiscovery() bool { + if x != nil { + return x.DisablePathMtuDiscovery + } + return false +} + +var File_transport_internet_hysteria_config_proto protoreflect.FileDescriptor + +const file_transport_internet_hysteria_config_proto_rawDesc = "" + + "\n" + + "(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\x87\x04\n" + + "\x06Config\x12\x18\n" + + "\aversion\x18\x01 \x01(\x05R\aversion\x12\x12\n" + + "\x04auth\x18\x02 \x01(\tR\x04auth\x12\x0e\n" + + "\x02up\x18\x03 \x01(\x04R\x02up\x12\x12\n" + + "\x04down\x18\x04 \x01(\x04R\x04down\x12\x14\n" + + "\x05ports\x18\x05 \x03(\rR\x05ports\x12\x1a\n" + + "\binterval\x18\x06 \x01(\x03R\binterval\x12;\n" + + "\x1ainit_stream_receive_window\x18\a \x01(\x04R\x17initStreamReceiveWindow\x129\n" + + "\x19max_stream_receive_window\x18\b \x01(\x04R\x16maxStreamReceiveWindow\x127\n" + + "\x18init_conn_receive_window\x18\t \x01(\x04R\x15initConnReceiveWindow\x125\n" + + "\x17max_conn_receive_window\x18\n" + + " \x01(\x04R\x14maxConnReceiveWindow\x12(\n" + + "\x10max_idle_timeout\x18\v \x01(\x03R\x0emaxIdleTimeout\x12*\n" + + "\x11keep_alive_period\x18\f \x01(\x03R\x0fkeepAlivePeriod\x12;\n" + + "\x1adisable_path_mtu_discovery\x18\r \x01(\bR\x17disablePathMtuDiscoveryB\x82\x01\n" + + "$com.xray.transport.internet.hysteriaP\x01Z5github.com/xtls/xray-core/transport/internet/hysteria\xaa\x02 Xray.Transport.Internet.Hysteriab\x06proto3" + +var ( + file_transport_internet_hysteria_config_proto_rawDescOnce sync.Once + file_transport_internet_hysteria_config_proto_rawDescData []byte +) + +func file_transport_internet_hysteria_config_proto_rawDescGZIP() []byte { + file_transport_internet_hysteria_config_proto_rawDescOnce.Do(func() { + file_transport_internet_hysteria_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_hysteria_config_proto_rawDesc), len(file_transport_internet_hysteria_config_proto_rawDesc))) + }) + return file_transport_internet_hysteria_config_proto_rawDescData +} + +var file_transport_internet_hysteria_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_hysteria_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.hysteria.Config +} +var file_transport_internet_hysteria_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_hysteria_config_proto_init() } +func file_transport_internet_hysteria_config_proto_init() { + if File_transport_internet_hysteria_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_hysteria_config_proto_rawDesc), len(file_transport_internet_hysteria_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_hysteria_config_proto_goTypes, + DependencyIndexes: file_transport_internet_hysteria_config_proto_depIdxs, + MessageInfos: file_transport_internet_hysteria_config_proto_msgTypes, + }.Build() + File_transport_internet_hysteria_config_proto = out.File + file_transport_internet_hysteria_config_proto_goTypes = nil + file_transport_internet_hysteria_config_proto_depIdxs = nil +} diff --git a/transport/internet/hysteria/config.proto b/transport/internet/hysteria/config.proto new file mode 100644 index 00000000..22113338 --- /dev/null +++ b/transport/internet/hysteria/config.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package xray.transport.internet.hysteria; +option csharp_namespace = "Xray.Transport.Internet.Hysteria"; +option go_package = "github.com/xtls/xray-core/transport/internet/hysteria"; +option java_package = "com.xray.transport.internet.hysteria"; +option java_multiple_files = true; + +message Config { + int32 version = 1; + string auth = 2; + uint64 up = 3; + uint64 down = 4; + repeated uint32 ports = 5; + int64 interval = 6; + + uint64 init_stream_receive_window = 7; + uint64 max_stream_receive_window = 8; + uint64 init_conn_receive_window = 9; + uint64 max_conn_receive_window = 10; + int64 max_idle_timeout = 11; + int64 keep_alive_period = 12; + bool disable_path_mtu_discovery = 13; +} + diff --git a/transport/internet/hysteria/congestion/bbr/bandwidth.go b/transport/internet/hysteria/congestion/bbr/bandwidth.go new file mode 100644 index 00000000..52deb249 --- /dev/null +++ b/transport/internet/hysteria/congestion/bbr/bandwidth.go @@ -0,0 +1,27 @@ +package bbr + +import ( + "math" + "time" + + "github.com/apernet/quic-go/congestion" +) + +const ( + infBandwidth = Bandwidth(math.MaxUint64) +) + +// Bandwidth of a connection +type Bandwidth uint64 + +const ( + // BitsPerSecond is 1 bit per second + BitsPerSecond Bandwidth = 1 + // BytesPerSecond is 1 byte per second + BytesPerSecond = 8 * BitsPerSecond +) + +// BandwidthFromDelta calculates the bandwidth from a number of bytes and a time delta +func BandwidthFromDelta(bytes congestion.ByteCount, delta time.Duration) Bandwidth { + return Bandwidth(bytes) * Bandwidth(time.Second) / Bandwidth(delta) * BytesPerSecond +} diff --git a/transport/internet/hysteria/congestion/bbr/bandwidth_sampler.go b/transport/internet/hysteria/congestion/bbr/bandwidth_sampler.go new file mode 100644 index 00000000..a1e22d6f --- /dev/null +++ b/transport/internet/hysteria/congestion/bbr/bandwidth_sampler.go @@ -0,0 +1,874 @@ +package bbr + +import ( + "math" + "time" + + "github.com/apernet/quic-go/congestion" +) + +const ( + infRTT = time.Duration(math.MaxInt64) + defaultConnectionStateMapQueueSize = 256 + defaultCandidatesBufferSize = 256 +) + +type roundTripCount uint64 + +// SendTimeState is a subset of ConnectionStateOnSentPacket which is returned +// to the caller when the packet is acked or lost. +type sendTimeState struct { + // Whether other states in this object is valid. + isValid bool + // Whether the sender is app limited at the time the packet was sent. + // App limited bandwidth sample might be artificially low because the sender + // did not have enough data to send in order to saturate the link. + isAppLimited bool + // Total number of sent bytes at the time the packet was sent. + // Includes the packet itself. + totalBytesSent congestion.ByteCount + // Total number of acked bytes at the time the packet was sent. + totalBytesAcked congestion.ByteCount + // Total number of lost bytes at the time the packet was sent. + totalBytesLost congestion.ByteCount + // Total number of inflight bytes at the time the packet was sent. + // Includes the packet itself. + // It should be equal to |total_bytes_sent| minus the sum of + // |total_bytes_acked|, |total_bytes_lost| and total neutered bytes. + bytesInFlight congestion.ByteCount +} + +func newSendTimeState( + isAppLimited bool, + totalBytesSent congestion.ByteCount, + totalBytesAcked congestion.ByteCount, + totalBytesLost congestion.ByteCount, + bytesInFlight congestion.ByteCount, +) *sendTimeState { + return &sendTimeState{ + isValid: true, + isAppLimited: isAppLimited, + totalBytesSent: totalBytesSent, + totalBytesAcked: totalBytesAcked, + totalBytesLost: totalBytesLost, + bytesInFlight: bytesInFlight, + } +} + +type extraAckedEvent struct { + // The excess bytes acknowlwedged in the time delta for this event. + extraAcked congestion.ByteCount + + // The bytes acknowledged and time delta from the event. + bytesAcked congestion.ByteCount + timeDelta time.Duration + // The round trip of the event. + round roundTripCount +} + +func maxExtraAckedEventFunc(a, b extraAckedEvent) int { + if a.extraAcked > b.extraAcked { + return 1 + } else if a.extraAcked < b.extraAcked { + return -1 + } + return 0 +} + +// BandwidthSample +type bandwidthSample struct { + // The bandwidth at that particular sample. Zero if no valid bandwidth sample + // is available. + bandwidth Bandwidth + // The RTT measurement at this particular sample. Zero if no RTT sample is + // available. Does not correct for delayed ack time. + rtt time.Duration + // |send_rate| is computed from the current packet being acked('P') and an + // earlier packet that is acked before P was sent. + sendRate Bandwidth + // States captured when the packet was sent. + stateAtSend sendTimeState +} + +func newBandwidthSample() *bandwidthSample { + return &bandwidthSample{ + sendRate: infBandwidth, + } +} + +// MaxAckHeightTracker is part of the BandwidthSampler. It is called after every +// ack event to keep track the degree of ack aggregation(a.k.a "ack height"). +type maxAckHeightTracker struct { + // Tracks the maximum number of bytes acked faster than the estimated + // bandwidth. + maxAckHeightFilter *WindowedFilter[extraAckedEvent, roundTripCount] + // The time this aggregation started and the number of bytes acked during it. + aggregationEpochStartTime congestion.Time + aggregationEpochBytes congestion.ByteCount + // The last sent packet number before the current aggregation epoch started. + lastSentPacketNumberBeforeEpoch congestion.PacketNumber + // The number of ack aggregation epochs ever started, including the ongoing + // one. Stats only. + numAckAggregationEpochs uint64 + ackAggregationBandwidthThreshold float64 + startNewAggregationEpochAfterFullRound bool + reduceExtraAckedOnBandwidthIncrease bool +} + +func newMaxAckHeightTracker(windowLength roundTripCount) *maxAckHeightTracker { + return &maxAckHeightTracker{ + maxAckHeightFilter: NewWindowedFilter(windowLength, maxExtraAckedEventFunc), + lastSentPacketNumberBeforeEpoch: invalidPacketNumber, + ackAggregationBandwidthThreshold: 1.0, + } +} + +func (m *maxAckHeightTracker) Get() congestion.ByteCount { + return m.maxAckHeightFilter.GetBest().extraAcked +} + +func (m *maxAckHeightTracker) Update( + bandwidthEstimate Bandwidth, + isNewMaxBandwidth bool, + roundTripCount roundTripCount, + lastSentPacketNumber congestion.PacketNumber, + lastAckedPacketNumber congestion.PacketNumber, + ackTime congestion.Time, + bytesAcked congestion.ByteCount, +) congestion.ByteCount { + forceNewEpoch := false + + if m.reduceExtraAckedOnBandwidthIncrease && isNewMaxBandwidth { + // Save and clear existing entries. + best := m.maxAckHeightFilter.GetBest() + secondBest := m.maxAckHeightFilter.GetSecondBest() + thirdBest := m.maxAckHeightFilter.GetThirdBest() + m.maxAckHeightFilter.Clear() + + // Reinsert the heights into the filter after recalculating. + expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, best.timeDelta) + if expectedBytesAcked < best.bytesAcked { + best.extraAcked = best.bytesAcked - expectedBytesAcked + m.maxAckHeightFilter.Update(best, best.round) + } + expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, secondBest.timeDelta) + if expectedBytesAcked < secondBest.bytesAcked { + secondBest.extraAcked = secondBest.bytesAcked - expectedBytesAcked + m.maxAckHeightFilter.Update(secondBest, secondBest.round) + } + expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, thirdBest.timeDelta) + if expectedBytesAcked < thirdBest.bytesAcked { + thirdBest.extraAcked = thirdBest.bytesAcked - expectedBytesAcked + m.maxAckHeightFilter.Update(thirdBest, thirdBest.round) + } + } + + // If any packet sent after the start of the epoch has been acked, start a new + // epoch. + if m.startNewAggregationEpochAfterFullRound && + m.lastSentPacketNumberBeforeEpoch != invalidPacketNumber && + lastAckedPacketNumber != invalidPacketNumber && + lastAckedPacketNumber > m.lastSentPacketNumberBeforeEpoch { + forceNewEpoch = true + } + if m.aggregationEpochStartTime.IsZero() || forceNewEpoch { + m.aggregationEpochBytes = bytesAcked + m.aggregationEpochStartTime = ackTime + m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber + m.numAckAggregationEpochs++ + return 0 + } + + // Compute how many bytes are expected to be delivered, assuming max bandwidth + // is correct. + aggregationDelta := ackTime.Sub(m.aggregationEpochStartTime) + expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, aggregationDelta) + // Reset the current aggregation epoch as soon as the ack arrival rate is less + // than or equal to the max bandwidth. + if m.aggregationEpochBytes <= congestion.ByteCount(m.ackAggregationBandwidthThreshold*float64(expectedBytesAcked)) { + // Reset to start measuring a new aggregation epoch. + m.aggregationEpochBytes = bytesAcked + m.aggregationEpochStartTime = ackTime + m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber + m.numAckAggregationEpochs++ + return 0 + } + + m.aggregationEpochBytes += bytesAcked + + // Compute how many extra bytes were delivered vs max bandwidth. + extraBytesAcked := m.aggregationEpochBytes - expectedBytesAcked + newEvent := extraAckedEvent{ + extraAcked: extraBytesAcked, + bytesAcked: m.aggregationEpochBytes, + timeDelta: aggregationDelta, + } + m.maxAckHeightFilter.Update(newEvent, roundTripCount) + return extraBytesAcked +} + +func (m *maxAckHeightTracker) SetFilterWindowLength(length roundTripCount) { + m.maxAckHeightFilter.SetWindowLength(length) +} + +func (m *maxAckHeightTracker) Reset(newHeight congestion.ByteCount, newTime roundTripCount) { + newEvent := extraAckedEvent{ + extraAcked: newHeight, + round: newTime, + } + m.maxAckHeightFilter.Reset(newEvent, newTime) +} + +func (m *maxAckHeightTracker) SetAckAggregationBandwidthThreshold(threshold float64) { + m.ackAggregationBandwidthThreshold = threshold +} + +func (m *maxAckHeightTracker) SetStartNewAggregationEpochAfterFullRound(value bool) { + m.startNewAggregationEpochAfterFullRound = value +} + +func (m *maxAckHeightTracker) SetReduceExtraAckedOnBandwidthIncrease(value bool) { + m.reduceExtraAckedOnBandwidthIncrease = value +} + +func (m *maxAckHeightTracker) AckAggregationBandwidthThreshold() float64 { + return m.ackAggregationBandwidthThreshold +} + +func (m *maxAckHeightTracker) NumAckAggregationEpochs() uint64 { + return m.numAckAggregationEpochs +} + +// AckPoint represents a point on the ack line. +type ackPoint struct { + ackTime congestion.Time + totalBytesAcked congestion.ByteCount +} + +// RecentAckPoints maintains the most recent 2 ack points at distinct times. +type recentAckPoints struct { + ackPoints [2]ackPoint +} + +func (r *recentAckPoints) Update(ackTime congestion.Time, totalBytesAcked congestion.ByteCount) { + if ackTime.Before(r.ackPoints[1].ackTime) { + r.ackPoints[1].ackTime = ackTime + } else if ackTime.After(r.ackPoints[1].ackTime) { + r.ackPoints[0] = r.ackPoints[1] + r.ackPoints[1].ackTime = ackTime + } + + r.ackPoints[1].totalBytesAcked = totalBytesAcked +} + +func (r *recentAckPoints) Clear() { + r.ackPoints[0] = ackPoint{} + r.ackPoints[1] = ackPoint{} +} + +func (r *recentAckPoints) MostRecentPoint() *ackPoint { + return &r.ackPoints[1] +} + +func (r *recentAckPoints) LessRecentPoint() *ackPoint { + if r.ackPoints[0].totalBytesAcked != 0 { + return &r.ackPoints[0] + } + + return &r.ackPoints[1] +} + +// ConnectionStateOnSentPacket represents the information about a sent packet +// and the state of the connection at the moment the packet was sent, +// specifically the information about the most recently acknowledged packet at +// that moment. +type connectionStateOnSentPacket struct { + // Time at which the packet is sent. + sentTime congestion.Time + // Size of the packet. + size congestion.ByteCount + // The value of |totalBytesSentAtLastAckedPacket| at the time the + // packet was sent. + totalBytesSentAtLastAckedPacket congestion.ByteCount + // The value of |lastAckedPacketSentTime| at the time the packet was + // sent. + lastAckedPacketSentTime congestion.Time + // The value of |lastAckedPacketAckTime| at the time the packet was + // sent. + lastAckedPacketAckTime congestion.Time + // Send time states that are returned to the congestion controller when the + // packet is acked or lost. + sendTimeState sendTimeState +} + +// Snapshot constructor. Records the current state of the bandwidth +// sampler. +// |bytes_in_flight| is the bytes in flight right after the packet is sent. +func newConnectionStateOnSentPacket( + sentTime congestion.Time, + size congestion.ByteCount, + bytesInFlight congestion.ByteCount, + sampler *bandwidthSampler, +) *connectionStateOnSentPacket { + return &connectionStateOnSentPacket{ + sentTime: sentTime, + size: size, + totalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket, + lastAckedPacketSentTime: sampler.lastAckedPacketSentTime, + lastAckedPacketAckTime: sampler.lastAckedPacketAckTime, + sendTimeState: *newSendTimeState( + sampler.isAppLimited, + sampler.totalBytesSent, + sampler.totalBytesAcked, + sampler.totalBytesLost, + bytesInFlight, + ), + } +} + +// BandwidthSampler keeps track of sent and acknowledged packets and outputs a +// bandwidth sample for every packet acknowledged. The samples are taken for +// individual packets, and are not filtered; the consumer has to filter the +// bandwidth samples itself. In certain cases, the sampler will locally severely +// underestimate the bandwidth, hence a maximum filter with a size of at least +// one RTT is recommended. +// +// This class bases its samples on the slope of two curves: the number of bytes +// sent over time, and the number of bytes acknowledged as received over time. +// It produces a sample of both slopes for every packet that gets acknowledged, +// based on a slope between two points on each of the corresponding curves. Note +// that due to the packet loss, the number of bytes on each curve might get +// further and further away from each other, meaning that it is not feasible to +// compare byte values coming from different curves with each other. +// +// The obvious points for measuring slope sample are the ones corresponding to +// the packet that was just acknowledged. Let us denote them as S_1 (point at +// which the current packet was sent) and A_1 (point at which the current packet +// was acknowledged). However, taking a slope requires two points on each line, +// so estimating bandwidth requires picking a packet in the past with respect to +// which the slope is measured. +// +// For that purpose, BandwidthSampler always keeps track of the most recently +// acknowledged packet, and records it together with every outgoing packet. +// When a packet gets acknowledged (A_1), it has not only information about when +// it itself was sent (S_1), but also the information about the latest +// acknowledged packet right before it was sent (S_0 and A_0). +// +// Based on that data, send and ack rate are estimated as: +// +// send_rate = (bytes(S_1) - bytes(S_0)) / (time(S_1) - time(S_0)) +// ack_rate = (bytes(A_1) - bytes(A_0)) / (time(A_1) - time(A_0)) +// +// Here, the ack rate is intuitively the rate we want to treat as bandwidth. +// However, in certain cases (e.g. ack compression) the ack rate at a point may +// end up higher than the rate at which the data was originally sent, which is +// not indicative of the real bandwidth. Hence, we use the send rate as an upper +// bound, and the sample value is +// +// rate_sample = min(send_rate, ack_rate) +// +// An important edge case handled by the sampler is tracking the app-limited +// samples. There are multiple meaning of "app-limited" used interchangeably, +// hence it is important to understand and to be able to distinguish between +// them. +// +// Meaning 1: connection state. The connection is said to be app-limited when +// there is no outstanding data to send. This means that certain bandwidth +// samples in the future would not be an accurate indication of the link +// capacity, and it is important to inform consumer about that. Whenever +// connection becomes app-limited, the sampler is notified via OnAppLimited() +// method. +// +// Meaning 2: a phase in the bandwidth sampler. As soon as the bandwidth +// sampler becomes notified about the connection being app-limited, it enters +// app-limited phase. In that phase, all *sent* packets are marked as +// app-limited. Note that the connection itself does not have to be +// app-limited during the app-limited phase, and in fact it will not be +// (otherwise how would it send packets?). The boolean flag below indicates +// whether the sampler is in that phase. +// +// Meaning 3: a flag on the sent packet and on the sample. If a sent packet is +// sent during the app-limited phase, the resulting sample related to the +// packet will be marked as app-limited. +// +// With the terminology issue out of the way, let us consider the question of +// what kind of situation it addresses. +// +// Consider a scenario where we first send packets 1 to 20 at a regular +// bandwidth, and then immediately run out of data. After a few seconds, we send +// packets 21 to 60, and only receive ack for 21 between sending packets 40 and +// 41. In this case, when we sample bandwidth for packets 21 to 40, the S_0/A_0 +// we use to compute the slope is going to be packet 20, a few seconds apart +// from the current packet, hence the resulting estimate would be extremely low +// and not indicative of anything. Only at packet 41 the S_0/A_0 will become 21, +// meaning that the bandwidth sample would exclude the quiescence. +// +// Based on the analysis of that scenario, we implement the following rule: once +// OnAppLimited() is called, all sent packets will produce app-limited samples +// up until an ack for a packet that was sent after OnAppLimited() was called. +// Note that while the scenario above is not the only scenario when the +// connection is app-limited, the approach works in other cases too. + +type congestionEventSample struct { + // The maximum bandwidth sample from all acked packets. + // QuicBandwidth::Zero() if no samples are available. + sampleMaxBandwidth Bandwidth + // Whether |sample_max_bandwidth| is from a app-limited sample. + sampleIsAppLimited bool + // The minimum rtt sample from all acked packets. + // QuicTime::Delta::Infinite() if no samples are available. + sampleRtt time.Duration + // For each packet p in acked packets, this is the max value of INFLIGHT(p), + // where INFLIGHT(p) is the number of bytes acked while p is inflight. + sampleMaxInflight congestion.ByteCount + // The send state of the largest packet in acked_packets, unless it is + // empty. If acked_packets is empty, it's the send state of the largest + // packet in lost_packets. + lastPacketSendState sendTimeState + // The number of extra bytes acked from this ack event, compared to what is + // expected from the flow's bandwidth. Larger value means more ack + // aggregation. + extraAcked congestion.ByteCount +} + +func newCongestionEventSample() *congestionEventSample { + return &congestionEventSample{ + sampleRtt: infRTT, + } +} + +type bandwidthSampler struct { + // The total number of congestion controlled bytes sent during the connection. + totalBytesSent congestion.ByteCount + + // The total number of congestion controlled bytes which were acknowledged. + totalBytesAcked congestion.ByteCount + + // The total number of congestion controlled bytes which were lost. + totalBytesLost congestion.ByteCount + + // The total number of congestion controlled bytes which have been neutered. + totalBytesNeutered congestion.ByteCount + + // The value of |total_bytes_sent_| at the time the last acknowledged packet + // was sent. Valid only when |last_acked_packet_sent_time_| is valid. + totalBytesSentAtLastAckedPacket congestion.ByteCount + + // The time at which the last acknowledged packet was sent. Set to + // QuicTime::Zero() if no valid timestamp is available. + lastAckedPacketSentTime congestion.Time + + // The time at which the most recent packet was acknowledged. + lastAckedPacketAckTime congestion.Time + + // The most recently sent packet. + lastSentPacket congestion.PacketNumber + + // The most recently acked packet. + lastAckedPacket congestion.PacketNumber + + // Indicates whether the bandwidth sampler is currently in an app-limited + // phase. + isAppLimited bool + + // The packet that will be acknowledged after this one will cause the sampler + // to exit the app-limited phase. + endOfAppLimitedPhase congestion.PacketNumber + + // Record of the connection state at the point where each packet in flight was + // sent, indexed by the packet number. + connectionStateMap *packetNumberIndexedQueue[connectionStateOnSentPacket] + + recentAckPoints recentAckPoints + a0Candidates RingBuffer[ackPoint] + + // Maximum number of tracked packets. + maxTrackedPackets congestion.ByteCount + + maxAckHeightTracker *maxAckHeightTracker + totalBytesAckedAfterLastAckEvent congestion.ByteCount + + // True if connection option 'BSAO' is set. + overestimateAvoidance bool + + // True if connection option 'BBRB' is set. + limitMaxAckHeightTrackerBySendRate bool +} + +func newBandwidthSampler(maxAckHeightTrackerWindowLength roundTripCount) *bandwidthSampler { + b := &bandwidthSampler{ + maxAckHeightTracker: newMaxAckHeightTracker(maxAckHeightTrackerWindowLength), + connectionStateMap: newPacketNumberIndexedQueue[connectionStateOnSentPacket](defaultConnectionStateMapQueueSize), + lastSentPacket: invalidPacketNumber, + lastAckedPacket: invalidPacketNumber, + endOfAppLimitedPhase: invalidPacketNumber, + } + + b.a0Candidates.Init(defaultCandidatesBufferSize) + + return b +} + +func (b *bandwidthSampler) MaxAckHeight() congestion.ByteCount { + return b.maxAckHeightTracker.Get() +} + +func (b *bandwidthSampler) NumAckAggregationEpochs() uint64 { + return b.maxAckHeightTracker.NumAckAggregationEpochs() +} + +func (b *bandwidthSampler) SetMaxAckHeightTrackerWindowLength(length roundTripCount) { + b.maxAckHeightTracker.SetFilterWindowLength(length) +} + +func (b *bandwidthSampler) ResetMaxAckHeightTracker(newHeight congestion.ByteCount, newTime roundTripCount) { + b.maxAckHeightTracker.Reset(newHeight, newTime) +} + +func (b *bandwidthSampler) SetStartNewAggregationEpochAfterFullRound(value bool) { + b.maxAckHeightTracker.SetStartNewAggregationEpochAfterFullRound(value) +} + +func (b *bandwidthSampler) SetLimitMaxAckHeightTrackerBySendRate(value bool) { + b.limitMaxAckHeightTrackerBySendRate = value +} + +func (b *bandwidthSampler) SetReduceExtraAckedOnBandwidthIncrease(value bool) { + b.maxAckHeightTracker.SetReduceExtraAckedOnBandwidthIncrease(value) +} + +func (b *bandwidthSampler) EnableOverestimateAvoidance() { + if b.overestimateAvoidance { + return + } + + b.overestimateAvoidance = true + b.maxAckHeightTracker.SetAckAggregationBandwidthThreshold(2.0) +} + +func (b *bandwidthSampler) IsOverestimateAvoidanceEnabled() bool { + return b.overestimateAvoidance +} + +func (b *bandwidthSampler) OnPacketSent( + sentTime congestion.Time, + packetNumber congestion.PacketNumber, + bytes congestion.ByteCount, + bytesInFlight congestion.ByteCount, + isRetransmittable bool, +) { + b.lastSentPacket = packetNumber + + if !isRetransmittable { + return + } + + b.totalBytesSent += bytes + + // If there are no packets in flight, the time at which the new transmission + // opens can be treated as the A_0 point for the purpose of bandwidth + // sampling. This underestimates bandwidth to some extent, and produces some + // artificially low samples for most packets in flight, but it provides with + // samples at important points where we would not have them otherwise, most + // importantly at the beginning of the connection. + if bytesInFlight == 0 { + b.lastAckedPacketAckTime = sentTime + if b.overestimateAvoidance { + b.recentAckPoints.Clear() + b.recentAckPoints.Update(sentTime, b.totalBytesAcked) + b.a0Candidates.Clear() + b.a0Candidates.PushBack(*b.recentAckPoints.MostRecentPoint()) + } + b.totalBytesSentAtLastAckedPacket = b.totalBytesSent + + // In this situation ack compression is not a concern, set send rate to + // effectively infinite. + b.lastAckedPacketSentTime = sentTime + } + + b.connectionStateMap.Emplace(packetNumber, newConnectionStateOnSentPacket( + sentTime, + bytes, + bytesInFlight+bytes, + b, + )) +} + +func (b *bandwidthSampler) OnCongestionEvent( + ackTime congestion.Time, + ackedPackets []congestion.AckedPacketInfo, + lostPackets []congestion.LostPacketInfo, + maxBandwidth Bandwidth, + estBandwidthUpperBound Bandwidth, + roundTripCount roundTripCount, +) congestionEventSample { + eventSample := newCongestionEventSample() + + var lastLostPacketSendState sendTimeState + + for _, p := range lostPackets { + sendState := b.OnPacketLost(p.PacketNumber, p.BytesLost) + if sendState.isValid { + lastLostPacketSendState = sendState + } + } + + if len(ackedPackets) == 0 { + // Only populate send state for a loss-only event. + eventSample.lastPacketSendState = lastLostPacketSendState + return *eventSample + } + + var lastAckedPacketSendState sendTimeState + var maxSendRate Bandwidth + + for _, p := range ackedPackets { + sample := b.onPacketAcknowledged(ackTime, p.PacketNumber) + if !sample.stateAtSend.isValid { + continue + } + + lastAckedPacketSendState = sample.stateAtSend + + if sample.rtt != 0 { + eventSample.sampleRtt = min(eventSample.sampleRtt, sample.rtt) + } + if sample.bandwidth > eventSample.sampleMaxBandwidth { + eventSample.sampleMaxBandwidth = sample.bandwidth + eventSample.sampleIsAppLimited = sample.stateAtSend.isAppLimited + } + if sample.sendRate != infBandwidth { + maxSendRate = max(maxSendRate, sample.sendRate) + } + inflightSample := b.totalBytesAcked - lastAckedPacketSendState.totalBytesAcked + if inflightSample > eventSample.sampleMaxInflight { + eventSample.sampleMaxInflight = inflightSample + } + } + + if !lastLostPacketSendState.isValid { + eventSample.lastPacketSendState = lastAckedPacketSendState + } else if !lastAckedPacketSendState.isValid { + eventSample.lastPacketSendState = lastLostPacketSendState + } else { + // If two packets are inflight and an alarm is armed to lose a packet and it + // wakes up late, then the first of two in flight packets could have been + // acknowledged before the wakeup, which re-evaluates loss detection, and + // could declare the later of the two lost. + if lostPackets[len(lostPackets)-1].PacketNumber > ackedPackets[len(ackedPackets)-1].PacketNumber { + eventSample.lastPacketSendState = lastLostPacketSendState + } else { + eventSample.lastPacketSendState = lastAckedPacketSendState + } + } + + isNewMaxBandwidth := eventSample.sampleMaxBandwidth > maxBandwidth + maxBandwidth = max(maxBandwidth, eventSample.sampleMaxBandwidth) + if b.limitMaxAckHeightTrackerBySendRate { + maxBandwidth = max(maxBandwidth, maxSendRate) + } + + eventSample.extraAcked = b.onAckEventEnd(min(estBandwidthUpperBound, maxBandwidth), isNewMaxBandwidth, roundTripCount) + + return *eventSample +} + +func (b *bandwidthSampler) OnPacketLost(packetNumber congestion.PacketNumber, bytesLost congestion.ByteCount) (s sendTimeState) { + b.totalBytesLost += bytesLost + if sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber); sentPacketPointer != nil { + sentPacketToSendTimeState(sentPacketPointer, &s) + } + return s +} + +func (b *bandwidthSampler) OnPacketNeutered(packetNumber congestion.PacketNumber) { + b.connectionStateMap.Remove(packetNumber, func(sentPacket connectionStateOnSentPacket) { + b.totalBytesNeutered += sentPacket.size + }) +} + +func (b *bandwidthSampler) OnAppLimited() { + b.isAppLimited = true + b.endOfAppLimitedPhase = b.lastSentPacket +} + +func (b *bandwidthSampler) RemoveObsoletePackets(leastUnacked congestion.PacketNumber) { + // A packet can become obsolete when it is removed from QuicUnackedPacketMap's + // view of inflight before it is acked or marked as lost. For example, when + // QuicSentPacketManager::RetransmitCryptoPackets retransmits a crypto packet, + // the packet is removed from QuicUnackedPacketMap's inflight, but is not + // marked as acked or lost in the BandwidthSampler. + b.connectionStateMap.RemoveUpTo(leastUnacked) +} + +func (b *bandwidthSampler) TotalBytesSent() congestion.ByteCount { + return b.totalBytesSent +} + +func (b *bandwidthSampler) TotalBytesLost() congestion.ByteCount { + return b.totalBytesLost +} + +func (b *bandwidthSampler) TotalBytesAcked() congestion.ByteCount { + return b.totalBytesAcked +} + +func (b *bandwidthSampler) TotalBytesNeutered() congestion.ByteCount { + return b.totalBytesNeutered +} + +func (b *bandwidthSampler) IsAppLimited() bool { + return b.isAppLimited +} + +func (b *bandwidthSampler) EndOfAppLimitedPhase() congestion.PacketNumber { + return b.endOfAppLimitedPhase +} + +func (b *bandwidthSampler) max_ack_height() congestion.ByteCount { + return b.maxAckHeightTracker.Get() +} + +func (b *bandwidthSampler) chooseA0Point(totalBytesAcked congestion.ByteCount, a0 *ackPoint) bool { + if b.a0Candidates.Empty() { + return false + } + + if b.a0Candidates.Len() == 1 { + *a0 = *b.a0Candidates.Front() + return true + } + + for i := 1; i < b.a0Candidates.Len(); i++ { + if b.a0Candidates.Offset(i).totalBytesAcked > totalBytesAcked { + *a0 = *b.a0Candidates.Offset(i - 1) + if i > 1 { + for j := 0; j < i-1; j++ { + b.a0Candidates.PopFront() + } + } + return true + } + } + + *a0 = *b.a0Candidates.Back() + for k := 0; k < b.a0Candidates.Len()-1; k++ { + b.a0Candidates.PopFront() + } + return true +} + +func (b *bandwidthSampler) onPacketAcknowledged(ackTime congestion.Time, packetNumber congestion.PacketNumber) bandwidthSample { + sample := newBandwidthSample() + b.lastAckedPacket = packetNumber + sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber) + if sentPacketPointer == nil { + return *sample + } + + // OnPacketAcknowledgedInner + b.totalBytesAcked += sentPacketPointer.size + b.totalBytesSentAtLastAckedPacket = sentPacketPointer.sendTimeState.totalBytesSent + b.lastAckedPacketSentTime = sentPacketPointer.sentTime + b.lastAckedPacketAckTime = ackTime + if b.overestimateAvoidance { + b.recentAckPoints.Update(ackTime, b.totalBytesAcked) + } + + if b.isAppLimited { + // Exit app-limited phase in two cases: + // (1) end_of_app_limited_phase_ is not initialized, i.e., so far all + // packets are sent while there are buffered packets or pending data. + // (2) The current acked packet is after the sent packet marked as the end + // of the app limit phase. + if b.endOfAppLimitedPhase == invalidPacketNumber || + packetNumber > b.endOfAppLimitedPhase { + b.isAppLimited = false + } + } + + // There might have been no packets acknowledged at the moment when the + // current packet was sent. In that case, there is no bandwidth sample to + // make. + if sentPacketPointer.lastAckedPacketSentTime.IsZero() { + return *sample + } + + // Infinite rate indicates that the sampler is supposed to discard the + // current send rate sample and use only the ack rate. + sendRate := infBandwidth + if sentPacketPointer.sentTime.After(sentPacketPointer.lastAckedPacketSentTime) { + sendRate = BandwidthFromDelta( + sentPacketPointer.sendTimeState.totalBytesSent-sentPacketPointer.totalBytesSentAtLastAckedPacket, + sentPacketPointer.sentTime.Sub(sentPacketPointer.lastAckedPacketSentTime)) + } + + var a0 ackPoint + if b.overestimateAvoidance && b.chooseA0Point(sentPacketPointer.sendTimeState.totalBytesAcked, &a0) { + } else { + a0.ackTime = sentPacketPointer.lastAckedPacketAckTime + a0.totalBytesAcked = sentPacketPointer.sendTimeState.totalBytesAcked + } + + // During the slope calculation, ensure that ack time of the current packet is + // always larger than the time of the previous packet, otherwise division by + // zero or integer underflow can occur. + if ackTime.Sub(a0.ackTime) <= 0 { + return *sample + } + + ackRate := BandwidthFromDelta(b.totalBytesAcked-a0.totalBytesAcked, ackTime.Sub(a0.ackTime)) + + sample.bandwidth = min(sendRate, ackRate) + // Note: this sample does not account for delayed acknowledgement time. This + // means that the RTT measurements here can be artificially high, especially + // on low bandwidth connections. + sample.rtt = ackTime.Sub(sentPacketPointer.sentTime) + sample.sendRate = sendRate + sentPacketToSendTimeState(sentPacketPointer, &sample.stateAtSend) + + return *sample +} + +func (b *bandwidthSampler) onAckEventEnd( + bandwidthEstimate Bandwidth, + isNewMaxBandwidth bool, + roundTripCount roundTripCount, +) congestion.ByteCount { + newlyAckedBytes := b.totalBytesAcked - b.totalBytesAckedAfterLastAckEvent + if newlyAckedBytes == 0 { + return 0 + } + b.totalBytesAckedAfterLastAckEvent = b.totalBytesAcked + extraAcked := b.maxAckHeightTracker.Update( + bandwidthEstimate, + isNewMaxBandwidth, + roundTripCount, + b.lastSentPacket, + b.lastAckedPacket, + b.lastAckedPacketAckTime, + newlyAckedBytes) + // If |extra_acked| is zero, i.e. this ack event marks the start of a new ack + // aggregation epoch, save LessRecentPoint, which is the last ack point of the + // previous epoch, as a A0 candidate. + if b.overestimateAvoidance && extraAcked == 0 { + b.a0Candidates.PushBack(*b.recentAckPoints.LessRecentPoint()) + } + return extraAcked +} + +func sentPacketToSendTimeState(sentPacket *connectionStateOnSentPacket, sendTimeState *sendTimeState) { + *sendTimeState = sentPacket.sendTimeState + sendTimeState.isValid = true +} + +// BytesFromBandwidthAndTimeDelta calculates the bytes +// from a bandwidth(bits per second) and a time delta +func bytesFromBandwidthAndTimeDelta(bandwidth Bandwidth, delta time.Duration) congestion.ByteCount { + return (congestion.ByteCount(bandwidth) * congestion.ByteCount(delta)) / + (congestion.ByteCount(time.Second) * 8) +} + +func timeDeltaFromBytesAndBandwidth(bytes congestion.ByteCount, bandwidth Bandwidth) time.Duration { + return time.Duration(bytes*8) * time.Second / time.Duration(bandwidth) +} diff --git a/transport/internet/hysteria/congestion/bbr/bbr_sender.go b/transport/internet/hysteria/congestion/bbr/bbr_sender.go new file mode 100644 index 00000000..93c5bf84 --- /dev/null +++ b/transport/internet/hysteria/congestion/bbr/bbr_sender.go @@ -0,0 +1,980 @@ +package bbr + +import ( + "fmt" + "math/rand" + "net" + "os" + "strconv" + "time" + + "github.com/apernet/quic-go/congestion" + + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/common" +) + +// BbrSender implements BBR congestion control algorithm. BBR aims to estimate +// the current available Bottleneck Bandwidth and RTT (hence the name), and +// regulates the pacing rate and the size of the congestion window based on +// those signals. +// +// BBR relies on pacing in order to function properly. Do not use BBR when +// pacing is disabled. +// + +const ( + minBps = 65536 // 64 KB/s + + invalidPacketNumber = -1 + initialCongestionWindowPackets = 32 + + // Constants based on TCP defaults. + // The minimum CWND to ensure delayed acks don't reduce bandwidth measurements. + // Does not inflate the pacing rate. + defaultMinimumCongestionWindow = 4 * congestion.ByteCount(congestion.InitialPacketSize) + + // The gain used for the STARTUP, equal to 2/ln(2). + defaultHighGain = 2.885 + // The newly derived gain for STARTUP, equal to 4 * ln(2) + derivedHighGain = 2.773 + // The newly derived CWND gain for STARTUP, 2. + derivedHighCWNDGain = 2.0 + + debugEnv = "HYSTERIA_BBR_DEBUG" +) + +// The cycle of gains used during the PROBE_BW stage. +var pacingGain = [...]float64{1.25, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0} + +const ( + // The length of the gain cycle. + gainCycleLength = len(pacingGain) + // The size of the bandwidth filter window, in round-trips. + bandwidthWindowSize = gainCycleLength + 2 + + // The time after which the current min_rtt value expires. + minRttExpiry = 10 * time.Second + // The minimum time the connection can spend in PROBE_RTT mode. + probeRttTime = 200 * time.Millisecond + // If the bandwidth does not increase by the factor of |kStartupGrowthTarget| + // within |kRoundTripsWithoutGrowthBeforeExitingStartup| rounds, the connection + // will exit the STARTUP mode. + startupGrowthTarget = 1.25 + roundTripsWithoutGrowthBeforeExitingStartup = int64(3) + + // Flag. + defaultStartupFullLossCount = 8 + quicBbr2DefaultLossThreshold = 0.02 + maxBbrBurstPackets = 10 +) + +type bbrMode int + +const ( + // Startup phase of the connection. + bbrModeStartup = iota + // After achieving the highest possible bandwidth during the startup, lower + // the pacing rate in order to drain the queue. + bbrModeDrain + // Cruising mode. + bbrModeProbeBw + // Temporarily slow down sending in order to empty the buffer and measure + // the real minimum RTT. + bbrModeProbeRtt +) + +// Indicates how the congestion control limits the amount of bytes in flight. +type bbrRecoveryState int + +const ( + // Do not limit. + bbrRecoveryStateNotInRecovery = iota + // Allow an extra outstanding byte for each byte acknowledged. + bbrRecoveryStateConservation + // Allow two extra outstanding bytes for each byte acknowledged (slow + // start). + bbrRecoveryStateGrowth +) + +type bbrSender struct { + rttStats congestion.RTTStatsProvider + clock Clock + pacer *common.Pacer + + mode bbrMode + + // Bandwidth sampler provides BBR with the bandwidth measurements at + // individual points. + sampler *bandwidthSampler + + // The number of the round trips that have occurred during the connection. + roundTripCount roundTripCount + + // The packet number of the most recently sent packet. + lastSentPacket congestion.PacketNumber + // Acknowledgement of any packet after |current_round_trip_end_| will cause + // the round trip counter to advance. + currentRoundTripEnd congestion.PacketNumber + + // Number of congestion events with some losses, in the current round. + numLossEventsInRound uint64 + + // Number of total bytes lost in the current round. + bytesLostInRound congestion.ByteCount + + // The filter that tracks the maximum bandwidth over the multiple recent + // round-trips. + maxBandwidth *WindowedFilter[Bandwidth, roundTripCount] + + // Minimum RTT estimate. Automatically expires within 10 seconds (and + // triggers PROBE_RTT mode) if no new value is sampled during that period. + minRtt time.Duration + // The time at which the current value of |min_rtt_| was assigned. + minRttTimestamp congestion.Time + + // The maximum allowed number of bytes in flight. + congestionWindow congestion.ByteCount + + // The initial value of the |congestion_window_|. + initialCongestionWindow congestion.ByteCount + + // The largest value the |congestion_window_| can achieve. + maxCongestionWindow congestion.ByteCount + + // The smallest value the |congestion_window_| can achieve. + minCongestionWindow congestion.ByteCount + + // The pacing gain applied during the STARTUP phase. + highGain float64 + + // The CWND gain applied during the STARTUP phase. + highCwndGain float64 + + // The pacing gain applied during the DRAIN phase. + drainGain float64 + + // The current pacing rate of the connection. + pacingRate Bandwidth + + // The gain currently applied to the pacing rate. + pacingGain float64 + // The gain currently applied to the congestion window. + congestionWindowGain float64 + + // The gain used for the congestion window during PROBE_BW. Latched from + // quic_bbr_cwnd_gain flag. + congestionWindowGainConstant float64 + // The number of RTTs to stay in STARTUP mode. Defaults to 3. + numStartupRtts int64 + + // Number of round-trips in PROBE_BW mode, used for determining the current + // pacing gain cycle. + cycleCurrentOffset int + // The time at which the last pacing gain cycle was started. + lastCycleStart congestion.Time + + // Indicates whether the connection has reached the full bandwidth mode. + isAtFullBandwidth bool + // Number of rounds during which there was no significant bandwidth increase. + roundsWithoutBandwidthGain int64 + // The bandwidth compared to which the increase is measured. + bandwidthAtLastRound Bandwidth + + // Set to true upon exiting quiescence. + exitingQuiescence bool + + // Time at which PROBE_RTT has to be exited. Setting it to zero indicates + // that the time is yet unknown as the number of packets in flight has not + // reached the required value. + exitProbeRttAt congestion.Time + // Indicates whether a round-trip has passed since PROBE_RTT became active. + probeRttRoundPassed bool + + // Indicates whether the most recent bandwidth sample was marked as + // app-limited. + lastSampleIsAppLimited bool + // Indicates whether any non app-limited samples have been recorded. + hasNoAppLimitedSample bool + + // Current state of recovery. + recoveryState bbrRecoveryState + // Receiving acknowledgement of a packet after |end_recovery_at_| will cause + // BBR to exit the recovery mode. A value above zero indicates at least one + // loss has been detected, so it must not be set back to zero. + endRecoveryAt congestion.PacketNumber + // A window used to limit the number of bytes in flight during loss recovery. + recoveryWindow congestion.ByteCount + // If true, consider all samples in recovery app-limited. + isAppLimitedRecovery bool // not used + + // When true, pace at 1.5x and disable packet conservation in STARTUP. + slowerStartup bool // not used + // When true, disables packet conservation in STARTUP. + rateBasedStartup bool // not used + + // When true, add the most recent ack aggregation measurement during STARTUP. + enableAckAggregationDuringStartup bool + // When true, expire the windowed ack aggregation values in STARTUP when + // bandwidth increases more than 25%. + expireAckAggregationInStartup bool + + // If true, will not exit low gain mode until bytes_in_flight drops below BDP + // or it's time for high gain mode. + drainToTarget bool + + // If true, slow down pacing rate in STARTUP when overshooting is detected. + detectOvershooting bool + // Bytes lost while detect_overshooting_ is true. + bytesLostWhileDetectingOvershooting congestion.ByteCount + // Slow down pacing rate if + // bytes_lost_while_detecting_overshooting_ * + // bytes_lost_multiplier_while_detecting_overshooting_ > IW. + bytesLostMultiplierWhileDetectingOvershooting uint8 + // When overshooting is detected, do not drop pacing_rate_ below this value / + // min_rtt. + cwndToCalculateMinPacingRate congestion.ByteCount + + // Max congestion window when adjusting network parameters. + maxCongestionWindowWithNetworkParametersAdjusted congestion.ByteCount // not used + + // Params. + maxDatagramSize congestion.ByteCount + // Recorded on packet sent. equivalent |unacked_packets_->bytes_in_flight()| + bytesInFlight congestion.ByteCount + + debug bool +} + +var _ congestion.CongestionControl = &bbrSender{} + +func NewBbrSender( + clock Clock, + initialMaxDatagramSize congestion.ByteCount, +) *bbrSender { + return newBbrSender( + clock, + initialMaxDatagramSize, + initialCongestionWindowPackets*initialMaxDatagramSize, + congestion.MaxCongestionWindowPackets*initialMaxDatagramSize, + ) +} + +func newBbrSender( + clock Clock, + initialMaxDatagramSize, + initialCongestionWindow, + initialMaxCongestionWindow congestion.ByteCount, +) *bbrSender { + debug, _ := strconv.ParseBool(os.Getenv(debugEnv)) + b := &bbrSender{ + clock: clock, + mode: bbrModeStartup, + sampler: newBandwidthSampler(roundTripCount(bandwidthWindowSize)), + lastSentPacket: invalidPacketNumber, + currentRoundTripEnd: invalidPacketNumber, + maxBandwidth: NewWindowedFilter(roundTripCount(bandwidthWindowSize), MaxFilter[Bandwidth]), + congestionWindow: initialCongestionWindow, + initialCongestionWindow: initialCongestionWindow, + maxCongestionWindow: initialMaxCongestionWindow, + minCongestionWindow: defaultMinimumCongestionWindow, + highGain: defaultHighGain, + highCwndGain: defaultHighGain, + drainGain: 1.0 / defaultHighGain, + pacingGain: 1.0, + congestionWindowGain: 1.0, + congestionWindowGainConstant: 2.0, + numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup, + recoveryState: bbrRecoveryStateNotInRecovery, + endRecoveryAt: invalidPacketNumber, + recoveryWindow: initialMaxCongestionWindow, + bytesLostMultiplierWhileDetectingOvershooting: 2, + cwndToCalculateMinPacingRate: initialCongestionWindow, + maxCongestionWindowWithNetworkParametersAdjusted: initialMaxCongestionWindow, + maxDatagramSize: initialMaxDatagramSize, + debug: debug, + } + b.pacer = common.NewPacer(b.bandwidthForPacer) + + /* + if b.tracer != nil { + b.lastState = logging.CongestionStateStartup + b.tracer.UpdatedCongestionState(logging.CongestionStateStartup) + } + */ + + b.enterStartupMode(b.clock.Now()) + b.setHighCwndGain(derivedHighCWNDGain) + + return b +} + +func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { + b.rttStats = provider +} + +// TimeUntilSend implements the SendAlgorithm interface. +func (b *bbrSender) TimeUntilSend(bytesInFlight congestion.ByteCount) congestion.Time { + return b.pacer.TimeUntilSend() +} + +// HasPacingBudget implements the SendAlgorithm interface. +func (b *bbrSender) HasPacingBudget(now congestion.Time) bool { + return b.pacer.Budget(now) >= b.maxDatagramSize +} + +// OnPacketSent implements the SendAlgorithm interface. +func (b *bbrSender) OnPacketSent( + sentTime congestion.Time, + bytesInFlight congestion.ByteCount, + packetNumber congestion.PacketNumber, + bytes congestion.ByteCount, + isRetransmittable bool, +) { + b.pacer.SentPacket(sentTime, bytes) + + b.lastSentPacket = packetNumber + b.bytesInFlight = bytesInFlight + + if bytesInFlight == 0 { + b.exitingQuiescence = true + } + + b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable) +} + +// CanSend implements the SendAlgorithm interface. +func (b *bbrSender) CanSend(bytesInFlight congestion.ByteCount) bool { + return bytesInFlight < b.GetCongestionWindow() +} + +// MaybeExitSlowStart implements the SendAlgorithm interface. +func (b *bbrSender) MaybeExitSlowStart() { + // Do nothing +} + +// OnPacketAcked implements the SendAlgorithm interface. +func (b *bbrSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes, priorInFlight congestion.ByteCount, eventTime congestion.Time) { + // Do nothing. +} + +// OnPacketLost implements the SendAlgorithm interface. +func (b *bbrSender) OnPacketLost(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { + // Do nothing. +} + +// OnRetransmissionTimeout implements the SendAlgorithm interface. +func (b *bbrSender) OnRetransmissionTimeout(packetsRetransmitted bool) { + // Do nothing. +} + +// SetMaxDatagramSize implements the SendAlgorithm interface. +func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) { + if s < b.maxDatagramSize { + panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s)) + } + cwndIsMinCwnd := b.congestionWindow == b.minCongestionWindow + b.maxDatagramSize = s + if cwndIsMinCwnd { + b.congestionWindow = b.minCongestionWindow + } + b.pacer.SetMaxDatagramSize(s) +} + +// InSlowStart implements the SendAlgorithmWithDebugInfos interface. +func (b *bbrSender) InSlowStart() bool { + return b.mode == bbrModeStartup +} + +// InRecovery implements the SendAlgorithmWithDebugInfos interface. +func (b *bbrSender) InRecovery() bool { + return b.recoveryState != bbrRecoveryStateNotInRecovery +} + +// GetCongestionWindow implements the SendAlgorithmWithDebugInfos interface. +func (b *bbrSender) GetCongestionWindow() congestion.ByteCount { + if b.mode == bbrModeProbeRtt { + return b.probeRttCongestionWindow() + } + + if b.InRecovery() { + return min(b.congestionWindow, b.recoveryWindow) + } + + return b.congestionWindow +} + +func (b *bbrSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { + // Do nothing. +} + +func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime congestion.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { + totalBytesAckedBefore := b.sampler.TotalBytesAcked() + totalBytesLostBefore := b.sampler.TotalBytesLost() + + var isRoundStart, minRttExpired bool + var excessAcked, bytesLost congestion.ByteCount + + // The send state of the largest packet in acked_packets, unless it is + // empty. If acked_packets is empty, it's the send state of the largest + // packet in lost_packets. + var lastPacketSendState sendTimeState + + b.maybeAppLimited(priorInFlight) + + // Update bytesInFlight + b.bytesInFlight = priorInFlight + for _, p := range ackedPackets { + b.bytesInFlight -= p.BytesAcked + } + for _, p := range lostPackets { + b.bytesInFlight -= p.BytesLost + } + + if len(ackedPackets) != 0 { + lastAckedPacket := ackedPackets[len(ackedPackets)-1].PacketNumber + isRoundStart = b.updateRoundTripCounter(lastAckedPacket) + b.updateRecoveryState(lastAckedPacket, len(lostPackets) != 0, isRoundStart) + } + + sample := b.sampler.OnCongestionEvent(eventTime, + ackedPackets, lostPackets, b.maxBandwidth.GetBest(), infBandwidth, b.roundTripCount) + if sample.lastPacketSendState.isValid { + b.lastSampleIsAppLimited = sample.lastPacketSendState.isAppLimited + b.hasNoAppLimitedSample = b.hasNoAppLimitedSample || !b.lastSampleIsAppLimited + } + // Avoid updating |max_bandwidth_| if a) this is a loss-only event, or b) all + // packets in |acked_packets| did not generate valid samples. (e.g. ack of + // ack-only packets). In both cases, sampler_.total_bytes_acked() will not + // change. + if totalBytesAckedBefore != b.sampler.TotalBytesAcked() { + if !sample.sampleIsAppLimited || sample.sampleMaxBandwidth > b.maxBandwidth.GetBest() { + b.maxBandwidth.Update(sample.sampleMaxBandwidth, b.roundTripCount) + } + } + + if sample.sampleRtt != infRTT { + minRttExpired = b.maybeUpdateMinRtt(eventTime, sample.sampleRtt) + } + bytesLost = b.sampler.TotalBytesLost() - totalBytesLostBefore + + excessAcked = sample.extraAcked + lastPacketSendState = sample.lastPacketSendState + + if len(lostPackets) != 0 { + b.numLossEventsInRound++ + b.bytesLostInRound += bytesLost + } + + // Handle logic specific to PROBE_BW mode. + if b.mode == bbrModeProbeBw { + b.updateGainCyclePhase(eventTime, priorInFlight, len(lostPackets) != 0) + } + + // Handle logic specific to STARTUP and DRAIN modes. + if isRoundStart && !b.isAtFullBandwidth { + b.checkIfFullBandwidthReached(&lastPacketSendState) + } + + b.maybeExitStartupOrDrain(eventTime) + + // Handle logic specific to PROBE_RTT. + b.maybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired) + + // Calculate number of packets acked and lost. + bytesAcked := b.sampler.TotalBytesAcked() - totalBytesAckedBefore + + // After the model is updated, recalculate the pacing rate and congestion + // window. + b.calculatePacingRate(bytesLost) + b.calculateCongestionWindow(bytesAcked, excessAcked) + b.calculateRecoveryWindow(bytesAcked, bytesLost) + + // Cleanup internal state. + // This is where we clean up obsolete (acked or lost) packets from the bandwidth sampler. + // The "least unacked" should actually be FirstOutstanding, but since we are not passing + // that through OnCongestionEventEx, we will only do an estimate using acked/lost packets + // for now. Because of fast retransmission, they should differ by no more than 2 packets. + // (this is controlled by packetThreshold in quic-go's sentPacketHandler) + var leastUnacked congestion.PacketNumber + if len(ackedPackets) != 0 { + leastUnacked = ackedPackets[len(ackedPackets)-1].PacketNumber - 2 + } else { + leastUnacked = lostPackets[len(lostPackets)-1].PacketNumber + 1 + } + b.sampler.RemoveObsoletePackets(leastUnacked) + + if isRoundStart { + b.numLossEventsInRound = 0 + b.bytesLostInRound = 0 + } +} + +func (b *bbrSender) PacingRate() Bandwidth { + if b.pacingRate == 0 { + return Bandwidth(b.highGain * float64( + BandwidthFromDelta(b.initialCongestionWindow, b.getMinRtt()))) + } + + return b.pacingRate +} + +func (b *bbrSender) hasGoodBandwidthEstimateForResumption() bool { + return b.hasNonAppLimitedSample() +} + +func (b *bbrSender) hasNonAppLimitedSample() bool { + return b.hasNoAppLimitedSample +} + +// Sets the pacing gain used in STARTUP. Must be greater than 1. +func (b *bbrSender) setHighGain(highGain float64) { + b.highGain = highGain + if b.mode == bbrModeStartup { + b.pacingGain = highGain + } +} + +// Sets the CWND gain used in STARTUP. Must be greater than 1. +func (b *bbrSender) setHighCwndGain(highCwndGain float64) { + b.highCwndGain = highCwndGain + if b.mode == bbrModeStartup { + b.congestionWindowGain = highCwndGain + } +} + +// Sets the gain used in DRAIN. Must be less than 1. +func (b *bbrSender) setDrainGain(drainGain float64) { + b.drainGain = drainGain +} + +// Get the current bandwidth estimate. Note that Bandwidth is in bits per second. +func (b *bbrSender) bandwidthEstimate() Bandwidth { + return b.maxBandwidth.GetBest() +} + +func (b *bbrSender) bandwidthForPacer() congestion.ByteCount { + bps := congestion.ByteCount(float64(b.PacingRate()) / float64(BytesPerSecond)) + if bps < minBps { + // We need to make sure that the bandwidth value for pacer is never zero, + // otherwise it will go into an edge case where HasPacingBudget = false + // but TimeUntilSend is before, causing the quic-go send loop to go crazy and get stuck. + return minBps + } + return bps +} + +// Returns the current estimate of the RTT of the connection. Outside of the +// edge cases, this is minimum RTT. +func (b *bbrSender) getMinRtt() time.Duration { + if b.minRtt != 0 { + return b.minRtt + } + // min_rtt could be available if the handshake packet gets neutered then + // gets acknowledged. This could only happen for QUIC crypto where we do not + // drop keys. + minRtt := b.rttStats.MinRTT() + if minRtt == 0 { + return 100 * time.Millisecond + } else { + return minRtt + } +} + +// Computes the target congestion window using the specified gain. +func (b *bbrSender) getTargetCongestionWindow(gain float64) congestion.ByteCount { + bdp := bdpFromRttAndBandwidth(b.getMinRtt(), b.bandwidthEstimate()) + congestionWindow := congestion.ByteCount(gain * float64(bdp)) + + // BDP estimate will be zero if no bandwidth samples are available yet. + if congestionWindow == 0 { + congestionWindow = congestion.ByteCount(gain * float64(b.initialCongestionWindow)) + } + + return max(congestionWindow, b.minCongestionWindow) +} + +// The target congestion window during PROBE_RTT. +func (b *bbrSender) probeRttCongestionWindow() congestion.ByteCount { + return b.minCongestionWindow +} + +func (b *bbrSender) maybeUpdateMinRtt(now congestion.Time, sampleMinRtt time.Duration) bool { + // Do not expire min_rtt if none was ever available. + minRttExpired := b.minRtt != 0 && now.After(b.minRttTimestamp.Add(minRttExpiry)) + if minRttExpired || sampleMinRtt < b.minRtt || b.minRtt == 0 { + b.minRtt = sampleMinRtt + b.minRttTimestamp = now + } + + return minRttExpired +} + +// Enters the STARTUP mode. +func (b *bbrSender) enterStartupMode(now congestion.Time) { + b.mode = bbrModeStartup + // b.maybeTraceStateChange(logging.CongestionStateStartup) + b.pacingGain = b.highGain + b.congestionWindowGain = b.highCwndGain + + if b.debug { + b.debugPrint("Phase: STARTUP") + } +} + +// Enters the PROBE_BW mode. +func (b *bbrSender) enterProbeBandwidthMode(now congestion.Time) { + b.mode = bbrModeProbeBw + // b.maybeTraceStateChange(logging.CongestionStateProbeBw) + b.congestionWindowGain = b.congestionWindowGainConstant + + // Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is + // excluded because in that case increased gain and decreased gain would not + // follow each other. + b.cycleCurrentOffset = int(rand.Int31n(congestion.PacketsPerConnectionID)) % (gainCycleLength - 1) + if b.cycleCurrentOffset >= 1 { + b.cycleCurrentOffset += 1 + } + + b.lastCycleStart = now + b.pacingGain = pacingGain[b.cycleCurrentOffset] + + if b.debug { + b.debugPrint("Phase: PROBE_BW") + } +} + +// Updates the round-trip counter if a round-trip has passed. Returns true if +// the counter has been advanced. +func (b *bbrSender) updateRoundTripCounter(lastAckedPacket congestion.PacketNumber) bool { + if b.currentRoundTripEnd == invalidPacketNumber || lastAckedPacket > b.currentRoundTripEnd { + b.roundTripCount++ + b.currentRoundTripEnd = b.lastSentPacket + return true + } + return false +} + +// Updates the current gain used in PROBE_BW mode. +func (b *bbrSender) updateGainCyclePhase(now congestion.Time, priorInFlight congestion.ByteCount, hasLosses bool) { + // In most cases, the cycle is advanced after an RTT passes. + shouldAdvanceGainCycling := now.After(b.lastCycleStart.Add(b.getMinRtt())) + // If the pacing gain is above 1.0, the connection is trying to probe the + // bandwidth by increasing the number of bytes in flight to at least + // pacing_gain * BDP. Make sure that it actually reaches the target, as long + // as there are no losses suggesting that the buffers are not able to hold + // that much. + if b.pacingGain > 1.0 && !hasLosses && priorInFlight < b.getTargetCongestionWindow(b.pacingGain) { + shouldAdvanceGainCycling = false + } + + // If pacing gain is below 1.0, the connection is trying to drain the extra + // queue which could have been incurred by probing prior to it. If the number + // of bytes in flight falls down to the estimated BDP value earlier, conclude + // that the queue has been successfully drained and exit this cycle early. + if b.pacingGain < 1.0 && b.bytesInFlight <= b.getTargetCongestionWindow(1) { + shouldAdvanceGainCycling = true + } + + if shouldAdvanceGainCycling { + b.cycleCurrentOffset = (b.cycleCurrentOffset + 1) % gainCycleLength + b.lastCycleStart = now + // Stay in low gain mode until the target BDP is hit. + // Low gain mode will be exited immediately when the target BDP is achieved. + if b.drainToTarget && b.pacingGain < 1 && + pacingGain[b.cycleCurrentOffset] == 1 && + b.bytesInFlight > b.getTargetCongestionWindow(1) { + return + } + b.pacingGain = pacingGain[b.cycleCurrentOffset] + } +} + +// Tracks for how many round-trips the bandwidth has not increased +// significantly. +func (b *bbrSender) checkIfFullBandwidthReached(lastPacketSendState *sendTimeState) { + if b.lastSampleIsAppLimited { + return + } + + target := Bandwidth(float64(b.bandwidthAtLastRound) * startupGrowthTarget) + if b.bandwidthEstimate() >= target { + b.bandwidthAtLastRound = b.bandwidthEstimate() + b.roundsWithoutBandwidthGain = 0 + if b.expireAckAggregationInStartup { + // Expire old excess delivery measurements now that bandwidth increased. + b.sampler.ResetMaxAckHeightTracker(0, b.roundTripCount) + } + return + } + + b.roundsWithoutBandwidthGain++ + if b.roundsWithoutBandwidthGain >= b.numStartupRtts || + b.shouldExitStartupDueToLoss(lastPacketSendState) { + b.isAtFullBandwidth = true + } +} + +func (b *bbrSender) maybeAppLimited(bytesInFlight congestion.ByteCount) { + if bytesInFlight < b.getTargetCongestionWindow(1) { + b.sampler.OnAppLimited() + } +} + +// Transitions from STARTUP to DRAIN and from DRAIN to PROBE_BW if +// appropriate. +func (b *bbrSender) maybeExitStartupOrDrain(now congestion.Time) { + if b.mode == bbrModeStartup && b.isAtFullBandwidth { + b.mode = bbrModeDrain + // b.maybeTraceStateChange(logging.CongestionStateDrain) + b.pacingGain = b.drainGain + b.congestionWindowGain = b.highCwndGain + + if b.debug { + b.debugPrint("Phase: DRAIN") + } + } + if b.mode == bbrModeDrain && b.bytesInFlight <= b.getTargetCongestionWindow(1) { + b.enterProbeBandwidthMode(now) + } +} + +// Decides whether to enter or exit PROBE_RTT. +func (b *bbrSender) maybeEnterOrExitProbeRtt(now congestion.Time, isRoundStart, minRttExpired bool) { + if minRttExpired && !b.exitingQuiescence && b.mode != bbrModeProbeRtt { + b.mode = bbrModeProbeRtt + // b.maybeTraceStateChange(logging.CongestionStateProbRtt) + b.pacingGain = 1.0 + // Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight| + // is at the target small value. + b.exitProbeRttAt = 0 + + if b.debug { + b.debugPrint("BandwidthEstimate: %s, CongestionWindowGain: %.2f, PacingGain: %.2f, PacingRate: %s", + formatSpeed(b.bandwidthEstimate()), b.congestionWindowGain, b.pacingGain, formatSpeed(b.PacingRate())) + b.debugPrint("Phase: PROBE_RTT") + } + } + + if b.mode == bbrModeProbeRtt { + b.sampler.OnAppLimited() + // b.maybeTraceStateChange(logging.CongestionStateApplicationLimited) + + if b.exitProbeRttAt.IsZero() { + // If the window has reached the appropriate size, schedule exiting + // PROBE_RTT. The CWND during PROBE_RTT is kMinimumCongestionWindow, but + // we allow an extra packet since QUIC checks CWND before sending a + // packet. + if b.bytesInFlight < b.probeRttCongestionWindow()+congestion.MaxPacketBufferSize { + b.exitProbeRttAt = now.Add(probeRttTime) + b.probeRttRoundPassed = false + } + } else { + if isRoundStart { + b.probeRttRoundPassed = true + } + if now.Sub(b.exitProbeRttAt) >= 0 && b.probeRttRoundPassed { + b.minRttTimestamp = now + if b.debug { + b.debugPrint("MinRTT: %s", b.getMinRtt()) + } + if !b.isAtFullBandwidth { + b.enterStartupMode(now) + } else { + b.enterProbeBandwidthMode(now) + } + } + } + } + + b.exitingQuiescence = false +} + +// Determines whether BBR needs to enter, exit or advance state of the +// recovery. +func (b *bbrSender) updateRecoveryState(lastAckedPacket congestion.PacketNumber, hasLosses, isRoundStart bool) { + // Disable recovery in startup, if loss-based exit is enabled. + if !b.isAtFullBandwidth { + return + } + + // Exit recovery when there are no losses for a round. + if hasLosses { + b.endRecoveryAt = b.lastSentPacket + } + + switch b.recoveryState { + case bbrRecoveryStateNotInRecovery: + if hasLosses { + b.recoveryState = bbrRecoveryStateConservation + // This will cause the |recovery_window_| to be set to the correct + // value in CalculateRecoveryWindow(). + b.recoveryWindow = 0 + // Since the conservation phase is meant to be lasting for a whole + // round, extend the current round as if it were started right now. + b.currentRoundTripEnd = b.lastSentPacket + } + case bbrRecoveryStateConservation: + if isRoundStart { + b.recoveryState = bbrRecoveryStateGrowth + } + fallthrough + case bbrRecoveryStateGrowth: + // Exit recovery if appropriate. + if !hasLosses && lastAckedPacket > b.endRecoveryAt { + b.recoveryState = bbrRecoveryStateNotInRecovery + } + } +} + +// Determines the appropriate pacing rate for the connection. +func (b *bbrSender) calculatePacingRate(bytesLost congestion.ByteCount) { + if b.bandwidthEstimate() == 0 { + return + } + + targetRate := Bandwidth(b.pacingGain * float64(b.bandwidthEstimate())) + if b.isAtFullBandwidth { + b.pacingRate = targetRate + return + } + + // Pace at the rate of initial_window / RTT as soon as RTT measurements are + // available. + if b.pacingRate == 0 && b.rttStats.MinRTT() != 0 { + b.pacingRate = BandwidthFromDelta(b.initialCongestionWindow, b.rttStats.MinRTT()) + return + } + + if b.detectOvershooting { + b.bytesLostWhileDetectingOvershooting += bytesLost + // Check for overshooting with network parameters adjusted when pacing rate + // > target_rate and loss has been detected. + if b.pacingRate > targetRate && b.bytesLostWhileDetectingOvershooting > 0 { + if b.hasNoAppLimitedSample || + b.bytesLostWhileDetectingOvershooting*congestion.ByteCount(b.bytesLostMultiplierWhileDetectingOvershooting) > b.initialCongestionWindow { + // We are fairly sure overshoot happens if 1) there is at least one + // non app-limited bw sample or 2) half of IW gets lost. Slow pacing + // rate. + b.pacingRate = max(targetRate, BandwidthFromDelta(b.cwndToCalculateMinPacingRate, b.rttStats.MinRTT())) + b.bytesLostWhileDetectingOvershooting = 0 + b.detectOvershooting = false + } + } + } + + // Do not decrease the pacing rate during startup. + b.pacingRate = max(b.pacingRate, targetRate) +} + +// Determines the appropriate congestion window for the connection. +func (b *bbrSender) calculateCongestionWindow(bytesAcked, excessAcked congestion.ByteCount) { + if b.mode == bbrModeProbeRtt { + return + } + + targetWindow := b.getTargetCongestionWindow(b.congestionWindowGain) + if b.isAtFullBandwidth { + // Add the max recently measured ack aggregation to CWND. + targetWindow += b.sampler.MaxAckHeight() + } else if b.enableAckAggregationDuringStartup { + // Add the most recent excess acked. Because CWND never decreases in + // STARTUP, this will automatically create a very localized max filter. + targetWindow += excessAcked + } + + // Instead of immediately setting the target CWND as the new one, BBR grows + // the CWND towards |target_window| by only increasing it |bytes_acked| at a + // time. + if b.isAtFullBandwidth { + b.congestionWindow = min(targetWindow, b.congestionWindow+bytesAcked) + } else if b.congestionWindow < targetWindow || + b.sampler.TotalBytesAcked() < b.initialCongestionWindow { + // If the connection is not yet out of startup phase, do not decrease the + // window. + b.congestionWindow += bytesAcked + } + + // Enforce the limits on the congestion window. + b.congestionWindow = max(b.congestionWindow, b.minCongestionWindow) + b.congestionWindow = min(b.congestionWindow, b.maxCongestionWindow) +} + +// Determines the appropriate window that constrains the in-flight during recovery. +func (b *bbrSender) calculateRecoveryWindow(bytesAcked, bytesLost congestion.ByteCount) { + if b.recoveryState == bbrRecoveryStateNotInRecovery { + return + } + + // Set up the initial recovery window. + if b.recoveryWindow == 0 { + b.recoveryWindow = b.bytesInFlight + bytesAcked + b.recoveryWindow = max(b.minCongestionWindow, b.recoveryWindow) + return + } + + // Remove losses from the recovery window, while accounting for a potential + // integer underflow. + if b.recoveryWindow >= bytesLost { + b.recoveryWindow = b.recoveryWindow - bytesLost + } else { + b.recoveryWindow = b.maxDatagramSize + } + + // In CONSERVATION mode, just subtracting losses is sufficient. In GROWTH, + // release additional |bytes_acked| to achieve a slow-start-like behavior. + if b.recoveryState == bbrRecoveryStateGrowth { + b.recoveryWindow += bytesAcked + } + + // Always allow sending at least |bytes_acked| in response. + b.recoveryWindow = max(b.recoveryWindow, b.bytesInFlight+bytesAcked) + b.recoveryWindow = max(b.minCongestionWindow, b.recoveryWindow) +} + +// Return whether we should exit STARTUP due to excessive loss. +func (b *bbrSender) shouldExitStartupDueToLoss(lastPacketSendState *sendTimeState) bool { + if b.numLossEventsInRound < defaultStartupFullLossCount || !lastPacketSendState.isValid { + return false + } + + inflightAtSend := lastPacketSendState.bytesInFlight + + if inflightAtSend > 0 && b.bytesLostInRound > 0 { + if b.bytesLostInRound > congestion.ByteCount(float64(inflightAtSend)*quicBbr2DefaultLossThreshold) { + return true + } + return false + } + return false +} + +func (b *bbrSender) debugPrint(format string, a ...any) { + fmt.Printf("[BBRSender] [%s] %s\n", + time.Now().Format("15:04:05"), + fmt.Sprintf(format, a...)) +} + +func bdpFromRttAndBandwidth(rtt time.Duration, bandwidth Bandwidth) congestion.ByteCount { + return congestion.ByteCount(rtt) * congestion.ByteCount(bandwidth) / congestion.ByteCount(BytesPerSecond) / congestion.ByteCount(time.Second) +} + +func GetInitialPacketSize(addr net.Addr) congestion.ByteCount { + // If this is not a UDP address, we don't know anything about the MTU. + // Use the minimum size of an Initial packet as the max packet size. + if _, ok := addr.(*net.UDPAddr); ok { + return congestion.InitialPacketSize + } else { + return congestion.MinInitialPacketSize + } +} + +func formatSpeed(bw Bandwidth) string { + bwf := float64(bw) + units := []string{"bps", "Kbps", "Mbps", "Gbps"} + unitIndex := 0 + for bwf > 1000 && unitIndex < len(units)-1 { + bwf /= 1000 + unitIndex++ + } + return fmt.Sprintf("%.2f %s", bwf, units[unitIndex]) +} diff --git a/transport/internet/hysteria/congestion/bbr/clock.go b/transport/internet/hysteria/congestion/bbr/clock.go new file mode 100644 index 00000000..fee7df6a --- /dev/null +++ b/transport/internet/hysteria/congestion/bbr/clock.go @@ -0,0 +1,18 @@ +package bbr + +import "github.com/apernet/quic-go/congestion" + +// A Clock returns the current time +type Clock interface { + Now() congestion.Time +} + +// DefaultClock implements the Clock interface using the Go stdlib clock. +type DefaultClock struct{} + +var _ Clock = DefaultClock{} + +// Now gets the current time +func (DefaultClock) Now() congestion.Time { + return congestion.Now() +} diff --git a/transport/internet/hysteria/congestion/bbr/packet_number_indexed_queue.go b/transport/internet/hysteria/congestion/bbr/packet_number_indexed_queue.go new file mode 100644 index 00000000..08b99dea --- /dev/null +++ b/transport/internet/hysteria/congestion/bbr/packet_number_indexed_queue.go @@ -0,0 +1,199 @@ +package bbr + +import ( + "github.com/apernet/quic-go/congestion" +) + +// packetNumberIndexedQueue is a queue of mostly continuous numbered entries +// which supports the following operations: +// - adding elements to the end of the queue, or at some point past the end +// - removing elements in any order +// - retrieving elements +// If all elements are inserted in order, all of the operations above are +// amortized O(1) time. +// +// Internally, the data structure is a deque where each element is marked as +// present or not. The deque starts at the lowest present index. Whenever an +// element is removed, it's marked as not present, and the front of the deque is +// cleared of elements that are not present. +// +// The tail of the queue is not cleared due to the assumption of entries being +// inserted in order, though removing all elements of the queue will return it +// to its initial state. +// +// Note that this data structure is inherently hazardous, since an addition of +// just two entries will cause it to consume all of the memory available. +// Because of that, it is not a general-purpose container and should not be used +// as one. + +type entryWrapper[T any] struct { + present bool + entry T +} + +type packetNumberIndexedQueue[T any] struct { + entries RingBuffer[entryWrapper[T]] + numberOfPresentEntries int + firstPacket congestion.PacketNumber +} + +func newPacketNumberIndexedQueue[T any](size int) *packetNumberIndexedQueue[T] { + q := &packetNumberIndexedQueue[T]{ + firstPacket: invalidPacketNumber, + } + + q.entries.Init(size) + + return q +} + +// Emplace inserts data associated |packet_number| into (or past) the end of the +// queue, filling up the missing intermediate entries as necessary. Returns +// true if the element has been inserted successfully, false if it was already +// in the queue or inserted out of order. +func (p *packetNumberIndexedQueue[T]) Emplace(packetNumber congestion.PacketNumber, entry *T) bool { + if packetNumber == invalidPacketNumber || entry == nil { + return false + } + + if p.IsEmpty() { + p.entries.PushBack(entryWrapper[T]{ + present: true, + entry: *entry, + }) + p.numberOfPresentEntries = 1 + p.firstPacket = packetNumber + return true + } + + // Do not allow insertion out-of-order. + if packetNumber <= p.LastPacket() { + return false + } + + // Handle potentially missing elements. + offset := int(packetNumber - p.FirstPacket()) + if gap := offset - p.entries.Len(); gap > 0 { + for i := 0; i < gap; i++ { + p.entries.PushBack(entryWrapper[T]{}) + } + } + + p.entries.PushBack(entryWrapper[T]{ + present: true, + entry: *entry, + }) + p.numberOfPresentEntries++ + return true +} + +// GetEntry Retrieve the entry associated with the packet number. Returns the pointer +// to the entry in case of success, or nullptr if the entry does not exist. +func (p *packetNumberIndexedQueue[T]) GetEntry(packetNumber congestion.PacketNumber) *T { + ew := p.getEntryWraper(packetNumber) + if ew == nil { + return nil + } + + return &ew.entry +} + +// Remove, Same as above, but if an entry is present in the queue, also call f(entry) +// before removing it. +func (p *packetNumberIndexedQueue[T]) Remove(packetNumber congestion.PacketNumber, f func(T)) bool { + ew := p.getEntryWraper(packetNumber) + if ew == nil { + return false + } + if f != nil { + f(ew.entry) + } + ew.present = false + p.numberOfPresentEntries-- + + if packetNumber == p.FirstPacket() { + p.clearup() + } + + return true +} + +// RemoveUpTo, but not including |packet_number|. +// Unused slots in the front are also removed, which means when the function +// returns, |first_packet()| can be larger than |packet_number|. +func (p *packetNumberIndexedQueue[T]) RemoveUpTo(packetNumber congestion.PacketNumber) { + for !p.entries.Empty() && + p.firstPacket != invalidPacketNumber && + p.firstPacket < packetNumber { + if p.entries.Front().present { + p.numberOfPresentEntries-- + } + p.entries.PopFront() + p.firstPacket++ + } + p.clearup() + + return +} + +// IsEmpty return if queue is empty. +func (p *packetNumberIndexedQueue[T]) IsEmpty() bool { + return p.numberOfPresentEntries == 0 +} + +// NumberOfPresentEntries returns the number of entries in the queue. +func (p *packetNumberIndexedQueue[T]) NumberOfPresentEntries() int { + return p.numberOfPresentEntries +} + +// EntrySlotsUsed returns the number of entries allocated in the underlying deque. This is +// proportional to the memory usage of the queue. +func (p *packetNumberIndexedQueue[T]) EntrySlotsUsed() int { + return p.entries.Len() +} + +// FirstPacket returns packet number of the first entry in the queue. +func (p *packetNumberIndexedQueue[T]) FirstPacket() (packetNumber congestion.PacketNumber) { + return p.firstPacket +} + +// LastPacket returns packet number of the last entry ever inserted in the queue. Note that the +// entry in question may have already been removed. Zero if the queue is +// empty. +func (p *packetNumberIndexedQueue[T]) LastPacket() (packetNumber congestion.PacketNumber) { + if p.IsEmpty() { + return invalidPacketNumber + } + + return p.firstPacket + congestion.PacketNumber(p.entries.Len()-1) +} + +func (p *packetNumberIndexedQueue[T]) clearup() { + for !p.entries.Empty() && !p.entries.Front().present { + p.entries.PopFront() + p.firstPacket++ + } + if p.entries.Empty() { + p.firstPacket = invalidPacketNumber + } +} + +func (p *packetNumberIndexedQueue[T]) getEntryWraper(packetNumber congestion.PacketNumber) *entryWrapper[T] { + if packetNumber == invalidPacketNumber || + p.IsEmpty() || + packetNumber < p.firstPacket { + return nil + } + + offset := int(packetNumber - p.firstPacket) + if offset >= p.entries.Len() { + return nil + } + + ew := p.entries.Offset(offset) + if ew == nil || !ew.present { + return nil + } + + return ew +} diff --git a/transport/internet/hysteria/congestion/bbr/ringbuffer.go b/transport/internet/hysteria/congestion/bbr/ringbuffer.go new file mode 100644 index 00000000..ed92d4ce --- /dev/null +++ b/transport/internet/hysteria/congestion/bbr/ringbuffer.go @@ -0,0 +1,118 @@ +package bbr + +// A RingBuffer is a ring buffer. +// It acts as a heap that doesn't cause any allocations. +type RingBuffer[T any] struct { + ring []T + headPos, tailPos int + full bool +} + +// Init preallocs a buffer with a certain size. +func (r *RingBuffer[T]) Init(size int) { + r.ring = make([]T, size) +} + +// Len returns the number of elements in the ring buffer. +func (r *RingBuffer[T]) Len() int { + if r.full { + return len(r.ring) + } + if r.tailPos >= r.headPos { + return r.tailPos - r.headPos + } + return r.tailPos - r.headPos + len(r.ring) +} + +// Empty says if the ring buffer is empty. +func (r *RingBuffer[T]) Empty() bool { + return !r.full && r.headPos == r.tailPos +} + +// PushBack adds a new element. +// If the ring buffer is full, its capacity is increased first. +func (r *RingBuffer[T]) PushBack(t T) { + if r.full || len(r.ring) == 0 { + r.grow() + } + r.ring[r.tailPos] = t + r.tailPos++ + if r.tailPos == len(r.ring) { + r.tailPos = 0 + } + if r.tailPos == r.headPos { + r.full = true + } +} + +// PopFront returns the next element. +// It must not be called when the buffer is empty, that means that +// callers might need to check if there are elements in the buffer first. +func (r *RingBuffer[T]) PopFront() T { + if r.Empty() { + panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: pop from an empty queue") + } + r.full = false + t := r.ring[r.headPos] + r.ring[r.headPos] = *new(T) + r.headPos++ + if r.headPos == len(r.ring) { + r.headPos = 0 + } + return t +} + +// Offset returns the offset element. +// It must not be called when the buffer is empty, that means that +// callers might need to check if there are elements in the buffer first +// and check if the index larger than buffer length. +func (r *RingBuffer[T]) Offset(index int) *T { + if r.Empty() || index >= r.Len() { + panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: offset from invalid index") + } + offset := (r.headPos + index) % len(r.ring) + return &r.ring[offset] +} + +// Front returns the front element. +// It must not be called when the buffer is empty, that means that +// callers might need to check if there are elements in the buffer first. +func (r *RingBuffer[T]) Front() *T { + if r.Empty() { + panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: front from an empty queue") + } + return &r.ring[r.headPos] +} + +// Back returns the back element. +// It must not be called when the buffer is empty, that means that +// callers might need to check if there are elements in the buffer first. +func (r *RingBuffer[T]) Back() *T { + if r.Empty() { + panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: back from an empty queue") + } + return r.Offset(r.Len() - 1) +} + +// Grow the maximum size of the queue. +// This method assume the queue is full. +func (r *RingBuffer[T]) grow() { + oldRing := r.ring + newSize := len(oldRing) * 2 + if newSize == 0 { + newSize = 1 + } + r.ring = make([]T, newSize) + headLen := copy(r.ring, oldRing[r.headPos:]) + copy(r.ring[headLen:], oldRing[:r.headPos]) + r.headPos, r.tailPos, r.full = 0, len(oldRing), false +} + +// Clear removes all elements. +func (r *RingBuffer[T]) Clear() { + var zeroValue T + for i := range r.ring { + r.ring[i] = zeroValue + } + r.headPos, r.tailPos, r.full = 0, 0, false +} diff --git a/transport/internet/hysteria/congestion/bbr/windowed_filter.go b/transport/internet/hysteria/congestion/bbr/windowed_filter.go new file mode 100644 index 00000000..4773bce5 --- /dev/null +++ b/transport/internet/hysteria/congestion/bbr/windowed_filter.go @@ -0,0 +1,162 @@ +package bbr + +import ( + "golang.org/x/exp/constraints" +) + +// Implements Kathleen Nichols' algorithm for tracking the minimum (or maximum) +// estimate of a stream of samples over some fixed time interval. (E.g., +// the minimum RTT over the past five minutes.) The algorithm keeps track of +// the best, second best, and third best min (or max) estimates, maintaining an +// invariant that the measurement time of the n'th best >= n-1'th best. + +// The algorithm works as follows. On a reset, all three estimates are set to +// the same sample. The second best estimate is then recorded in the second +// quarter of the window, and a third best estimate is recorded in the second +// half of the window, bounding the worst case error when the true min is +// monotonically increasing (or true max is monotonically decreasing) over the +// window. +// +// A new best sample replaces all three estimates, since the new best is lower +// (or higher) than everything else in the window and it is the most recent. +// The window thus effectively gets reset on every new min. The same property +// holds true for second best and third best estimates. Specifically, when a +// sample arrives that is better than the second best but not better than the +// best, it replaces the second and third best estimates but not the best +// estimate. Similarly, a sample that is better than the third best estimate +// but not the other estimates replaces only the third best estimate. +// +// Finally, when the best expires, it is replaced by the second best, which in +// turn is replaced by the third best. The newest sample replaces the third +// best. + +type WindowedFilterValue interface { + any +} + +type WindowedFilterTime interface { + constraints.Integer | constraints.Float +} + +type WindowedFilter[V WindowedFilterValue, T WindowedFilterTime] struct { + // Time length of window. + windowLength T + estimates []entry[V, T] + comparator func(V, V) int +} + +type entry[V WindowedFilterValue, T WindowedFilterTime] struct { + sample V + time T +} + +// Compares two values and returns true if the first is greater than or equal +// to the second. +func MaxFilter[O constraints.Ordered](a, b O) int { + if a > b { + return 1 + } else if a < b { + return -1 + } + return 0 +} + +// Compares two values and returns true if the first is less than or equal +// to the second. +func MinFilter[O constraints.Ordered](a, b O) int { + if a < b { + return 1 + } else if a > b { + return -1 + } + return 0 +} + +func NewWindowedFilter[V WindowedFilterValue, T WindowedFilterTime](windowLength T, comparator func(V, V) int) *WindowedFilter[V, T] { + return &WindowedFilter[V, T]{ + windowLength: windowLength, + estimates: make([]entry[V, T], 3, 3), + comparator: comparator, + } +} + +// Changes the window length. Does not update any current samples. +func (f *WindowedFilter[V, T]) SetWindowLength(windowLength T) { + f.windowLength = windowLength +} + +func (f *WindowedFilter[V, T]) GetBest() V { + return f.estimates[0].sample +} + +func (f *WindowedFilter[V, T]) GetSecondBest() V { + return f.estimates[1].sample +} + +func (f *WindowedFilter[V, T]) GetThirdBest() V { + return f.estimates[2].sample +} + +// Updates best estimates with |sample|, and expires and updates best +// estimates as necessary. +func (f *WindowedFilter[V, T]) Update(newSample V, newTime T) { + // Reset all estimates if they have not yet been initialized, if new sample + // is a new best, or if the newest recorded estimate is too old. + if f.comparator(f.estimates[0].sample, *new(V)) == 0 || + f.comparator(newSample, f.estimates[0].sample) >= 0 || + newTime-f.estimates[2].time > f.windowLength { + f.Reset(newSample, newTime) + return + } + + if f.comparator(newSample, f.estimates[1].sample) >= 0 { + f.estimates[1] = entry[V, T]{newSample, newTime} + f.estimates[2] = f.estimates[1] + } else if f.comparator(newSample, f.estimates[2].sample) >= 0 { + f.estimates[2] = entry[V, T]{newSample, newTime} + } + + // Expire and update estimates as necessary. + if newTime-f.estimates[0].time > f.windowLength { + // The best estimate hasn't been updated for an entire window, so promote + // second and third best estimates. + f.estimates[0] = f.estimates[1] + f.estimates[1] = f.estimates[2] + f.estimates[2] = entry[V, T]{newSample, newTime} + // Need to iterate one more time. Check if the new best estimate is + // outside the window as well, since it may also have been recorded a + // long time ago. Don't need to iterate once more since we cover that + // case at the beginning of the method. + if newTime-f.estimates[0].time > f.windowLength { + f.estimates[0] = f.estimates[1] + f.estimates[1] = f.estimates[2] + } + return + } + if f.comparator(f.estimates[1].sample, f.estimates[0].sample) == 0 && + newTime-f.estimates[1].time > f.windowLength/4 { + // A quarter of the window has passed without a better sample, so the + // second-best estimate is taken from the second quarter of the window. + f.estimates[1] = entry[V, T]{newSample, newTime} + f.estimates[2] = f.estimates[1] + return + } + + if f.comparator(f.estimates[2].sample, f.estimates[1].sample) == 0 && + newTime-f.estimates[2].time > f.windowLength/2 { + // We've passed a half of the window without a better estimate, so take + // a third-best estimate from the second half of the window. + f.estimates[2] = entry[V, T]{newSample, newTime} + } +} + +// Resets all estimates to new sample. +func (f *WindowedFilter[V, T]) Reset(newSample V, newTime T) { + f.estimates[2] = entry[V, T]{newSample, newTime} + f.estimates[1] = f.estimates[2] + f.estimates[0] = f.estimates[1] +} + +func (f *WindowedFilter[V, T]) Clear() { + f.estimates = make([]entry[V, T], 3, 3) +} diff --git a/transport/internet/hysteria/congestion/brutal/brutal.go b/transport/internet/hysteria/congestion/brutal/brutal.go new file mode 100644 index 00000000..ba0bfda8 --- /dev/null +++ b/transport/internet/hysteria/congestion/brutal/brutal.go @@ -0,0 +1,185 @@ +package brutal + +import ( + "fmt" + "os" + "strconv" + "time" + + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/common" + + "github.com/apernet/quic-go/congestion" +) + +const ( + pktInfoSlotCount = 5 // slot index is based on seconds, so this is basically how many seconds we sample + minSampleCount = 50 + minAckRate = 0.8 + congestionWindowMultiplier = 2 + + debugEnv = "HYSTERIA_BRUTAL_DEBUG" + debugPrintInterval = 2 +) + +var _ congestion.CongestionControl = &BrutalSender{} + +type BrutalSender struct { + rttStats congestion.RTTStatsProvider + bps congestion.ByteCount + maxDatagramSize congestion.ByteCount + pacer *common.Pacer + + pktInfoSlots [pktInfoSlotCount]pktInfo + ackRate float64 + + debug bool + lastAckPrintTimestamp int64 +} + +type pktInfo struct { + Timestamp int64 + AckCount uint64 + LossCount uint64 +} + +func NewBrutalSender(bps uint64) *BrutalSender { + debug, _ := strconv.ParseBool(os.Getenv(debugEnv)) + bs := &BrutalSender{ + bps: congestion.ByteCount(bps), + maxDatagramSize: congestion.InitialPacketSize, + ackRate: 1, + debug: debug, + } + bs.pacer = common.NewPacer(func() congestion.ByteCount { + return congestion.ByteCount(float64(bs.bps) / bs.ackRate) + }) + return bs +} + +func (b *BrutalSender) SetRTTStatsProvider(rttStats congestion.RTTStatsProvider) { + b.rttStats = rttStats +} + +func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) congestion.Time { + return b.pacer.TimeUntilSend() +} + +func (b *BrutalSender) HasPacingBudget(now congestion.Time) bool { + return b.pacer.Budget(now) >= b.maxDatagramSize +} + +func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool { + return bytesInFlight <= b.GetCongestionWindow() +} + +func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount { + rtt := b.rttStats.SmoothedRTT() + if rtt <= 0 { + return 10240 + } + cwnd := congestion.ByteCount(float64(b.bps) * rtt.Seconds() * congestionWindowMultiplier / b.ackRate) + if cwnd < b.maxDatagramSize { + cwnd = b.maxDatagramSize + } + return cwnd +} + +func (b *BrutalSender) OnPacketSent(sentTime congestion.Time, bytesInFlight congestion.ByteCount, + packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool, +) { + b.pacer.SentPacket(sentTime, bytes) +} + +func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount, + priorInFlight congestion.ByteCount, eventTime congestion.Time, +) { + // Stub +} + +func (b *BrutalSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes congestion.ByteCount, + priorInFlight congestion.ByteCount, +) { + // Stub +} + +func (b *BrutalSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime congestion.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { + currentTimestamp := int64(time.Duration(eventTime) / time.Second) + slot := currentTimestamp % pktInfoSlotCount + if b.pktInfoSlots[slot].Timestamp == currentTimestamp { + b.pktInfoSlots[slot].LossCount += uint64(len(lostPackets)) + b.pktInfoSlots[slot].AckCount += uint64(len(ackedPackets)) + } else { + // uninitialized slot or too old, reset + b.pktInfoSlots[slot].Timestamp = currentTimestamp + b.pktInfoSlots[slot].AckCount = uint64(len(ackedPackets)) + b.pktInfoSlots[slot].LossCount = uint64(len(lostPackets)) + } + b.updateAckRate(currentTimestamp) +} + +func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) { + b.maxDatagramSize = size + b.pacer.SetMaxDatagramSize(size) + if b.debug { + b.debugPrint("SetMaxDatagramSize: %d", size) + } +} + +func (b *BrutalSender) updateAckRate(currentTimestamp int64) { + minTimestamp := currentTimestamp - pktInfoSlotCount + var ackCount, lossCount uint64 + for _, info := range b.pktInfoSlots { + if info.Timestamp < minTimestamp { + continue + } + ackCount += info.AckCount + lossCount += info.LossCount + } + if ackCount+lossCount < minSampleCount { + b.ackRate = 1 + if b.canPrintAckRate(currentTimestamp) { + b.lastAckPrintTimestamp = currentTimestamp + b.debugPrint("Not enough samples (total=%d, ack=%d, loss=%d, rtt=%d)", + ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds()) + } + return + } + rate := float64(ackCount) / float64(ackCount+lossCount) + if rate < minAckRate { + b.ackRate = minAckRate + if b.canPrintAckRate(currentTimestamp) { + b.lastAckPrintTimestamp = currentTimestamp + b.debugPrint("ACK rate too low: %.2f, clamped to %.2f (total=%d, ack=%d, loss=%d, rtt=%d)", + rate, minAckRate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds()) + } + return + } + b.ackRate = rate + if b.canPrintAckRate(currentTimestamp) { + b.lastAckPrintTimestamp = currentTimestamp + b.debugPrint("ACK rate: %.2f (total=%d, ack=%d, loss=%d, rtt=%d)", + rate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds()) + } +} + +func (b *BrutalSender) InSlowStart() bool { + return false +} + +func (b *BrutalSender) InRecovery() bool { + return false +} + +func (b *BrutalSender) MaybeExitSlowStart() {} + +func (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {} + +func (b *BrutalSender) canPrintAckRate(currentTimestamp int64) bool { + return b.debug && currentTimestamp-b.lastAckPrintTimestamp >= debugPrintInterval +} + +func (b *BrutalSender) debugPrint(format string, a ...any) { + fmt.Printf("[BrutalSender] [%s] %s\n", + time.Now().Format("15:04:05"), + fmt.Sprintf(format, a...)) +} diff --git a/transport/internet/hysteria/congestion/common/pacer.go b/transport/internet/hysteria/congestion/common/pacer.go new file mode 100644 index 00000000..779c2f1d --- /dev/null +++ b/transport/internet/hysteria/congestion/common/pacer.go @@ -0,0 +1,79 @@ +package common + +import ( + "time" + + "github.com/apernet/quic-go/congestion" +) + +const ( + maxBurstPackets = 10 + maxBurstPacingDelayMultiplier = 4 +) + +// Pacer implements a token bucket pacing algorithm. +type Pacer struct { + budgetAtLastSent congestion.ByteCount + maxDatagramSize congestion.ByteCount + lastSentTime congestion.Time + getBandwidth func() congestion.ByteCount // in bytes/s +} + +func NewPacer(getBandwidth func() congestion.ByteCount) *Pacer { + p := &Pacer{ + budgetAtLastSent: maxBurstPackets * congestion.InitialPacketSize, + maxDatagramSize: congestion.InitialPacketSize, + getBandwidth: getBandwidth, + } + return p +} + +func (p *Pacer) SentPacket(sendTime congestion.Time, size congestion.ByteCount) { + budget := p.Budget(sendTime) + if size > budget { + p.budgetAtLastSent = 0 + } else { + p.budgetAtLastSent = budget - size + } + p.lastSentTime = sendTime +} + +func (p *Pacer) Budget(now congestion.Time) congestion.ByteCount { + if p.lastSentTime.IsZero() { + return p.maxBurstSize() + } + budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9 + if budget < 0 { // protect against overflows + budget = congestion.ByteCount(1<<62 - 1) + } + return min(p.maxBurstSize(), budget) +} + +func (p *Pacer) maxBurstSize() congestion.ByteCount { + return max( + congestion.ByteCount((maxBurstPacingDelayMultiplier*congestion.MinPacingDelay).Nanoseconds())*p.getBandwidth()/1e9, + maxBurstPackets*p.maxDatagramSize, + ) +} + +// TimeUntilSend returns when the next packet should be sent. +// It returns the zero value if a packet can be sent immediately. +func (p *Pacer) TimeUntilSend() congestion.Time { + if p.budgetAtLastSent >= p.maxDatagramSize { + return 0 + } + diff := 1e9 * uint64(p.maxDatagramSize-p.budgetAtLastSent) + bw := uint64(p.getBandwidth()) + // We might need to round up this value. + // Otherwise, we might have a budget (slightly) smaller than the datagram size when the timer expires. + d := diff / bw + // this is effectively a math.Ceil, but using only integer math + if diff%bw > 0 { + d++ + } + return p.lastSentTime.Add(max(congestion.MinPacingDelay, time.Duration(d)*time.Nanosecond)) +} + +func (p *Pacer) SetMaxDatagramSize(s congestion.ByteCount) { + p.maxDatagramSize = s +} diff --git a/transport/internet/hysteria/congestion/utils.go b/transport/internet/hysteria/congestion/utils.go new file mode 100644 index 00000000..1036760e --- /dev/null +++ b/transport/internet/hysteria/congestion/utils.go @@ -0,0 +1,18 @@ +package congestion + +import ( + "github.com/apernet/quic-go" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/brutal" +) + +func UseBBR(conn *quic.Conn) { + conn.SetCongestionControl(bbr.NewBbrSender( + bbr.DefaultClock{}, + bbr.GetInitialPacketSize(conn.RemoteAddr()), + )) +} + +func UseBrutal(conn *quic.Conn, tx uint64) { + conn.SetCongestionControl(brutal.NewBrutalSender(tx)) +} diff --git a/transport/internet/hysteria/conn.go b/transport/internet/hysteria/conn.go new file mode 100644 index 00000000..fe1259bb --- /dev/null +++ b/transport/internet/hysteria/conn.go @@ -0,0 +1,101 @@ +package hysteria + +import ( + "encoding/binary" + "io" + "time" + + "github.com/apernet/quic-go" + "github.com/xtls/xray-core/common/net" +) + +type interConn struct { + stream *quic.Stream + local net.Addr + remote net.Addr +} + +func (i *interConn) Read(b []byte) (int, error) { + return i.stream.Read(b) +} + +func (i *interConn) Write(b []byte) (int, error) { + return i.stream.Write(b) +} + +func (i *interConn) Close() error { + return i.stream.Close() +} + +func (i *interConn) LocalAddr() net.Addr { + return i.local +} + +func (i *interConn) RemoteAddr() net.Addr { + return i.remote +} + +func (i *interConn) SetDeadline(t time.Time) error { + return i.stream.SetDeadline(t) +} + +func (i *interConn) SetReadDeadline(t time.Time) error { + return i.stream.SetReadDeadline(t) +} + +func (i *interConn) SetWriteDeadline(t time.Time) error { + return i.stream.SetWriteDeadline(t) +} + +type InterUdpConn struct { + conn *quic.Conn + local net.Addr + remote net.Addr + + id uint32 + ch chan []byte + closed bool + closeFunc func() +} + +func (i *InterUdpConn) Read(p []byte) (int, error) { + b, ok := <-i.ch + if !ok { + return 0, io.EOF + } + n := copy(p, b) + return n, nil +} + +func (i *InterUdpConn) Write(p []byte) (int, error) { + binary.BigEndian.PutUint32(p, i.id) + if err := i.conn.SendDatagram(p); err != nil { + return 0, err + } + return len(p), nil +} + +func (i *InterUdpConn) Close() error { + i.closeFunc() + return nil +} + +func (i *InterUdpConn) LocalAddr() net.Addr { + return i.local +} + +func (i *InterUdpConn) RemoteAddr() net.Addr { + return i.remote +} + +func (i *InterUdpConn) SetDeadline(t time.Time) error { + return nil +} + +func (i *InterUdpConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (i *InterUdpConn) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/transport/internet/hysteria/dialer.go b/transport/internet/hysteria/dialer.go new file mode 100644 index 00000000..5a694aa4 --- /dev/null +++ b/transport/internet/hysteria/dialer.go @@ -0,0 +1,410 @@ +package hysteria + +import ( + "context" + go_tls "crypto/tls" + "encoding/binary" + "math/rand" + "net/http" + "net/url" + "strconv" + "sync" + "time" + + "github.com/apernet/quic-go" + "github.com/apernet/quic-go/http3" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/finalmask" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion" + "github.com/xtls/xray-core/transport/internet/hysteria/udphop" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" +) + +type udpSessionManager struct { + conn *quic.Conn + m map[uint32]*InterUdpConn + nextId uint32 + closed bool + mutex sync.RWMutex +} + +func (m *udpSessionManager) run() { + for { + d, err := m.conn.ReceiveDatagram(context.Background()) + if err != nil { + break + } + + if len(d) < 4 { + continue + } + sessionId := binary.BigEndian.Uint32(d[:4]) + + m.feed(sessionId, d) + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + m.closed = true + for _, udpConn := range m.m { + m.close(udpConn) + } +} + +func (m *udpSessionManager) close(udpConn *InterUdpConn) { + if !udpConn.closed { + udpConn.closed = true + close(udpConn.ch) + delete(m.m, udpConn.id) + } +} + +func (m *udpSessionManager) udp() (*InterUdpConn, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + if m.closed { + return nil, errors.New("closed") + } + + udpConn := &InterUdpConn{ + conn: m.conn, + local: m.conn.LocalAddr(), + remote: m.conn.RemoteAddr(), + + id: m.nextId, + ch: make(chan []byte, udpMessageChanSize), + } + udpConn.closeFunc = func() { + m.mutex.Lock() + defer m.mutex.Unlock() + m.close(udpConn) + } + m.m[m.nextId] = udpConn + m.nextId++ + + return udpConn, nil +} + +func (m *udpSessionManager) feed(sessionId uint32, d []byte) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + udpConn, ok := m.m[sessionId] + if !ok { + return + } + + select { + case udpConn.ch <- d: + default: + } +} + +type client struct { + ctx context.Context + dest net.Destination + pktConn net.PacketConn + conn *quic.Conn + config *Config + tlsConfig *go_tls.Config + udpmaskManager *finalmask.UdpmaskManager + socketConfig *internet.SocketConfig + udpSM *udpSessionManager + mutex sync.Mutex +} + +func (c *client) status() Status { + if c.conn == nil { + return StatusUnknown + } + select { + case <-c.conn.Context().Done(): + return StatusInactive + default: + return StatusActive + } +} + +func (c *client) close() { + _ = c.conn.CloseWithError(closeErrCodeOK, "") + _ = c.pktConn.Close() + c.pktConn = nil + c.conn = nil + c.udpSM = nil +} + +func (c *client) dial() error { + status := c.status() + if status == StatusActive { + return nil + } + if status == StatusInactive { + c.close() + } + + var index int + if len(c.config.Ports) > 0 { + index = rand.Intn(len(c.config.Ports)) + c.dest.Port = net.Port(c.config.Ports[index]) + } + + raw, err := internet.DialSystem(c.ctx, c.dest, c.socketConfig) + if err != nil { + return errors.New("failed to dial to dest").Base(err) + } + + remote := raw.RemoteAddr() + + pktConn, ok := raw.(net.PacketConn) + if !ok { + raw.Close() + return errors.New("raw is not PacketConn") + } + + if len(c.config.Ports) > 0 { + addr := &udphop.UDPHopAddr{ + IP: remote.(*net.UDPAddr).IP, + Ports: c.config.Ports, + } + pktConn, err = udphop.NewUDPHopPacketConn(addr, time.Duration(c.config.Interval)*time.Second, c.udphopDialer, pktConn, index) + if err != nil { + return errors.New("udphop err").Base(err) + } + } + + if c.udpmaskManager != nil { + pktConn, err = c.udpmaskManager.WrapPacketConnClient(pktConn) + if err != nil { + return errors.New("mask err").Base(err) + } + } + + var quicConn *quic.Conn + rt := &http3.Transport{ + TLSClientConfig: c.tlsConfig, + QUICConfig: &quic.Config{ + InitialStreamReceiveWindow: c.config.InitStreamReceiveWindow, + MaxStreamReceiveWindow: c.config.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: c.config.InitConnReceiveWindow, + MaxConnectionReceiveWindow: c.config.MaxConnReceiveWindow, + MaxIdleTimeout: time.Duration(c.config.MaxIdleTimeout) * time.Second, + KeepAlivePeriod: time.Duration(c.config.KeepAlivePeriod) * time.Second, + DisablePathMTUDiscovery: c.config.DisablePathMtuDiscovery, + EnableDatagrams: true, + MaxDatagramFrameSize: MaxDatagramFrameSize, + DisablePathManager: true, + }, + Dial: func(ctx context.Context, _ string, tlsCfg *go_tls.Config, cfg *quic.Config) (*quic.Conn, error) { + qc, err := quic.DialEarly(ctx, pktConn, remote, tlsCfg, cfg) + if err != nil { + return nil, err + } + quicConn = qc + return qc, nil + }, + } + req := &http.Request{ + Method: http.MethodPost, + URL: &url.URL{ + Scheme: "https", + Host: URLHost, + Path: URLPath, + }, + Header: http.Header{ + RequestHeaderAuth: []string{c.config.Auth}, + CommonHeaderCCRX: []string{strconv.FormatUint(c.config.Down, 10)}, + CommonHeaderPadding: []string{authRequestPadding.String()}, + }, + } + resp, err := rt.RoundTrip(req) + if err != nil { + if quicConn != nil { + _ = quicConn.CloseWithError(closeErrCodeProtocolError, "") + } + _ = pktConn.Close() + return errors.New("RoundTrip err").Base(err) + } + if resp.StatusCode != StatusAuthOK { + _ = quicConn.CloseWithError(closeErrCodeProtocolError, "") + _ = pktConn.Close() + return errors.New("auth failed") + } + _ = resp.Body.Close() + + serverUdp, _ := strconv.ParseBool(resp.Header.Get(ResponseHeaderUDPEnabled)) + serverAuto := resp.Header.Get(CommonHeaderCCRX) + serverDown, _ := strconv.ParseUint(serverAuto, 10, 64) + + if serverAuto == "auto" || c.config.Up == 0 || serverDown == 0 { + congestion.UseBBR(quicConn) + } else { + congestion.UseBrutal(quicConn, min(c.config.Up, serverDown)) + } + + c.pktConn = pktConn + c.conn = quicConn + if serverUdp { + c.udpSM = &udpSessionManager{ + conn: quicConn, + m: make(map[uint32]*InterUdpConn), + nextId: 1, + } + go c.udpSM.run() + } + + return nil +} + +func (c *client) clean() { + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.status() == StatusInactive { + c.close() + } +} + +func (c *client) tcp() (stat.Connection, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + err := c.dial() + if err != nil { + return nil, err + } + + stream, err := c.conn.OpenStream() + if err != nil { + return nil, err + } + + return &interConn{ + stream: stream, + local: c.conn.LocalAddr(), + remote: c.conn.RemoteAddr(), + }, nil +} + +func (c *client) udp() (stat.Connection, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + err := c.dial() + if err != nil { + return nil, err + } + + if c.udpSM == nil { + return nil, errors.New("server does not support udp") + } + + return c.udpSM.udp() +} + +func (c *client) setCtx(ctx context.Context) { + c.mutex.Lock() + defer c.mutex.Unlock() + + c.ctx = ctx +} + +func (c *client) udphopDialer(addr *net.UDPAddr) (net.PacketConn, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.status() != StatusActive { + errors.LogDebug(c.ctx, "stop hop on disconnected QUIC waiting to be closed") + return nil, errors.New() + } + + raw, err := internet.DialSystem(c.ctx, net.DestinationFromAddr(addr), c.socketConfig) + if err != nil { + errors.LogDebug(c.ctx, "failed to dial to dest skip hop") + return nil, errors.New() + } + + pktConn, ok := raw.(net.PacketConn) + if !ok { + errors.LogDebug(c.ctx, "raw is not PacketConn skip hop") + raw.Close() + return nil, errors.New() + } + + return pktConn, nil +} + +type clientManager struct { + m map[string]*client + mutex sync.Mutex +} + +func (m *clientManager) clean() { + m.mutex.Lock() + defer m.mutex.Unlock() + + for _, c := range m.m { + c.clean() + } +} + +var manger *clientManager + +func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { + tlsConfig := tls.ConfigFromStreamSettings(streamSettings) + if tlsConfig == nil { + return nil, errors.New("tls config is nil") + } + + addr := dest.NetAddr() + config := streamSettings.ProtocolSettings.(*Config) + + manger.mutex.Lock() + c, ok := manger.m[addr] + if !ok { + dest.Network = net.Network_UDP + c = &client{ + ctx: ctx, + dest: dest, + config: config, + tlsConfig: tlsConfig.GetTLSConfig(), + udpmaskManager: streamSettings.UdpmaskManager, + socketConfig: streamSettings.SocketSettings, + } + manger.m[addr] = c + } + c.setCtx(ctx) + manger.mutex.Unlock() + + outbounds := session.OutboundsFromContext(ctx) + targetUdp := len(outbounds) > 0 && outbounds[len(outbounds)-1].Target.Network == net.Network_UDP + + if targetUdp { + return c.udp() + } + return c.tcp() +} + +func init() { + manger = &clientManager{ + m: make(map[string]*client), + } + (&task.Periodic{ + Interval: 30 * time.Second, + Execute: func() error { + manger.clean() + return nil + }, + }).Start() +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} diff --git a/transport/internet/hysteria/padding/padding.go b/transport/internet/hysteria/padding/padding.go new file mode 100644 index 00000000..b134601e --- /dev/null +++ b/transport/internet/hysteria/padding/padding.go @@ -0,0 +1,24 @@ +package padding + +import ( + "math/rand" +) + +const ( + paddingChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +) + +// padding specifies a half-open range [Min, Max). +type Padding struct { + Min int + Max int +} + +func (p Padding) String() string { + n := p.Min + rand.Intn(p.Max-p.Min) + bs := make([]byte, n) + for i := range bs { + bs[i] = paddingChars[rand.Intn(len(paddingChars))] + } + return string(bs) +} diff --git a/transport/internet/hysteria/udphop/addr.go b/transport/internet/hysteria/udphop/addr.go new file mode 100644 index 00000000..70dae2a2 --- /dev/null +++ b/transport/internet/hysteria/udphop/addr.go @@ -0,0 +1,65 @@ +package udphop + +import ( + "fmt" + "net" +) + +type InvalidPortError struct { + PortStr string +} + +func (e InvalidPortError) Error() string { + return fmt.Sprintf("%s is not a valid port number or range", e.PortStr) +} + +// UDPHopAddr contains an IP address and a list of ports. +type UDPHopAddr struct { + IP net.IP + Ports []uint32 + PortStr string +} + +func (a *UDPHopAddr) Network() string { + return "udphop" +} + +func (a *UDPHopAddr) String() string { + return net.JoinHostPort(a.IP.String(), a.PortStr) +} + +// addrs returns a list of net.Addr's, one for each port. +func (a *UDPHopAddr) addrs() ([]net.Addr, error) { + var addrs []net.Addr + for _, port := range a.Ports { + addr := &net.UDPAddr{ + IP: a.IP, + Port: int(port), + } + addrs = append(addrs, addr) + } + return addrs, nil +} + +// func ResolveUDPHopAddr(addr string) (*UDPHopAddr, error) { +// host, portStr, err := net.SplitHostPort(addr) +// if err != nil { +// return nil, err +// } +// ip, err := net.ResolveIPAddr("ip", host) +// if err != nil { +// return nil, err +// } +// result := &UDPHopAddr{ +// IP: ip.IP, +// PortStr: portStr, +// } + +// pu := utils.ParsePortUnion(portStr) +// if pu == nil { +// return nil, InvalidPortError{portStr} +// } +// result.Ports = pu.Ports() + +// return result, nil +// } diff --git a/transport/internet/hysteria/udphop/conn.go b/transport/internet/hysteria/udphop/conn.go new file mode 100644 index 00000000..5615ec5b --- /dev/null +++ b/transport/internet/hysteria/udphop/conn.go @@ -0,0 +1,297 @@ +package udphop + +import ( + "errors" + "math/rand" + "net" + "sync" + "syscall" + "time" +) + +const ( + packetQueueSize = 1024 + udpBufferSize = 2048 // QUIC packets are at most 1500 bytes long, so 2k should be more than enough + + defaultHopInterval = 30 * time.Second +) + +type udpHopPacketConn struct { + Addr net.Addr + Addrs []net.Addr + HopInterval time.Duration + ListenUDPFunc ListenUDPFunc + + connMutex sync.RWMutex + prevConn net.PacketConn + currentConn net.PacketConn + addrIndex int + + readBufferSize int + writeBufferSize int + + recvQueue chan *udpPacket + closeChan chan struct{} + closed bool + + bufPool sync.Pool +} + +type udpPacket struct { + Buf []byte + N int + Addr net.Addr + Err error +} + +type ListenUDPFunc = func(*net.UDPAddr) (net.PacketConn, error) + +func NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval time.Duration, listenUDPFunc ListenUDPFunc, pktConn net.PacketConn, index int) (net.PacketConn, error) { + if hopInterval == 0 { + hopInterval = defaultHopInterval + } else if hopInterval < 5*time.Second { + return nil, errors.New("hop interval must be at least 5 seconds") + } + // if listenUDPFunc == nil { + // listenUDPFunc = func() (net.PacketConn, error) { + // return net.ListenUDP("udp", nil) + // } + // } + if listenUDPFunc == nil { + return nil, errors.New("nil listenUDPFunc") + } + addrs, err := addr.addrs() + if err != nil { + return nil, err + } + // curConn, err := listenUDPFunc() + // if err != nil { + // return nil, err + // } + hConn := &udpHopPacketConn{ + Addr: addr, + Addrs: addrs, + HopInterval: hopInterval, + ListenUDPFunc: listenUDPFunc, + prevConn: nil, + currentConn: pktConn, + addrIndex: index, + recvQueue: make(chan *udpPacket, packetQueueSize), + closeChan: make(chan struct{}), + bufPool: sync.Pool{ + New: func() interface{} { + return make([]byte, udpBufferSize) + }, + }, + } + go hConn.recvLoop(pktConn) + go hConn.hopLoop() + return hConn, nil +} + +func (u *udpHopPacketConn) recvLoop(conn net.PacketConn) { + for { + buf := u.bufPool.Get().([]byte) + n, addr, err := conn.ReadFrom(buf) + if err != nil { + u.bufPool.Put(buf) + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + // Only pass through timeout errors here, not permanent errors + // like connection closed. Connection close is normal as we close + // the old connection to exit this loop every time we hop. + u.recvQueue <- &udpPacket{nil, 0, nil, netErr} + } + return + } + select { + case u.recvQueue <- &udpPacket{buf, n, addr, nil}: + // Packet successfully queued + default: + // Queue is full, drop the packet + u.bufPool.Put(buf) + } + } +} + +func (u *udpHopPacketConn) hopLoop() { + ticker := time.NewTicker(u.HopInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + u.hop() + case <-u.closeChan: + return + } + } +} + +func (u *udpHopPacketConn) hop() { + u.connMutex.Lock() + defer u.connMutex.Unlock() + if u.closed { + return + } + // Update addrIndex to a new random value + u.addrIndex = rand.Intn(len(u.Addrs)) + newConn, err := u.ListenUDPFunc(u.Addrs[u.addrIndex].(*net.UDPAddr)) + if err != nil { + // Could be temporary, just skip this hop + return + } + // We need to keep receiving packets from the previous connection, + // because otherwise there will be packet loss due to the time gap + // between we hop to a new port and the server acknowledges this change. + // So we do the following: + // Close prevConn, + // move currentConn to prevConn, + // set newConn as currentConn, + // start recvLoop on newConn. + if u.prevConn != nil { + _ = u.prevConn.Close() // recvLoop for this conn will exit + } + u.prevConn = u.currentConn + u.currentConn = newConn + // Set buffer sizes if previously set + if u.readBufferSize > 0 { + _ = trySetReadBuffer(u.currentConn, u.readBufferSize) + } + if u.writeBufferSize > 0 { + _ = trySetWriteBuffer(u.currentConn, u.writeBufferSize) + } + go u.recvLoop(newConn) +} + +func (u *udpHopPacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + for { + select { + case p := <-u.recvQueue: + if p.Err != nil { + return 0, nil, p.Err + } + // Currently we do not check whether the packet is from + // the server or not due to performance reasons. + n := copy(b, p.Buf[:p.N]) + u.bufPool.Put(p.Buf) + return n, u.Addr, nil + case <-u.closeChan: + return 0, nil, net.ErrClosed + } + } +} + +func (u *udpHopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + if u.closed { + return 0, net.ErrClosed + } + // Skip the check for now, always write to the server, + // for the same reason as in ReadFrom. + return u.currentConn.WriteTo(b, u.Addrs[u.addrIndex]) +} + +func (u *udpHopPacketConn) Close() error { + u.connMutex.Lock() + defer u.connMutex.Unlock() + if u.closed { + return nil + } + // Close prevConn and currentConn + // Close closeChan to unblock ReadFrom & hopLoop + // Set closed flag to true to prevent double close + if u.prevConn != nil { + _ = u.prevConn.Close() + } + err := u.currentConn.Close() + close(u.closeChan) + u.closed = true + u.Addrs = nil // For GC + return err +} + +func (u *udpHopPacketConn) LocalAddr() net.Addr { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + return u.currentConn.LocalAddr() +} + +func (u *udpHopPacketConn) SetDeadline(t time.Time) error { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + if u.prevConn != nil { + _ = u.prevConn.SetDeadline(t) + } + return u.currentConn.SetDeadline(t) +} + +func (u *udpHopPacketConn) SetReadDeadline(t time.Time) error { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + if u.prevConn != nil { + _ = u.prevConn.SetReadDeadline(t) + } + return u.currentConn.SetReadDeadline(t) +} + +func (u *udpHopPacketConn) SetWriteDeadline(t time.Time) error { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + if u.prevConn != nil { + _ = u.prevConn.SetWriteDeadline(t) + } + return u.currentConn.SetWriteDeadline(t) +} + +// UDP-specific methods below + +func (u *udpHopPacketConn) SetReadBuffer(bytes int) error { + u.connMutex.Lock() + defer u.connMutex.Unlock() + u.readBufferSize = bytes + if u.prevConn != nil { + _ = trySetReadBuffer(u.prevConn, bytes) + } + return trySetReadBuffer(u.currentConn, bytes) +} + +func (u *udpHopPacketConn) SetWriteBuffer(bytes int) error { + u.connMutex.Lock() + defer u.connMutex.Unlock() + u.writeBufferSize = bytes + if u.prevConn != nil { + _ = trySetWriteBuffer(u.prevConn, bytes) + } + return trySetWriteBuffer(u.currentConn, bytes) +} + +func (u *udpHopPacketConn) SyscallConn() (syscall.RawConn, error) { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + sc, ok := u.currentConn.(syscall.Conn) + if !ok { + return nil, errors.New("not supported") + } + return sc.SyscallConn() +} + +func trySetReadBuffer(pc net.PacketConn, bytes int) error { + sc, ok := pc.(interface { + SetReadBuffer(bytes int) error + }) + if ok { + return sc.SetReadBuffer(bytes) + } + return nil +} + +func trySetWriteBuffer(pc net.PacketConn, bytes int) error { + sc, ok := pc.(interface { + SetWriteBuffer(bytes int) error + }) + if ok { + return sc.SetWriteBuffer(bytes) + } + return nil +} diff --git a/transport/internet/hysteria/utils/portunion.go b/transport/internet/hysteria/utils/portunion.go new file mode 100644 index 00000000..f76a6fd0 --- /dev/null +++ b/transport/internet/hysteria/utils/portunion.go @@ -0,0 +1,107 @@ +package utils + +import ( + "sort" + "strconv" + "strings" +) + +// PortUnion is a collection of multiple port ranges. +type PortUnion []PortRange + +// PortRange represents a range of ports. +// Start and End are inclusive. [Start, End] +type PortRange struct { + Start, End uint16 +} + +// ParsePortUnion parses a string of comma-separated port ranges (or single ports) into a PortUnion. +// Returns nil if the input is invalid. +// The returned PortUnion is guaranteed to be normalized. +func ParsePortUnion(s string) PortUnion { + if s == "all" || s == "*" { + // Wildcard special case + return PortUnion{PortRange{0, 65535}} + } + var result PortUnion + portStrs := strings.Split(s, ",") + for _, portStr := range portStrs { + if strings.Contains(portStr, "-") { + // Port range + portRange := strings.Split(portStr, "-") + if len(portRange) != 2 { + return nil + } + start, err := strconv.ParseUint(portRange[0], 10, 16) + if err != nil { + return nil + } + end, err := strconv.ParseUint(portRange[1], 10, 16) + if err != nil { + return nil + } + if start > end { + start, end = end, start + } + result = append(result, PortRange{uint16(start), uint16(end)}) + } else { + // Single port + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return nil + } + result = append(result, PortRange{uint16(port), uint16(port)}) + } + } + if result == nil { + return nil + } + return result.Normalize() +} + +// Normalize normalizes a PortUnion. +// No overlapping ranges, ranges are sorted from low to high. +func (u PortUnion) Normalize() PortUnion { + if len(u) == 0 { + return u + } + sort.Slice(u, func(i, j int) bool { + if u[i].Start == u[j].Start { + return u[i].End < u[j].End + } + return u[i].Start < u[j].Start + }) + normalized := PortUnion{u[0]} + for _, current := range u[1:] { + last := &normalized[len(normalized)-1] + if uint32(current.Start) <= uint32(last.End)+1 { + if current.End > last.End { + last.End = current.End + } + } else { + normalized = append(normalized, current) + } + } + return normalized +} + +// Ports returns all ports in the PortUnion as a slice. +func (u PortUnion) Ports() []uint16 { + var ports []uint16 + for _, r := range u { + for i := uint32(r.Start); i <= uint32(r.End); i++ { + ports = append(ports, uint16(i)) + } + } + return ports +} + +// Contains returns true if the PortUnion contains the given port. +func (u PortUnion) Contains(port uint16) bool { + for _, r := range u { + if port >= r.Start && port <= r.End { + return true + } + } + return false +} diff --git a/transport/internet/hysteria/utils/portunion_test.go b/transport/internet/hysteria/utils/portunion_test.go new file mode 100644 index 00000000..ba056a37 --- /dev/null +++ b/transport/internet/hysteria/utils/portunion_test.go @@ -0,0 +1,150 @@ +package utils + +import ( + "reflect" + "slices" + "testing" +) + +func TestParsePortUnion(t *testing.T) { + tests := []struct { + name string + s string + want PortUnion + }{ + { + name: "empty", + s: "", + want: nil, + }, + { + name: "all 1", + s: "all", + want: PortUnion{{0, 65535}}, + }, + { + name: "all 2", + s: "*", + want: PortUnion{{0, 65535}}, + }, + { + name: "single port", + s: "1234", + want: PortUnion{{1234, 1234}}, + }, + { + name: "multiple ports (unsorted)", + s: "5678,1234,9012", + want: PortUnion{{1234, 1234}, {5678, 5678}, {9012, 9012}}, + }, + { + name: "one range", + s: "1234-1240", + want: PortUnion{{1234, 1240}}, + }, + { + name: "one range (reversed)", + s: "1240-1234", + want: PortUnion{{1234, 1240}}, + }, + { + name: "multiple ports and ranges (reversed, unsorted, overlapping)", + s: "5678,1200-1236,9100-9012,1234-1240", + want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}}, + }, + { + name: "multiple ports and ranges with 65535 (reversed, unsorted, overlapping)", + s: "5678,1200-1236,65531-65535,65532-65534,9100-9012,1234-1240", + want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}, {65531, 65535}}, + }, + { + name: "multiple ports and ranges with 65535 (reversed, unsorted, overlapping) 2", + s: "5678,1200-1236,65532-65535,65531-65534,9100-9012,1234-1240", + want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}, {65531, 65535}}, + }, + { + name: "invalid 1", + s: "1234-", + want: nil, + }, + { + name: "invalid 2", + s: "1234-ggez", + want: nil, + }, + { + name: "invalid 3", + s: "233,", + want: nil, + }, + { + name: "invalid 4", + s: "1234-1240-1250", + want: nil, + }, + { + name: "invalid 5", + s: "-,,", + want: nil, + }, + { + name: "invalid 6", + s: "http", + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ParsePortUnion(tt.s); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParsePortUnion() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPortUnion_Ports(t *testing.T) { + tests := []struct { + name string + pu PortUnion + want []uint16 + }{ + { + name: "single port", + pu: PortUnion{{1234, 1234}}, + want: []uint16{1234}, + }, + { + name: "multiple ports", + pu: PortUnion{{1234, 1236}}, + want: []uint16{1234, 1235, 1236}, + }, + { + name: "multiple ports and ranges", + pu: PortUnion{{1234, 1236}, {5678, 5680}, {9000, 9002}}, + want: []uint16{1234, 1235, 1236, 5678, 5679, 5680, 9000, 9001, 9002}, + }, + { + name: "single port 65535", + pu: PortUnion{{65535, 65535}}, + want: []uint16{65535}, + }, + { + name: "port range with 65535", + pu: PortUnion{{65530, 65535}}, + want: []uint16{65530, 65531, 65532, 65533, 65534, 65535}, + }, + { + name: "multiple ports and ranges with 65535", + pu: PortUnion{{65530, 65535}, {1234, 1236}}, + want: []uint16{65530, 65531, 65532, 65533, 65534, 65535, 1234, 1235, 1236}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.pu.Ports(); !slices.Equal(got, tt.want) { + t.Errorf("PortUnion.Ports() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/transport/internet/memory_settings.go b/transport/internet/memory_settings.go index f1331353..db2b0d1f 100644 --- a/transport/internet/memory_settings.go +++ b/transport/internet/memory_settings.go @@ -1,6 +1,9 @@ package internet -import "github.com/xtls/xray-core/common/net" +import ( + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet/finalmask" +) // MemoryStreamConfig is a parsed form of StreamConfig. It is used to reduce the number of Protobuf parses. type MemoryStreamConfig struct { @@ -9,6 +12,8 @@ type MemoryStreamConfig struct { ProtocolSettings interface{} SecurityType string SecuritySettings interface{} + TcpmaskManager *finalmask.TcpmaskManager + UdpmaskManager *finalmask.UdpmaskManager SocketSettings *SocketConfig DownloadSettings *MemoryStreamConfig } @@ -45,5 +50,29 @@ func ToMemoryStreamConfig(s *StreamConfig) (*MemoryStreamConfig, error) { mss.SecuritySettings = ess } + if s != nil && len(s.Tcpmasks) > 0 { + var masks []finalmask.Tcpmask + for _, msg := range s.Tcpmasks { + instance, err := msg.GetInstance() + if err != nil { + return nil, err + } + masks = append(masks, instance.(finalmask.Tcpmask)) + } + mss.TcpmaskManager = finalmask.NewTcpmaskManager(masks) + } + + if s != nil && len(s.Udpmasks) > 0 { + var masks []finalmask.Udpmask + for _, msg := range s.Udpmasks { + instance, err := msg.GetInstance() + if err != nil { + return nil, err + } + masks = append(masks, instance.(finalmask.Udpmask)) + } + mss.UdpmaskManager = finalmask.NewUdpmaskManager(masks) + } + return mss, nil } diff --git a/transport/internet/splithttp/dialer.go b/transport/internet/splithttp/dialer.go index 3f6e9ea3..45b770e7 100644 --- a/transport/internet/splithttp/dialer.go +++ b/transport/internet/splithttp/dialer.go @@ -13,8 +13,8 @@ import ( "sync/atomic" "time" - "github.com/quic-go/quic-go" - "github.com/quic-go/quic-go/http3" + "github.com/apernet/quic-go" + "github.com/apernet/quic-go/http3" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" diff --git a/transport/internet/splithttp/hub.go b/transport/internet/splithttp/hub.go index 5e9b9408..442f196f 100644 --- a/transport/internet/splithttp/hub.go +++ b/transport/internet/splithttp/hub.go @@ -12,8 +12,8 @@ import ( "sync" "time" - "github.com/quic-go/quic-go" - "github.com/quic-go/quic-go/http3" + "github.com/apernet/quic-go" + "github.com/apernet/quic-go/http3" goreality "github.com/xtls/reality" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" From ef2a967f1291936caa90347ee1ba5e99adea7475 Mon Sep 17 00:00:00 2001 From: Owersun <4807375+Owersun@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:06:53 +0100 Subject: [PATCH 095/136] TUN inbound: Close connection when handling is done (#5531) https://github.com/XTLS/Xray-core/pull/5531#issuecomment-3744446015 --- proxy/tun/handler.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/proxy/tun/handler.go b/proxy/tun/handler.go index 2b73aad9..5cf21a75 100644 --- a/proxy/tun/handler.go +++ b/proxy/tun/handler.go @@ -102,6 +102,10 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin // HandleConnection pass the connection coming from the ip stack to the routing dispatcher func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) { + // when handling is done with any outcome, always signal back to the incoming connection + // to close, send completion packets back to the network, and cleanup + defer conn.Close() + sid := session.NewID() ctx := c.ContextWithID(t.ctx, sid) From 4bdf6e5c92fa1fd66594d3a441441fb1ebd0488b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Tue, 13 Jan 2026 22:35:24 +0800 Subject: [PATCH 096/136] TLS client: Verify leaf cert (name, time) when pinning self-signed CA (#5532) https://github.com/XTLS/Xray-core/pull/5154#issuecomment-3732159602 --- transport/internet/tls/config.go | 45 +++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index a25ad6ca..0fcbad57 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -290,8 +290,10 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509 // directly return success if pinned cert is leaf // or add the CA to RootCAs if pinned cert is CA(and can be used in VerifyPeerCertInNames for Self signed CA) RootCAs := r.RootCAs + var verifyResult verifyResult + var verifiedCert *x509.Certificate if r.PinnedPeerCertSha256 != nil { - verifyResult, verifiedCert := verifyChain(certs, r.PinnedPeerCertSha256) + verifyResult, verifiedCert = verifyChain(certs, r.PinnedPeerCertSha256) switch verifyResult { case certNotFound: return errors.New("peer cert is unrecognized") @@ -305,27 +307,39 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509 } } - if r.VerifyPeerCertInNames != nil { - if len(r.VerifyPeerCertInNames) > 0 { - opts := x509.VerifyOptions{ - Roots: RootCAs, - CurrentTime: time.Now(), - Intermediates: x509.NewCertPool(), - } - for _, cert := range certs[1:] { - opts.Intermediates.AddCert(cert) - } - for _, opts.DNSName = range r.VerifyPeerCertInNames { - if _, err := certs[0].Verify(opts); err == nil { - return nil - } + if len(r.VerifyPeerCertInNames) > 0 { + opts := x509.VerifyOptions{ + Roots: RootCAs, + CurrentTime: time.Now(), + Intermediates: x509.NewCertPool(), + } + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + for _, opts.DNSName = range r.VerifyPeerCertInNames { + if _, err := certs[0].Verify(opts); err == nil { + return nil } } + } else if len(verifiedChains) == 0 && verifyResult == foundCA { // if found ca and verifiedChains is empty, we need to verify here + opts := x509.VerifyOptions{ + Roots: RootCAs, + CurrentTime: time.Now(), + Intermediates: x509.NewCertPool(), + DNSName: r.Config.ServerName, + } + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + if _, err := certs[0].Verify(opts); err == nil { + return nil + } } return nil } type RandCarrier struct { + Config *tls.Config RootCAs *x509.CertPool VerifyPeerCertInNames []string PinnedPeerCertSha256 [][]byte @@ -366,6 +380,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { SessionTicketsDisabled: !c.EnableSessionResumption, VerifyPeerCertificate: randCarrier.verifyPeerCert, } + randCarrier.Config = config if len(c.VerifyPeerCertInNames) > 0 { config.InsecureSkipVerify = true } else { From 9a121a489b01b1af46f43cfb060fe88c89f1570b Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:32:01 +0000 Subject: [PATCH 097/136] v26.1.13 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 --- core/core.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/core.go b/core/core.go index 96624d1d..c6f0edb8 100644 --- a/core/core.go +++ b/core/core.go @@ -17,9 +17,9 @@ import ( ) var ( - Version_x byte = 25 - Version_y byte = 12 - Version_z byte = 8 + Version_x byte = 26 + Version_y byte = 1 + Version_z byte = 13 ) var ( From 0443de77980c8258b0bc7ce6b5068066f9aa4d99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 08:24:12 +0000 Subject: [PATCH 098/136] Bump github.com/refraction-networking/utls from 1.8.1 to 1.8.2 (#5535) Bumps [github.com/refraction-networking/utls](https://github.com/refraction-networking/utls) from 1.8.1 to 1.8.2. - [Release notes](https://github.com/refraction-networking/utls/releases) - [Commits](https://github.com/refraction-networking/utls/compare/v1.8.1...v1.8.2) --- updated-dependencies: - dependency-name: github.com/refraction-networking/utls dependency-version: 1.8.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f6bfcb79..fef388ad 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/miekg/dns v1.1.70 github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 - github.com/refraction-networking/utls v1.8.1 + github.com/refraction-networking/utls v1.8.2 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 diff --git a/go.sum b/go.sum index 0f8e1428..26bdf1a2 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= -github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= +github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= +github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= From 649e989fa2ebcde4ad2833596aeec7d0a3df4069 Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Wed, 14 Jan 2026 18:42:07 +0800 Subject: [PATCH 099/136] Hysteria: Fix transport's "udphop without salamander" dialing issue; Require `"version": 2` in outbound's `settings` as well (#5537) Updated example: https://github.com/XTLS/Xray-core/pull/5508#issue-3795798712 --- infra/conf/hysteria.go | 8 +++++++- infra/conf/transport_internet.go | 2 +- proxy/hysteria/config.pb.go | 17 +++++++++++++---- proxy/hysteria/config.proto | 3 ++- transport/internet/system_dialer.go | 8 ++++++++ 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/infra/conf/hysteria.go b/infra/conf/hysteria.go index 6512d105..f690c363 100644 --- a/infra/conf/hysteria.go +++ b/infra/conf/hysteria.go @@ -1,19 +1,25 @@ package conf import ( + "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/proxy/hysteria" "google.golang.org/protobuf/proto" ) type HysteriaClientConfig struct { + Version int32 `json:"version"` Address *Address `json:"address"` Port uint16 `json:"port"` } func (c *HysteriaClientConfig) Build() (proto.Message, error) { - config := new(hysteria.ClientConfig) + if c.Version != 2 { + return nil, errors.New("version != 2") + } + config := &hysteria.ClientConfig{} + config.Version = c.Version config.Server = &protocol.ServerEndpoint{ Address: c.Address.Build(), Port: uint32(c.Port), diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 742c7df5..3b21e383 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -453,7 +453,7 @@ func (c *HysteriaConfig) Build() (proto.Message, error) { } config := &hysteria.Config{} - config.Version = int32(c.Version) + config.Version = c.Version config.Auth = c.Auth config.Up = up config.Down = down diff --git a/proxy/hysteria/config.pb.go b/proxy/hysteria/config.pb.go index d58022cc..ef61eb29 100644 --- a/proxy/hysteria/config.pb.go +++ b/proxy/hysteria/config.pb.go @@ -24,7 +24,8 @@ const ( type ClientConfig struct { state protoimpl.MessageState `protogen:"open.v1"` - Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Server *protocol.ServerEndpoint `protobuf:"bytes,2,opt,name=server,proto3" json:"server,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -59,6 +60,13 @@ func (*ClientConfig) Descriptor() ([]byte, []int) { return file_proxy_hysteria_config_proto_rawDescGZIP(), []int{0} } +func (x *ClientConfig) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + func (x *ClientConfig) GetServer() *protocol.ServerEndpoint { if x != nil { return x.Server @@ -70,9 +78,10 @@ var File_proxy_hysteria_config_proto protoreflect.FileDescriptor const file_proxy_hysteria_config_proto_rawDesc = "" + "\n" + - "\x1bproxy/hysteria/config.proto\x12\x13xray.proxy.hysteria\x1a!common/protocol/server_spec.proto\"L\n" + - "\fClientConfig\x12<\n" + - "\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06serverB[\n" + + "\x1bproxy/hysteria/config.proto\x12\x13xray.proxy.hysteria\x1a!common/protocol/server_spec.proto\"f\n" + + "\fClientConfig\x12\x18\n" + + "\aversion\x18\x01 \x01(\x05R\aversion\x12<\n" + + "\x06server\x18\x02 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06serverB[\n" + "\x17com.xray.proxy.hysteriaP\x01Z(github.com/xtls/xray-core/proxy/hysteria\xaa\x02\x13Xray.Proxy.Hysteriab\x06proto3" var ( diff --git a/proxy/hysteria/config.proto b/proxy/hysteria/config.proto index c54c0ead..2a71ac53 100644 --- a/proxy/hysteria/config.proto +++ b/proxy/hysteria/config.proto @@ -9,5 +9,6 @@ option java_multiple_files = true; import "common/protocol/server_spec.proto"; message ClientConfig { - xray.common.protocol.ServerEndpoint server = 1; + int32 version = 1; + xray.common.protocol.ServerEndpoint server = 2; } diff --git a/transport/internet/system_dialer.go b/transport/internet/system_dialer.go index 27f3c9a9..01567576 100644 --- a/transport/internet/system_dialer.go +++ b/transport/internet/system_dialer.go @@ -195,6 +195,14 @@ func (c *PacketConnWrapper) SetWriteDeadline(t time.Time) error { return c.Conn.SetWriteDeadline(t) } +func (c *PacketConnWrapper) SyscallConn() (syscall.RawConn, error) { + sc, ok := c.Conn.(syscall.Conn) + if !ok { + return nil, syscall.EINVAL + } + return sc.SyscallConn() +} + type SystemDialerAdapter interface { Dial(network string, address string) (net.Conn, error) } From a384be0f8440e19f875848819bbbb6e6a0b80e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Wed, 14 Jan 2026 21:31:43 +0800 Subject: [PATCH 100/136] SS2022 outbound: Fix UDP leak (#5544) Fixes https://github.com/XTLS/Xray-core/issues/5541 --- common/singbridge/packet.go | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/common/singbridge/packet.go b/common/singbridge/packet.go index fef955e7..d4dccb4c 100644 --- a/common/singbridge/packet.go +++ b/common/singbridge/packet.go @@ -2,10 +2,12 @@ package singbridge import ( "context" + "time" B "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" + "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/transport" @@ -29,6 +31,8 @@ type PacketConnWrapper struct { cached buf.MultiBuffer } +// This ReadPacket implemented a timeout to avoid goroutine leak like PipeConnWrapper.Read() +// as a temporarily solution func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (M.Socksaddr, error) { if w.cached != nil { mb, bb := buf.SplitFirst(w.cached) @@ -47,10 +51,30 @@ func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (M.Socksaddr, error) { return ToSocksaddr(destination), nil } } - mb, err := w.ReadMultiBuffer() - if err != nil { - return M.Socksaddr{}, err + + // timeout + type readResult struct { + mb buf.MultiBuffer + err error } + c := make(chan readResult, 1) + go func() { + mb, err := w.ReadMultiBuffer() + c <- readResult{mb: mb, err: err} + }() + var mb buf.MultiBuffer + select { + case <-time.After(60 * time.Second): + common.Close(w.Reader) + common.Interrupt(w.Reader) + return M.Socksaddr{}, buf.ErrReadTimeout + case result := <-c: + if result.err != nil { + return M.Socksaddr{}, result.err + } + mb = result.mb + } + nb, bb := buf.SplitFirst(mb) if bb == nil { return M.Socksaddr{}, nil From 7c418486c86c799ac17b232183b1f8a917d9e83e Mon Sep 17 00:00:00 2001 From: Happ-dev Date: Fri, 16 Jan 2026 13:44:44 +0300 Subject: [PATCH 101/136] README.md: Add Happ RU to iOS & macOS Clients (#5551) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index af45948a..7625a14f 100644 --- a/README.md +++ b/README.md @@ -112,11 +112,11 @@ - [SimpleXray](https://github.com/lhear/SimpleXray) - [AnyPortal](https://github.com/AnyPortal/AnyPortal) - iOS & macOS arm64 & tvOS - - [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) ([tvOS](https://apps.apple.com/us/app/happ-proxy-utility-for-tv/id6748297274)) + - [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) | [Happ RU](https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973) | [Happ tvOS](https://apps.apple.com/us/app/happ-proxy-utility-for-tv/id6748297274) - [Streisand](https://apps.apple.com/app/streisand/id6450534064) - [OneXray](https://github.com/OneXray/OneXray) - macOS arm64 & x64 - - [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) + - [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) | [Happ RU](https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973) - [V2rayU](https://github.com/yanue/V2rayU) - [V2RayXS](https://github.com/tzmax/V2RayXS) - [Furious](https://github.com/LorenEteval/Furious) From d75b33a3a336b16826dd8a2b5e3b03eb44c0164b Mon Sep 17 00:00:00 2001 From: nobody <59990325+vrnobody@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:55:43 +0800 Subject: [PATCH 102/136] Commands: "xray run -dump" supports reading JSON from STDIN (#5550) Fixes https://github.com/XTLS/Xray-core/issues/5534 --- core/config.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/config.go b/core/config.go index 8f2018e6..3ab2a376 100644 --- a/core/config.go +++ b/core/config.go @@ -64,7 +64,11 @@ func GetMergedConfig(args cmdarg.Arg) (string, error) { var files []*ConfigSource supported := []string{"json", "yaml", "toml"} for _, file := range args { - format := GetFormat(file) + format := "json" + if file != "stdin:" { + format = GetFormat(file) + } + if slices.Contains(supported, format) { files = append(files, &ConfigSource{ Name: file, From 760223ad70f822fcb859a217e60e038a970610f9 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:23:39 +0000 Subject: [PATCH 103/136] TLS client: Skip TLS' built-in verification when using `pinnedPeerCertSha256`; Fixes https://github.com/XTLS/Xray-core/pull/5532#issuecomment-3745598515 https://github.com/XTLS/Xray-core/pull/5532#issuecomment-3759930283 https://github.com/XTLS/Xray-core/pull/5532#issuecomment-3760057266 https://github.com/XTLS/Xray-core/pull/5532#issuecomment-3760540231 --- transport/internet/tls/config.go | 48 ++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index 0fcbad57..a68ad9a5 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -281,35 +281,41 @@ func (c *Config) parseServerName() string { } func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) (err error) { - // extract x509 certificates from rawCerts(verifiedChains will be nil if InsecureSkipVerify is true) + // extract x509 certificates from rawCerts (verifiedChains will be nil if InsecureSkipVerify is true) certs := make([]*x509.Certificate, len(rawCerts)) for i, asn1Data := range rawCerts { certs[i], _ = x509.ParseCertificate(asn1Data) } + if len(certs) == 0 { + return errors.New("unexpected certs") + } + if certs[0].IsCA { + slices.Reverse(certs) + } // directly return success if pinned cert is leaf - // or add the CA to RootCAs if pinned cert is CA(and can be used in VerifyPeerCertInNames for Self signed CA) - RootCAs := r.RootCAs + // or replace RootCAs if pinned cert is CA (and can be used in VerifyPeerCertInNames) + CAs := r.RootCAs var verifyResult verifyResult var verifiedCert *x509.Certificate if r.PinnedPeerCertSha256 != nil { verifyResult, verifiedCert = verifyChain(certs, r.PinnedPeerCertSha256) switch verifyResult { case certNotFound: - return errors.New("peer cert is unrecognized") + return errors.New("peer cert is unrecognized (againsts pinnedPeerCertSha256)") case foundLeaf: return nil case foundCA: - RootCAs = x509.NewCertPool() - RootCAs.AddCert(verifiedCert) + CAs = x509.NewCertPool() + CAs.AddCert(verifiedCert) default: - panic("impossible PinnedPeerCertificateSha256 verify result") + panic("impossible pinnedPeerCertSha256 verify result") } } - if len(r.VerifyPeerCertInNames) > 0 { + if r.VerifyPeerCertInNames != nil { // RAW's Dial() may make it empty but not nil opts := x509.VerifyOptions{ - Roots: RootCAs, + Roots: CAs, CurrentTime: time.Now(), Intermediates: x509.NewCertPool(), } @@ -321,9 +327,15 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509 return nil } } - } else if len(verifiedChains) == 0 && verifyResult == foundCA { // if found ca and verifiedChains is empty, we need to verify here + if verifyResult == foundCA { + errors.New("peer cert is invalid (againsts pinned CA and verifyPeerCertInNames)") + } + return errors.New("peer cert is invalid (againsts root CAs and verifyPeerCertInNames)") + } + + if verifyResult == foundCA { // if found CA, we need to verify here opts := x509.VerifyOptions{ - Roots: RootCAs, + Roots: CAs, CurrentTime: time.Now(), Intermediates: x509.NewCertPool(), DNSName: r.Config.ServerName, @@ -334,8 +346,10 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509 if _, err := certs[0].Verify(opts); err == nil { return nil } + return errors.New("peer cert is invalid (againsts pinned CA and serverName)") } - return nil + + return nil // len(r.PinnedPeerCertSha256)==nil && len(r.VerifyPeerCertInNames)==nil } type RandCarrier struct { @@ -386,6 +400,11 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { } else { randCarrier.VerifyPeerCertInNames = nil } + if len(c.PinnedPeerCertSha256) > 0 { + config.InsecureSkipVerify = true + } else { + randCarrier.PinnedPeerCertSha256 = nil + } for _, opt := range opts { opt(config) @@ -540,17 +559,16 @@ const ( foundCA ) -func verifyChain(certs []*x509.Certificate, PinnedPeerCertificateSha256 [][]byte) (verifyResult, *x509.Certificate) { +func verifyChain(certs []*x509.Certificate, pinnedPeerCertSha256 [][]byte) (verifyResult, *x509.Certificate) { for _, cert := range certs { certHash := GenerateCertHash(cert) - for _, c := range PinnedPeerCertificateSha256 { + for _, c := range pinnedPeerCertSha256 { if hmac.Equal(certHash, c) { if cert.IsCA { return foundCA, cert } else { return foundLeaf, cert } - } } } From 09f619d67ccddfe35b106e0fc7ac9485ff6aa622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sat, 17 Jan 2026 17:42:06 +0800 Subject: [PATCH 104/136] TLS client: Add pin_test.go for leaf and CA (#5553) https://github.com/XTLS/Xray-core/pull/5532#issuecomment-3760231005 --- transport/internet/tls/pin_test.go | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/transport/internet/tls/pin_test.go b/transport/internet/tls/pin_test.go index e0558c45..a13b12da 100644 --- a/transport/internet/tls/pin_test.go +++ b/transport/internet/tls/pin_test.go @@ -1,12 +1,15 @@ package tls import ( + "crypto/tls" "crypto/x509" "encoding/hex" "encoding/pem" "testing" "github.com/stretchr/testify/assert" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/protocol/tls/cert" ) func TestCalculateCertHash(t *testing.T) { @@ -95,3 +98,60 @@ uI6HqHFD3iEct8fBkYfQiwH2e1eu9OwgujiWHsutyK8VvzVB3/YnhQ/TzciRjPqz assert.Equal(t, fingerprint, hash) }) } + +func TestVerifyPeerLeafCert(t *testing.T) { + leafCert := cert.MustGenerate(nil, cert.DNSNames("example.com")) + leaf := common.Must2(x509.ParseCertificate(leafCert.Certificate)) + + caHash := GenerateCertHash(leafCert.Certificate) + + r := &RandCarrier{ + Config: &tls.Config{ + ServerName: "example.com", + }, + PinnedPeerCertSha256: [][]byte{caHash}, + } + + rawCerts := [][]byte{leaf.Raw} + err := r.verifyPeerCert(rawCerts, nil) + if err != nil { + t.Fatal("expected to verify leaf cert signed by pinned CA, but got error:", err) + } + + // make the pinned hash incorrect + r.PinnedPeerCertSha256[0][0] += 1 + err = r.verifyPeerCert(rawCerts, nil) + if err == nil { + t.Fatal("expected to fail verifying leaf cert with incorrect pinned CA hash, but got no error") + } +} + +func TestVerifyPeerCACert(t *testing.T) { + caCert := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign)) + ca := common.Must2(x509.ParseCertificate(caCert.Certificate)) + + leafCert := cert.MustGenerate(caCert, cert.DNSNames("example.com")) + leaf := common.Must2(x509.ParseCertificate(leafCert.Certificate)) + + caHash := GenerateCertHash(ca) + + r := &RandCarrier{ + Config: &tls.Config{ + ServerName: "example.com", + }, + PinnedPeerCertSha256: [][]byte{caHash}, + } + + rawCerts := [][]byte{leaf.Raw, ca.Raw} + err := r.verifyPeerCert(rawCerts, nil) + if err != nil { + t.Fatal("expected to verify leaf cert signed by pinned CA, but got error:", err) + } + + // make the pinned hash incorrect + r.PinnedPeerCertSha256[0][0] += 1 + err = r.verifyPeerCert(rawCerts, nil) + if err == nil { + t.Fatal("expected to fail verifying leaf cert with incorrect pinned CA hash, but got no error") + } +} From 5f7474120f523ad1e36174481e0b16c3446cc29c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sat, 17 Jan 2026 20:33:40 +0800 Subject: [PATCH 105/136] Geofiles: Revert related changes for now, waiting for better changes (#5557) Reverts https://github.com/XTLS/Xray-core/commit/5d94a62a83073bc4aa378fb7e2198e15878e90da https://github.com/XTLS/Xray-core/commit/c715154309e41e759f544edd3667889e257fb3af https://github.com/XTLS/Xray-core/commit/961c352127f32c04034a88d021a683e6a80eac20 https://github.com/XTLS/Xray-core/commit/36425d2a6e53c755c96d25ef2323b7fffd1fab3f Fixes https://github.com/XTLS/Xray-core/issues/5538 https://github.com/XTLS/Xray-core/issues/5536 --- app/dns/dns.go | 95 ------------- app/dns/dns_test.go | 8 +- app/dns/nameserver.go | 15 -- app/router/condition.go | 44 +----- app/router/condition_geoip_test.go | 83 +++++++---- app/router/condition_test.go | 93 ++++-------- app/router/config.go | 103 +------------- common/platform/filesystem/asset_tools.go | 52 ------- common/platform/filesystem/file.go | 26 ---- common/platform/filesystem/file_other.go | 54 ------- common/platform/windows.go | 5 +- infra/conf/dns.go | 19 ++- infra/conf/router.go | 163 ++++++++++++++++------ 13 files changed, 227 insertions(+), 533 deletions(-) delete mode 100644 common/platform/filesystem/asset_tools.go delete mode 100644 common/platform/filesystem/file_other.go diff --git a/app/dns/dns.go b/app/dns/dns.go index 5d6154f9..603640f1 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -12,15 +12,12 @@ import ( "sync" "time" - router "github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/strmatcher" "github.com/xtls/xray-core/features/dns" - "google.golang.org/protobuf/proto" ) // DNS is a DNS rely server. @@ -100,25 +97,6 @@ func New(ctx context.Context, config *Config) (*DNS, error) { } for _, ns := range config.NameServer { - if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { - err := parseDomains(ns) - if err != nil { - return nil, errors.New("failed to parse dns domain rules: ").Base(err) - } - - expectedGeoip, err := router.GetGeoIPList(ns.ExpectedGeoip) - if err != nil { - return nil, errors.New("failed to parse dns expectIPs rules: ").Base(err) - } - ns.ExpectedGeoip = expectedGeoip - - unexpectedGeoip, err := router.GetGeoIPList(ns.UnexpectedGeoip) - if err != nil { - return nil, errors.New("failed to parse dns unexpectedGeoip rules: ").Base(err) - } - ns.UnexpectedGeoip = unexpectedGeoip - - } domainRuleCount += len(ns.PrioritizedDomain) } @@ -602,76 +580,3 @@ func detectGUIPlatform() bool { } return false } - -func parseDomains(ns *NameServer) error { - pureDomains := []*router.Domain{} - - // convert to pure domain - for _, pd := range ns.PrioritizedDomain { - pureDomains = append(pureDomains, &router.Domain{ - Type: router.Domain_Type(pd.Type), - Value: pd.Domain, - }) - } - - domainList := []*router.Domain{} - for _, domain := range pureDomains { - val := strings.Split(domain.Value, "_") - if len(val) >= 2 { - - fileName := val[0] - code := val[1] - - bs, err := filesystem.ReadAsset(fileName) - if err != nil { - return errors.New("failed to load file: ", fileName).Base(err) - } - bs = filesystem.Find(bs, []byte(code)) - var geosite router.GeoSite - - if err := proto.Unmarshal(bs, &geosite); err != nil { - return errors.New("failed Unmarshal :").Base(err) - } - - // parse attr - if len(val) == 3 { - siteWithAttr := strings.Split(val[2], ",") - attrs := router.ParseAttrs(siteWithAttr) - if !attrs.IsEmpty() { - filteredDomains := make([]*router.Domain, 0, len(pureDomains)) - for _, domain := range geosite.Domain { - if attrs.Match(domain) { - filteredDomains = append(filteredDomains, domain) - } - } - geosite.Domain = filteredDomains - } - - } - - domainList = append(domainList, geosite.Domain...) - - // update ns.OriginalRules Size - ruleTag := strings.Join(val, ":") - for i, oRule := range ns.OriginalRules { - if oRule.Rule == strings.ToLower(ruleTag) { - ns.OriginalRules[i].Size = uint32(len(geosite.Domain)) - } - } - - } else { - domainList = append(domainList, domain) - } - } - - // convert back to NameServer_PriorityDomain - ns.PrioritizedDomain = []*NameServer_PriorityDomain{} - for _, pd := range domainList { - ns.PrioritizedDomain = append(ns.PrioritizedDomain, &NameServer_PriorityDomain{ - Type: ToDomainMatchingType(pd.Type), - Domain: pd.Value, - }) - } - - return nil -} diff --git a/app/dns/dns_test.go b/app/dns/dns_test.go index d103704c..cb70b0b3 100644 --- a/app/dns/dns_test.go +++ b/app/dns/dns_test.go @@ -541,7 +541,7 @@ func TestIPMatch(t *testing.T) { }, ExpectedGeoip: []*router.GeoIP{ { - // local + CountryCode: "local", Cidr: []*router.CIDR{ { // inner ip, will not match @@ -565,7 +565,7 @@ func TestIPMatch(t *testing.T) { }, ExpectedGeoip: []*router.GeoIP{ { - // test + CountryCode: "test", Cidr: []*router.CIDR{ { Ip: []byte{8, 8, 8, 8}, @@ -574,7 +574,7 @@ func TestIPMatch(t *testing.T) { }, }, { - // test + CountryCode: "test", Cidr: []*router.CIDR{ { Ip: []byte{8, 8, 8, 4}, @@ -669,7 +669,7 @@ func TestLocalDomain(t *testing.T) { }, ExpectedGeoip: []*router.GeoIP{ { // Will match localhost, localhost-a and localhost-b, - // local + CountryCode: "local", Cidr: []*router.CIDR{ {Ip: []byte{127, 0, 0, 2}, Prefix: 32}, {Ip: []byte{127, 0, 0, 3}, Prefix: 32}, diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go index e57b74bf..bad9277c 100644 --- a/app/dns/nameserver.go +++ b/app/dns/nameserver.go @@ -297,18 +297,3 @@ func ResolveIpOptionOverride(queryStrategy QueryStrategy, ipOption dns.IPOption) return ipOption } } - -func ToDomainMatchingType(t router.Domain_Type) DomainMatchingType { - switch t { - case router.Domain_Domain: - return DomainMatchingType_Subdomain - case router.Domain_Full: - return DomainMatchingType_Full - case router.Domain_Plain: - return DomainMatchingType_Keyword - case router.Domain_Regex: - return DomainMatchingType_Regex - default: - panic("unknown domain type") - } -} diff --git a/app/router/condition.go b/app/router/condition.go index 245ef6c2..ca4f9605 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -309,48 +309,6 @@ func (m *AttributeMatcher) Apply(ctx routing.Context) bool { return m.Match(attributes) } -// Geo attribute -type GeoAttributeMatcher interface { - Match(*Domain) bool -} - -type GeoBooleanMatcher string - -func (m GeoBooleanMatcher) Match(domain *Domain) bool { - for _, attr := range domain.Attribute { - if attr.Key == string(m) { - return true - } - } - return false -} - -type GeoAttributeList struct { - Matcher []GeoAttributeMatcher -} - -func (al *GeoAttributeList) Match(domain *Domain) bool { - for _, matcher := range al.Matcher { - if !matcher.Match(domain) { - return false - } - } - return true -} - -func (al *GeoAttributeList) IsEmpty() bool { - return len(al.Matcher) == 0 -} - -func ParseAttrs(attrs []string) *GeoAttributeList { - al := new(GeoAttributeList) - for _, attr := range attrs { - lc := strings.ToLower(attr) - al.Matcher = append(al.Matcher, GeoBooleanMatcher(lc)) - } - return al -} - type ProcessNameMatcher struct { ProcessNames []string AbsPaths []string @@ -439,4 +397,4 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool { } } return false -} \ No newline at end of file +} diff --git a/app/router/condition_geoip_test.go b/app/router/condition_geoip_test.go index de289a71..b712db9e 100644 --- a/app/router/condition_geoip_test.go +++ b/app/router/condition_geoip_test.go @@ -1,17 +1,40 @@ package router_test import ( + "fmt" "os" "path/filepath" - "runtime" "testing" "github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/platform/filesystem" + "google.golang.org/protobuf/proto" ) +func getAssetPath(file string) (string, error) { + path := platform.GetAssetLocation(file) + _, err := os.Stat(path) + if os.IsNotExist(err) { + path := filepath.Join("..", "..", "resources", file) + _, err := os.Stat(path) + if os.IsNotExist(err) { + return "", fmt.Errorf("can't find %s in standard asset locations or {project_root}/resources", file) + } + if err != nil { + return "", fmt.Errorf("can't stat %s: %v", path, err) + } + return path, nil + } + if err != nil { + return "", fmt.Errorf("can't stat %s: %v", path, err) + } + + return path, nil +} + func TestGeoIPMatcher(t *testing.T) { cidrList := []*router.CIDR{ {Ip: []byte{0, 0, 0, 0}, Prefix: 8}, @@ -159,11 +182,12 @@ func TestGeoIPReverseMatcher(t *testing.T) { } func TestGeoIPMatcher4CN(t *testing.T) { - geo := "geoip:cn" - geoip, err := loadGeoIP(geo) + ips, err := loadGeoIP("CN") common.Must(err) - matcher, err := router.BuildOptimizedGeoIPMatcher(geoip) + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: ips, + }) common.Must(err) if matcher.Match([]byte{8, 8, 8, 8}) { @@ -172,11 +196,12 @@ func TestGeoIPMatcher4CN(t *testing.T) { } func TestGeoIPMatcher6US(t *testing.T) { - geo := "geoip:us" - geoip, err := loadGeoIP(geo) + ips, err := loadGeoIP("US") common.Must(err) - matcher, err := router.BuildOptimizedGeoIPMatcher(geoip) + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: ips, + }) common.Must(err) if !matcher.Match(net.ParseAddress("2001:4860:4860::8888").IP()) { @@ -184,34 +209,37 @@ func TestGeoIPMatcher6US(t *testing.T) { } } -func loadGeoIP(geo string) (*router.GeoIP, error) { - os.Setenv("XRAY_LOCATION_ASSET", filepath.Join("..", "..", "resources")) - - geoip, err := conf.ToCidrList([]string{geo}) +func loadGeoIP(country string) ([]*router.CIDR, error) { + path, err := getAssetPath("geoip.dat") + if err != nil { + return nil, err + } + geoipBytes, err := filesystem.ReadFile(path) if err != nil { return nil, err } - if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { - geoip, err = router.GetGeoIPList(geoip) - if err != nil { - return nil, err + var geoipList router.GeoIPList + if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil { + return nil, err + } + + for _, geoip := range geoipList.Entry { + if geoip.CountryCode == country { + return geoip.Cidr, nil } } - if len(geoip) == 0 { - panic("country not found: " + geo) - } - - return geoip[0], nil + panic("country not found: " + country) } func BenchmarkGeoIPMatcher4CN(b *testing.B) { - geo := "geoip:cn" - geoip, err := loadGeoIP(geo) + ips, err := loadGeoIP("CN") common.Must(err) - matcher, err := router.BuildOptimizedGeoIPMatcher(geoip) + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: ips, + }) common.Must(err) b.ResetTimer() @@ -222,11 +250,12 @@ func BenchmarkGeoIPMatcher4CN(b *testing.B) { } func BenchmarkGeoIPMatcher6US(b *testing.B) { - geo := "geoip:us" - geoip, err := loadGeoIP(geo) + ips, err := loadGeoIP("US") common.Must(err) - matcher, err := router.BuildOptimizedGeoIPMatcher(geoip) + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: ips, + }) common.Must(err) b.ResetTimer() diff --git a/app/router/condition_test.go b/app/router/condition_test.go index 283e6725..1272aef6 100644 --- a/app/router/condition_test.go +++ b/app/router/condition_test.go @@ -1,22 +1,20 @@ package router_test import ( - "os" - "path/filepath" - "runtime" "strconv" "testing" - "github.com/xtls/xray-core/app/router" . "github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol/http" "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/features/routing" routing_session "github.com/xtls/xray-core/features/routing/session" - "github.com/xtls/xray-core/infra/conf" + "google.golang.org/protobuf/proto" ) func withBackground() routing.Context { @@ -302,25 +300,32 @@ func TestRoutingRule(t *testing.T) { } } -func loadGeoSiteDomains(geo string) ([]*Domain, error) { - os.Setenv("XRAY_LOCATION_ASSET", filepath.Join("..", "..", "resources")) - - domains, err := conf.ParseDomainRule(geo) +func loadGeoSite(country string) ([]*Domain, error) { + path, err := getAssetPath("geosite.dat") + if err != nil { + return nil, err + } + geositeBytes, err := filesystem.ReadFile(path) if err != nil { return nil, err } - if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { - domains, err = router.GetDomainList(domains) - if err != nil { - return nil, err + var geositeList GeoSiteList + if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil { + return nil, err + } + + for _, site := range geositeList.Entry { + if site.CountryCode == country { + return site.Domain, nil } } - return domains, nil + + return nil, errors.New("country not found: " + country) } func TestChinaSites(t *testing.T) { - domains, err := loadGeoSiteDomains("geosite:cn") + domains, err := loadGeoSite("CN") common.Must(err) acMatcher, err := NewMphMatcherGroup(domains) @@ -361,50 +366,8 @@ func TestChinaSites(t *testing.T) { } } -func TestChinaSitesWithAttrs(t *testing.T) { - domains, err := loadGeoSiteDomains("geosite:google@cn") - common.Must(err) - - acMatcher, err := NewMphMatcherGroup(domains) - common.Must(err) - - type TestCase struct { - Domain string - Output bool - } - testCases := []TestCase{ - { - Domain: "google.cn", - Output: true, - }, - { - Domain: "recaptcha.net", - Output: true, - }, - { - Domain: "164.com", - Output: false, - }, - { - Domain: "164.com", - Output: false, - }, - } - - for i := 0; i < 1024; i++ { - testCases = append(testCases, TestCase{Domain: strconv.Itoa(i) + ".not-exists.com", Output: false}) - } - - for _, testCase := range testCases { - r := acMatcher.ApplyDomain(testCase.Domain) - if r != testCase.Output { - t.Error("ACDomainMatcher expected output ", testCase.Output, " for domain ", testCase.Domain, " but got ", r) - } - } -} - func BenchmarkMphDomainMatcher(b *testing.B) { - domains, err := loadGeoSiteDomains("geosite:cn") + domains, err := loadGeoSite("CN") common.Must(err) matcher, err := NewMphMatcherGroup(domains) @@ -449,11 +412,11 @@ func BenchmarkMultiGeoIPMatcher(b *testing.B) { var geoips []*GeoIP { - ips, err := loadGeoIP("geoip:cn") + ips, err := loadGeoIP("CN") common.Must(err) geoips = append(geoips, &GeoIP{ CountryCode: "CN", - Cidr: ips.Cidr, + Cidr: ips, }) } @@ -462,25 +425,25 @@ func BenchmarkMultiGeoIPMatcher(b *testing.B) { common.Must(err) geoips = append(geoips, &GeoIP{ CountryCode: "JP", - Cidr: ips.Cidr, + Cidr: ips, }) } { - ips, err := loadGeoIP("geoip:ca") + ips, err := loadGeoIP("CA") common.Must(err) geoips = append(geoips, &GeoIP{ CountryCode: "CA", - Cidr: ips.Cidr, + Cidr: ips, }) } { - ips, err := loadGeoIP("geoip:us") + ips, err := loadGeoIP("US") common.Must(err) geoips = append(geoips, &GeoIP{ CountryCode: "US", - Cidr: ips.Cidr, + Cidr: ips, }) } diff --git a/app/router/config.go b/app/router/config.go index 5399589b..14310641 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -3,14 +3,11 @@ package router import ( "context" "regexp" - "runtime" "strings" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/routing" - "google.golang.org/protobuf/proto" ) type Rule struct { @@ -76,15 +73,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } if len(rr.Geoip) > 0 { - geoip := rr.Geoip - if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { - var err error - geoip, err = GetGeoIPList(rr.Geoip) - if err != nil { - return nil, errors.New("failed to build geoip from mmap").Base(err) - } - } - cond, err := NewIPMatcher(geoip, MatcherAsType_Target) + cond, err := NewIPMatcher(rr.Geoip, MatcherAsType_Target) if err != nil { return nil, err } @@ -109,20 +98,11 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } if len(rr.Domain) > 0 { - domains := rr.Domain - if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { - var err error - domains, err = GetDomainList(rr.Domain) - if err != nil { - return nil, errors.New("failed to build domains from mmap").Base(err) - } - } - - matcher, err := NewMphMatcherGroup(domains) + matcher, err := NewMphMatcherGroup(rr.Domain) if err != nil { return nil, errors.New("failed to build domain condition with MphDomainMatcher").Base(err) } - errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(domains), " domain rule(s)") + errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)") conds.Add(matcher) } @@ -183,80 +163,3 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch return nil, errors.New("unrecognized balancer type") } } - -func GetGeoIPList(ips []*GeoIP) ([]*GeoIP, error) { - geoipList := []*GeoIP{} - for _, ip := range ips { - if ip.CountryCode != "" { - val := strings.Split(ip.CountryCode, "_") - fileName := "geoip.dat" - if len(val) == 2 { - fileName = strings.ToLower(val[0]) - } - bs, err := filesystem.ReadAsset(fileName) - if err != nil { - return nil, errors.New("failed to load file: ", fileName).Base(err) - } - bs = filesystem.Find(bs, []byte(ip.CountryCode)) - - var geoip GeoIP - - if err := proto.Unmarshal(bs, &geoip); err != nil { - return nil, errors.New("failed Unmarshal :").Base(err) - } - geoipList = append(geoipList, &geoip) - - } else { - geoipList = append(geoipList, ip) - } - } - return geoipList, nil - -} - -func GetDomainList(domains []*Domain) ([]*Domain, error) { - domainList := []*Domain{} - for _, domain := range domains { - val := strings.Split(domain.Value, "_") - - if len(val) >= 2 { - - fileName := val[0] - code := val[1] - - bs, err := filesystem.ReadAsset(fileName) - if err != nil { - return nil, errors.New("failed to load file: ", fileName).Base(err) - } - bs = filesystem.Find(bs, []byte(code)) - var geosite GeoSite - - if err := proto.Unmarshal(bs, &geosite); err != nil { - return nil, errors.New("failed Unmarshal :").Base(err) - } - - // parse attr - if len(val) == 3 { - siteWithAttr := strings.Split(val[2], ",") - attrs := ParseAttrs(siteWithAttr) - - if !attrs.IsEmpty() { - filteredDomains := make([]*Domain, 0, len(domains)) - for _, domain := range geosite.Domain { - if attrs.Match(domain) { - filteredDomains = append(filteredDomains, domain) - } - } - geosite.Domain = filteredDomains - } - - } - - domainList = append(domainList, geosite.Domain...) - - } else { - domainList = append(domainList, domain) - } - } - return domainList, nil -} diff --git a/common/platform/filesystem/asset_tools.go b/common/platform/filesystem/asset_tools.go deleted file mode 100644 index 45316f80..00000000 --- a/common/platform/filesystem/asset_tools.go +++ /dev/null @@ -1,52 +0,0 @@ -package filesystem - -func DecodeVarint(buf []byte) (x uint64, n int) { - for shift := uint(0); shift < 64; shift += 7 { - if n >= len(buf) { - return 0, 0 - } - b := uint64(buf[n]) - n++ - x |= (b & 0x7F) << shift - if (b & 0x80) == 0 { - return x, n - } - } - - // The number is too large to represent in a 64-bit value. - return 0, 0 -} - -func Find(data, code []byte) []byte { - codeL := len(code) - if codeL == 0 { - return nil - } - for { - dataL := len(data) - if dataL < 2 { - return nil - } - x, y := DecodeVarint(data[1:]) - if x == 0 && y == 0 { - return nil - } - headL, bodyL := 1+y, int(x) - dataL -= headL - if dataL < bodyL { - return nil - } - data = data[headL:] - if int(data[1]) == codeL { - for i := 0; i < codeL && data[2+i] == code[i]; i++ { - if i+1 == codeL { - return data[:bodyL] - } - } - } - if dataL == bodyL { - return nil - } - data = data[bodyL:] - } -} diff --git a/common/platform/filesystem/file.go b/common/platform/filesystem/file.go index ebe9b0f1..e4fe2a9a 100644 --- a/common/platform/filesystem/file.go +++ b/common/platform/filesystem/file.go @@ -1,12 +1,9 @@ -//go:build !windows && !wasm - package filesystem import ( "io" "os" "path/filepath" - "syscall" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/platform" @@ -19,29 +16,6 @@ var NewFileReader FileReaderFunc = func(path string) (io.ReadCloser, error) { } func ReadFile(path string) ([]byte, error) { - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - - stat, err := file.Stat() - if err != nil { - return nil, err - } - - size := stat.Size() - if size == 0 { - return []byte{}, nil - } - - // use mmap to save RAM - bs, err := syscall.Mmap(int(file.Fd()), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED) - if err == nil { - return bs, nil - } - - // fallback reader, err := NewFileReader(path) if err != nil { return nil, err diff --git a/common/platform/filesystem/file_other.go b/common/platform/filesystem/file_other.go deleted file mode 100644 index d3cbdcc7..00000000 --- a/common/platform/filesystem/file_other.go +++ /dev/null @@ -1,54 +0,0 @@ -//go:build windows || wasm - -package filesystem - -import ( - "io" - "os" - "path/filepath" - - "github.com/xtls/xray-core/common/buf" - "github.com/xtls/xray-core/common/platform" -) - -type FileReaderFunc func(path string) (io.ReadCloser, error) - -var NewFileReader FileReaderFunc = func(path string) (io.ReadCloser, error) { - return os.Open(path) -} - -func ReadFile(path string) ([]byte, error) { - reader, err := NewFileReader(path) - if err != nil { - return nil, err - } - defer reader.Close() - - return buf.ReadAllToBytes(reader) -} - -func ReadAsset(file string) ([]byte, error) { - return ReadFile(platform.GetAssetLocation(file)) -} - -func ReadCert(file string) ([]byte, error) { - if filepath.IsAbs(file) { - return ReadFile(file) - } - return ReadFile(platform.GetCertLocation(file)) -} - -func CopyFile(dst string, src string) error { - bytes, err := ReadFile(src) - if err != nil { - return err - } - f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return err - } - defer f.Close() - - _, err = f.Write(bytes) - return err -} diff --git a/common/platform/windows.go b/common/platform/windows.go index 5bd15520..684ddc9c 100644 --- a/common/platform/windows.go +++ b/common/platform/windows.go @@ -3,9 +3,7 @@ package platform -import ( - "path/filepath" -) +import "path/filepath" func LineSeparator() string { return "\r\n" @@ -14,7 +12,6 @@ func LineSeparator() string { // GetAssetLocation searches for `file` in the env dir and the executable dir func GetAssetLocation(file string) string { assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) - return filepath.Join(assetPath, file) } diff --git a/infra/conf/dns.go b/infra/conf/dns.go index f6d56913..a65f0ee8 100644 --- a/infra/conf/dns.go +++ b/infra/conf/dns.go @@ -80,6 +80,21 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error { return errors.New("failed to parse name server: ", string(data)) } +func toDomainMatchingType(t router.Domain_Type) dns.DomainMatchingType { + switch t { + case router.Domain_Domain: + return dns.DomainMatchingType_Subdomain + case router.Domain_Full: + return dns.DomainMatchingType_Full + case router.Domain_Plain: + return dns.DomainMatchingType_Keyword + case router.Domain_Regex: + return dns.DomainMatchingType_Regex + default: + panic("unknown domain type") + } +} + func (c *NameServerConfig) Build() (*dns.NameServer, error) { if c.Address == nil { return nil, errors.New("NameServer address is not specified.") @@ -89,14 +104,14 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) { var originalRules []*dns.NameServer_OriginalRule for _, rule := range c.Domains { - parsedDomain, err := ParseDomainRule(rule) + parsedDomain, err := parseDomainRule(rule) if err != nil { return nil, errors.New("invalid domain rule: ", rule).Base(err) } for _, pd := range parsedDomain { domains = append(domains, &dns.NameServer_PriorityDomain{ - Type: dns.ToDomainMatchingType(pd.Type), + Type: toDomainMatchingType(pd.Type), Domain: pd.Value, }) } diff --git a/infra/conf/router.go b/infra/conf/router.go index c9f5b43b..69217c7a 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -203,23 +203,17 @@ func loadFile(file string) ([]byte, error) { func loadIP(file, code string) ([]*router.CIDR, error) { index := file + ":" + code if IPCache[index] == nil { + bs, err := loadFile(file) + if err != nil { + return nil, errors.New("failed to load file: ", file).Base(err) + } + bs = find(bs, []byte(code)) + if bs == nil { + return nil, errors.New("code not found in ", file, ": ", code) + } var geoip router.GeoIP - - if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { - // dont pass code becuase we have country code in top level router.GeoIP - geoip = router.GeoIP{Cidr: []*router.CIDR{}} - } else { - bs, err := loadFile(file) - if err != nil { - return nil, errors.New("failed to load file: ", file).Base(err) - } - bs = filesystem.Find(bs, []byte(code)) - if bs == nil { - return nil, errors.New("code not found in ", file, ": ", code) - } - if err := proto.Unmarshal(bs, &geoip); err != nil { - return nil, errors.New("error unmarshal IP in ", file, ": ", code).Base(err) - } + if err := proto.Unmarshal(bs, &geoip); err != nil { + return nil, errors.New("error unmarshal IP in ", file, ": ", code).Base(err) } defer runtime.GC() // or debug.FreeOSMemory() return geoip.Cidr, nil // do not cache geoip @@ -231,28 +225,18 @@ func loadIP(file, code string) ([]*router.CIDR, error) { func loadSite(file, code string) ([]*router.Domain, error) { index := file + ":" + code if SiteCache[index] == nil { - var geosite router.GeoSite - - if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { - // pass file:code so can build optimized matcher later - domain := router.Domain{Value: file + "_" + code} - geosite = router.GeoSite{Domain: []*router.Domain{&domain}} - - } else { - - bs, err := loadFile(file) - if err != nil { - return nil, errors.New("failed to load file: ", file).Base(err) - } - bs = filesystem.Find(bs, []byte(code)) - if bs == nil { - return nil, errors.New("list not found in ", file, ": ", code) - } - if err := proto.Unmarshal(bs, &geosite); err != nil { - return nil, errors.New("error unmarshal Site in ", file, ": ", code).Base(err) - } + bs, err := loadFile(file) + if err != nil { + return nil, errors.New("failed to load file: ", file).Base(err) + } + bs = find(bs, []byte(code)) + if bs == nil { + return nil, errors.New("list not found in ", file, ": ", code) + } + var geosite router.GeoSite + if err := proto.Unmarshal(bs, &geosite); err != nil { + return nil, errors.New("error unmarshal Site in ", file, ": ", code).Base(err) } - defer runtime.GC() // or debug.FreeOSMemory() return geosite.Domain, nil // do not cache geosite SiteCache[index] = &geosite @@ -260,13 +244,105 @@ func loadSite(file, code string) ([]*router.Domain, error) { return SiteCache[index].Domain, nil } +func DecodeVarint(buf []byte) (x uint64, n int) { + for shift := uint(0); shift < 64; shift += 7 { + if n >= len(buf) { + return 0, 0 + } + b := uint64(buf[n]) + n++ + x |= (b & 0x7F) << shift + if (b & 0x80) == 0 { + return x, n + } + } + + // The number is too large to represent in a 64-bit value. + return 0, 0 +} + +func find(data, code []byte) []byte { + codeL := len(code) + if codeL == 0 { + return nil + } + for { + dataL := len(data) + if dataL < 2 { + return nil + } + x, y := DecodeVarint(data[1:]) + if x == 0 && y == 0 { + return nil + } + headL, bodyL := 1+y, int(x) + dataL -= headL + if dataL < bodyL { + return nil + } + data = data[headL:] + if int(data[1]) == codeL { + for i := 0; i < codeL && data[2+i] == code[i]; i++ { + if i+1 == codeL { + return data[:bodyL] + } + } + } + if dataL == bodyL { + return nil + } + data = data[bodyL:] + } +} + +type AttributeMatcher interface { + Match(*router.Domain) bool +} + +type BooleanMatcher string + +func (m BooleanMatcher) Match(domain *router.Domain) bool { + for _, attr := range domain.Attribute { + if attr.Key == string(m) { + return true + } + } + return false +} + +type AttributeList struct { + matcher []AttributeMatcher +} + +func (al *AttributeList) Match(domain *router.Domain) bool { + for _, matcher := range al.matcher { + if !matcher.Match(domain) { + return false + } + } + return true +} + +func (al *AttributeList) IsEmpty() bool { + return len(al.matcher) == 0 +} + +func parseAttrs(attrs []string) *AttributeList { + al := new(AttributeList) + for _, attr := range attrs { + lc := strings.ToLower(attr) + al.matcher = append(al.matcher, BooleanMatcher(lc)) + } + return al +} + func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) { parts := strings.Split(siteWithAttr, "@") if len(parts) == 0 { return nil, errors.New("empty site") } country := strings.ToUpper(parts[0]) - attrs := router.ParseAttrs(parts[1:]) + attrs := parseAttrs(parts[1:]) domains, err := loadSite(file, country) if err != nil { return nil, err @@ -276,11 +352,6 @@ func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, er return domains, nil } - if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { - domains[0].Value = domains[0].Value + "_" + strings.Join(parts[1:], ",") - return domains, nil - } - filteredDomains := make([]*router.Domain, 0, len(domains)) for _, domain := range domains { if attrs.Match(domain) { @@ -291,7 +362,7 @@ func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, er return filteredDomains, nil } -func ParseDomainRule(domain string) ([]*router.Domain, error) { +func parseDomainRule(domain string) ([]*router.Domain, error) { if strings.HasPrefix(domain, "geosite:") { country := strings.ToUpper(domain[8:]) domains, err := loadGeositeWithAttr("geosite.dat", country) @@ -489,7 +560,7 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) { if rawFieldRule.Domain != nil { for _, domain := range *rawFieldRule.Domain { - rules, err := ParseDomainRule(domain) + rules, err := parseDomainRule(domain) if err != nil { return nil, errors.New("failed to parse domain rule: ", domain).Base(err) } @@ -499,7 +570,7 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) { if rawFieldRule.Domains != nil { for _, domain := range *rawFieldRule.Domains { - rules, err := ParseDomainRule(domain) + rules, err := parseDomainRule(domain) if err != nil { return nil, errors.New("failed to parse domain rule: ", domain).Base(err) } From cfc78b3ac1eb998b935d2ac72166145423f87bfe Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Sat, 17 Jan 2026 21:29:50 +0800 Subject: [PATCH 106/136] Hysteria transport: Add `congestion` config (""/"reno"/"bbr"/"brutal"/"force-brutal") (#5549) Closes https://github.com/XTLS/Xray-core/issues/5546 --- infra/conf/transport_internet.go | 19 ++++++-- proxy/hysteria/client.go | 4 ++ proxy/hysteria/ctx/ctx.go | 20 ++++++++ transport/internet/hysteria/config.pb.go | 61 ++++++++++++++---------- transport/internet/hysteria/config.proto | 23 ++++----- transport/internet/hysteria/conn.go | 3 ++ transport/internet/hysteria/dialer.go | 33 +++++++++---- 7 files changed, 112 insertions(+), 51 deletions(-) create mode 100644 proxy/hysteria/ctx/ctx.go diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 3b21e383..ca7f866f 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -391,11 +391,12 @@ type UdpHop struct { } type HysteriaConfig struct { - Version int32 `json:"version"` - Auth string `json:"auth"` - Up Bandwidth `json:"up"` - Down Bandwidth `json:"down"` - UdpHop UdpHop `json:"udphop"` + Version int32 `json:"version"` + Auth string `json:"auth"` + Congestion string `json:"congestion"` + Up Bandwidth `json:"up"` + Down Bandwidth `json:"down"` + UdpHop UdpHop `json:"udphop"` InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"` MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"` @@ -410,6 +411,7 @@ func (c *HysteriaConfig) Build() (proto.Message, error) { if c.Version != 2 { return nil, errors.New("version != 2") } + up, err := c.Up.Bps() if err != nil { return nil, err @@ -418,6 +420,12 @@ func (c *HysteriaConfig) Build() (proto.Message, error) { if err != nil { return nil, err } + + c.Congestion = strings.ToLower(c.Congestion) + if c.Congestion == "force-brutal" && up == 0 { + return nil, errors.New("force-brutal require up") + } + var hop *PortList if err := json.Unmarshal(c.UdpHop.PortList, &hop); err != nil { hop = &PortList{} @@ -455,6 +463,7 @@ func (c *HysteriaConfig) Build() (proto.Message, error) { config := &hysteria.Config{} config.Version = c.Version config.Auth = c.Auth + config.Congestion = c.Congestion config.Up = up config.Down = down config.Ports = hop.Build().Ports() diff --git a/proxy/hysteria/client.go b/proxy/hysteria/client.go index 4e985443..6325261a 100644 --- a/proxy/hysteria/client.go +++ b/proxy/hysteria/client.go @@ -17,6 +17,7 @@ import ( "github.com/xtls/xray-core/common/task" "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/features/policy" + hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx" "github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/hysteria" @@ -55,6 +56,9 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter ob.CanSpliceCopy = 3 target := ob.Target + if target.Network == net.Network_UDP { + hyCtx.ContextWithRequireDatagram(ctx) + } conn, err := dialer.Dial(ctx, c.server.Destination) if err != nil { return errors.New("failed to find an available destination").AtWarning().Base(err) diff --git a/proxy/hysteria/ctx/ctx.go b/proxy/hysteria/ctx/ctx.go new file mode 100644 index 00000000..6c03a101 --- /dev/null +++ b/proxy/hysteria/ctx/ctx.go @@ -0,0 +1,20 @@ +package ctx + +import ( + "context" +) + +type key int + +const ( + requireDatagram key = iota +) + +func ContextWithRequireDatagram(ctx context.Context) context.Context { + return context.WithValue(ctx, requireDatagram, struct{}{}) +} + +func RequireDatagramFromContext(ctx context.Context) bool { + _, ok := ctx.Value(requireDatagram).(struct{}) + return ok +} diff --git a/transport/internet/hysteria/config.pb.go b/transport/internet/hysteria/config.pb.go index 5e453c30..8ec3a88e 100644 --- a/transport/internet/hysteria/config.pb.go +++ b/transport/internet/hysteria/config.pb.go @@ -25,17 +25,18 @@ type Config struct { state protoimpl.MessageState `protogen:"open.v1"` Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` Auth string `protobuf:"bytes,2,opt,name=auth,proto3" json:"auth,omitempty"` - Up uint64 `protobuf:"varint,3,opt,name=up,proto3" json:"up,omitempty"` - Down uint64 `protobuf:"varint,4,opt,name=down,proto3" json:"down,omitempty"` - Ports []uint32 `protobuf:"varint,5,rep,packed,name=ports,proto3" json:"ports,omitempty"` - Interval int64 `protobuf:"varint,6,opt,name=interval,proto3" json:"interval,omitempty"` - InitStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"` - MaxStreamReceiveWindow uint64 `protobuf:"varint,8,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"` - InitConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"` - MaxConnReceiveWindow uint64 `protobuf:"varint,10,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"` - MaxIdleTimeout int64 `protobuf:"varint,11,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"` - KeepAlivePeriod int64 `protobuf:"varint,12,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"` - DisablePathMtuDiscovery bool `protobuf:"varint,13,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"` + Congestion string `protobuf:"bytes,3,opt,name=congestion,proto3" json:"congestion,omitempty"` + Up uint64 `protobuf:"varint,4,opt,name=up,proto3" json:"up,omitempty"` + Down uint64 `protobuf:"varint,5,opt,name=down,proto3" json:"down,omitempty"` + Ports []uint32 `protobuf:"varint,6,rep,packed,name=ports,proto3" json:"ports,omitempty"` + Interval int64 `protobuf:"varint,7,opt,name=interval,proto3" json:"interval,omitempty"` + InitStreamReceiveWindow uint64 `protobuf:"varint,8,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"` + MaxStreamReceiveWindow uint64 `protobuf:"varint,9,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"` + InitConnReceiveWindow uint64 `protobuf:"varint,10,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"` + MaxConnReceiveWindow uint64 `protobuf:"varint,11,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"` + MaxIdleTimeout int64 `protobuf:"varint,12,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"` + KeepAlivePeriod int64 `protobuf:"varint,13,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"` + DisablePathMtuDiscovery bool `protobuf:"varint,14,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -84,6 +85,13 @@ func (x *Config) GetAuth() string { return "" } +func (x *Config) GetCongestion() string { + if x != nil { + return x.Congestion + } + return "" +} + func (x *Config) GetUp() uint64 { if x != nil { return x.Up @@ -165,22 +173,25 @@ var File_transport_internet_hysteria_config_proto protoreflect.FileDescriptor const file_transport_internet_hysteria_config_proto_rawDesc = "" + "\n" + - "(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\x87\x04\n" + + "(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\xa7\x04\n" + "\x06Config\x12\x18\n" + "\aversion\x18\x01 \x01(\x05R\aversion\x12\x12\n" + - "\x04auth\x18\x02 \x01(\tR\x04auth\x12\x0e\n" + - "\x02up\x18\x03 \x01(\x04R\x02up\x12\x12\n" + - "\x04down\x18\x04 \x01(\x04R\x04down\x12\x14\n" + - "\x05ports\x18\x05 \x03(\rR\x05ports\x12\x1a\n" + - "\binterval\x18\x06 \x01(\x03R\binterval\x12;\n" + - "\x1ainit_stream_receive_window\x18\a \x01(\x04R\x17initStreamReceiveWindow\x129\n" + - "\x19max_stream_receive_window\x18\b \x01(\x04R\x16maxStreamReceiveWindow\x127\n" + - "\x18init_conn_receive_window\x18\t \x01(\x04R\x15initConnReceiveWindow\x125\n" + - "\x17max_conn_receive_window\x18\n" + - " \x01(\x04R\x14maxConnReceiveWindow\x12(\n" + - "\x10max_idle_timeout\x18\v \x01(\x03R\x0emaxIdleTimeout\x12*\n" + - "\x11keep_alive_period\x18\f \x01(\x03R\x0fkeepAlivePeriod\x12;\n" + - "\x1adisable_path_mtu_discovery\x18\r \x01(\bR\x17disablePathMtuDiscoveryB\x82\x01\n" + + "\x04auth\x18\x02 \x01(\tR\x04auth\x12\x1e\n" + + "\n" + + "congestion\x18\x03 \x01(\tR\n" + + "congestion\x12\x0e\n" + + "\x02up\x18\x04 \x01(\x04R\x02up\x12\x12\n" + + "\x04down\x18\x05 \x01(\x04R\x04down\x12\x14\n" + + "\x05ports\x18\x06 \x03(\rR\x05ports\x12\x1a\n" + + "\binterval\x18\a \x01(\x03R\binterval\x12;\n" + + "\x1ainit_stream_receive_window\x18\b \x01(\x04R\x17initStreamReceiveWindow\x129\n" + + "\x19max_stream_receive_window\x18\t \x01(\x04R\x16maxStreamReceiveWindow\x127\n" + + "\x18init_conn_receive_window\x18\n" + + " \x01(\x04R\x15initConnReceiveWindow\x125\n" + + "\x17max_conn_receive_window\x18\v \x01(\x04R\x14maxConnReceiveWindow\x12(\n" + + "\x10max_idle_timeout\x18\f \x01(\x03R\x0emaxIdleTimeout\x12*\n" + + "\x11keep_alive_period\x18\r \x01(\x03R\x0fkeepAlivePeriod\x12;\n" + + "\x1adisable_path_mtu_discovery\x18\x0e \x01(\bR\x17disablePathMtuDiscoveryB\x82\x01\n" + "$com.xray.transport.internet.hysteriaP\x01Z5github.com/xtls/xray-core/transport/internet/hysteria\xaa\x02 Xray.Transport.Internet.Hysteriab\x06proto3" var ( diff --git a/transport/internet/hysteria/config.proto b/transport/internet/hysteria/config.proto index 22113338..787a2b40 100644 --- a/transport/internet/hysteria/config.proto +++ b/transport/internet/hysteria/config.proto @@ -9,17 +9,18 @@ option java_multiple_files = true; message Config { int32 version = 1; string auth = 2; - uint64 up = 3; - uint64 down = 4; - repeated uint32 ports = 5; - int64 interval = 6; + string congestion = 3; + uint64 up = 4; + uint64 down = 5; + repeated uint32 ports = 6; + int64 interval = 7; - uint64 init_stream_receive_window = 7; - uint64 max_stream_receive_window = 8; - uint64 init_conn_receive_window = 9; - uint64 max_conn_receive_window = 10; - int64 max_idle_timeout = 11; - int64 keep_alive_period = 12; - bool disable_path_mtu_discovery = 13; + uint64 init_stream_receive_window = 8; + uint64 max_stream_receive_window = 9; + uint64 init_conn_receive_window = 10; + uint64 max_conn_receive_window = 11; + int64 max_idle_timeout = 12; + int64 keep_alive_period = 13; + bool disable_path_mtu_discovery = 14; } diff --git a/transport/internet/hysteria/conn.go b/transport/internet/hysteria/conn.go index fe1259bb..28c20fec 100644 --- a/transport/internet/hysteria/conn.go +++ b/transport/internet/hysteria/conn.go @@ -64,6 +64,9 @@ func (i *InterUdpConn) Read(p []byte) (int, error) { return 0, io.EOF } n := copy(p, b) + if n != len(b) { + return 0, io.ErrShortBuffer + } return n, nil } diff --git a/transport/internet/hysteria/dialer.go b/transport/internet/hysteria/dialer.go index 5a694aa4..d4723a1b 100644 --- a/transport/internet/hysteria/dialer.go +++ b/transport/internet/hysteria/dialer.go @@ -16,8 +16,8 @@ import ( "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" - "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/task" + hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/finalmask" "github.com/xtls/xray-core/transport/internet/hysteria/congestion" @@ -115,8 +115,8 @@ type client struct { conn *quic.Conn config *Config tlsConfig *go_tls.Config - udpmaskManager *finalmask.UdpmaskManager socketConfig *internet.SocketConfig + udpmaskManager *finalmask.UdpmaskManager udpSM *udpSessionManager mutex sync.Mutex } @@ -243,10 +243,25 @@ func (c *client) dial() error { serverAuto := resp.Header.Get(CommonHeaderCCRX) serverDown, _ := strconv.ParseUint(serverAuto, 10, 64) - if serverAuto == "auto" || c.config.Up == 0 || serverDown == 0 { + switch c.config.Congestion { + case "reno": + errors.LogDebug(c.ctx, "congestion reno") + case "bbr": + errors.LogDebug(c.ctx, "congestion bbr") congestion.UseBBR(quicConn) - } else { - congestion.UseBrutal(quicConn, min(c.config.Up, serverDown)) + case "brutal", "": + if serverAuto == "auto" || c.config.Up == 0 || serverDown == 0 { + errors.LogDebug(c.ctx, "congestion bbr") + congestion.UseBBR(quicConn) + } else { + errors.LogDebug(c.ctx, "congestion brutal bytes per second ", min(c.config.Up, serverDown)) + congestion.UseBrutal(quicConn, min(c.config.Up, serverDown)) + } + case "force-brutal": + errors.LogDebug(c.ctx, "congestion brutal bytes per second ", c.config.Up) + congestion.UseBrutal(quicConn, c.config.Up) + default: + errors.LogDebug(c.ctx, "congestion reno") } c.pktConn = pktConn @@ -363,6 +378,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me return nil, errors.New("tls config is nil") } + requireDatagram := hyCtx.RequireDatagramFromContext(ctx) addr := dest.NetAddr() config := streamSettings.ProtocolSettings.(*Config) @@ -375,18 +391,15 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me dest: dest, config: config, tlsConfig: tlsConfig.GetTLSConfig(), - udpmaskManager: streamSettings.UdpmaskManager, socketConfig: streamSettings.SocketSettings, + udpmaskManager: streamSettings.UdpmaskManager, } manger.m[addr] = c } c.setCtx(ctx) manger.mutex.Unlock() - outbounds := session.OutboundsFromContext(ctx) - targetUdp := len(outbounds) > 0 && outbounds[len(outbounds)-1].Target.Network == net.Network_UDP - - if targetUdp { + if requireDatagram { return c.udp() } return c.tcp() From 6d6c045a5a161052a8d2838f75891505cda9799e Mon Sep 17 00:00:00 2001 From: osypai Date: Sun, 18 Jan 2026 11:39:39 +0800 Subject: [PATCH 107/136] TUN inbound: Add macOS support (#5559) --- proxy/tun/tun_darwin.go | 169 ++++++++++++++++++++++++++ proxy/tun/tun_darwin_endpoint.go | 201 +++++++++++++++++++++++++++++++ proxy/tun/tun_default.go | 2 +- 3 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 proxy/tun/tun_darwin.go create mode 100644 proxy/tun/tun_darwin_endpoint.go diff --git a/proxy/tun/tun_darwin.go b/proxy/tun/tun_darwin.go new file mode 100644 index 00000000..b2d6f89f --- /dev/null +++ b/proxy/tun/tun_darwin.go @@ -0,0 +1,169 @@ +//go:build darwin + +package tun + +import ( + "errors" + "fmt" + "strings" + "unsafe" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +const ( + utunControlName = "com.apple.net.utun_control" + utunOptIfName = 2 + sysprotoControl = 2 +) + +type DarwinTun struct { + tunFd int + name string + options TunOptions +} + +var _ Tun = (*DarwinTun)(nil) +var _ GVisorTun = (*DarwinTun)(nil) + +func NewTun(options TunOptions) (Tun, error) { + tunFd, name, err := openUTun(options.Name) + if err != nil { + return nil, err + } + + return &DarwinTun{ + tunFd: tunFd, + name: name, + options: options, + }, nil +} + +func (t *DarwinTun) Start() error { + if t.options.MTU > 0 { + if err := setMTU(t.name, int(t.options.MTU)); err != nil { + return err + } + } + return setState(t.name, true) +} + +func (t *DarwinTun) Close() error { + _ = setState(t.name, false) + return unix.Close(t.tunFd) +} + +func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) { + return newDarwinEndpoint(t.tunFd, t.options.MTU), nil +} + +func openUTun(name string) (int, string, error) { + fd, err := unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, sysprotoControl) + if err != nil { + return -1, "", err + } + + ctlInfo := &unix.CtlInfo{} + copy(ctlInfo.Name[:], utunControlName) + if err := unix.IoctlCtlInfo(fd, ctlInfo); err != nil { + _ = unix.Close(fd) + return -1, "", err + } + + sockaddr := &unix.SockaddrCtl{ + ID: ctlInfo.Id, + Unit: parseUTunUnit(name), + } + + if err := unix.Connect(fd, sockaddr); err != nil { + _ = unix.Close(fd) + return -1, "", err + } + + if err := unix.SetNonblock(fd, true); err != nil { + _ = unix.Close(fd) + return -1, "", err + } + + tunName, err := unix.GetsockoptString(fd, sysprotoControl, utunOptIfName) + if err != nil { + _ = unix.Close(fd) + return -1, "", err + } + + tunName = strings.TrimRight(tunName, "\x00") + if tunName == "" { + _ = unix.Close(fd) + return -1, "", errors.New("empty utun name") + } + + return fd, tunName, nil +} + +func parseUTunUnit(name string) uint32 { + var unit uint32 + if _, err := fmt.Sscanf(name, "utun%d", &unit); err != nil { + return 0 + } + return unit + 1 +} + +type ifreqMTU struct { + Name [unix.IFNAMSIZ]byte + MTU int32 + _ [12]byte +} + +type ifreqFlags struct { + Name [unix.IFNAMSIZ]byte + Flags int16 + _ [14]byte +} + +func setMTU(name string, mtu int) error { + if mtu <= 0 { + return nil + } + + fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) + if err != nil { + return err + } + defer func() { _ = unix.Close(fd) }() + + ifr := ifreqMTU{MTU: int32(mtu)} + copy(ifr.Name[:], name) + return ioctlPtr(fd, unix.SIOCSIFMTU, unsafe.Pointer(&ifr)) +} + +func setState(name string, up bool) error { + fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) + if err != nil { + return err + } + defer func() { _ = unix.Close(fd) }() + + ifr := ifreqFlags{} + copy(ifr.Name[:], name) + + if err := ioctlPtr(fd, unix.SIOCGIFFLAGS, unsafe.Pointer(&ifr)); err != nil { + return err + } + + if up { + ifr.Flags |= unix.IFF_UP + } else { + ifr.Flags &^= unix.IFF_UP + } + + return ioctlPtr(fd, unix.SIOCSIFFLAGS, unsafe.Pointer(&ifr)) +} + +func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error { + _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg)) + if errno != 0 { + return errno + } + return nil +} diff --git a/proxy/tun/tun_darwin_endpoint.go b/proxy/tun/tun_darwin_endpoint.go new file mode 100644 index 00000000..a0af543b --- /dev/null +++ b/proxy/tun/tun_darwin_endpoint.go @@ -0,0 +1,201 @@ +//go:build darwin + +package tun + +import ( + "context" + "encoding/binary" + "errors" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +const utunHeaderSize = 4 + +var ErrUnsupportedNetworkProtocol = errors.New("unsupported ip version") + +// DarwinEndpoint implements GVisor stack.LinkEndpoint +var _ stack.LinkEndpoint = (*DarwinEndpoint)(nil) + +type DarwinEndpoint struct { + tunFd int + mtu uint32 + dispatcherCancel context.CancelFunc +} + +func newDarwinEndpoint(tunFd int, mtu uint32) *DarwinEndpoint { + return &DarwinEndpoint{ + tunFd: tunFd, + mtu: mtu, + } +} + +func (e *DarwinEndpoint) MTU() uint32 { + return e.mtu +} + +func (e *DarwinEndpoint) SetMTU(_ uint32) { + // not Implemented, as it is not expected GVisor will be asking tun device to be modified +} + +func (e *DarwinEndpoint) MaxHeaderLength() uint16 { + return 0 +} + +func (e *DarwinEndpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +func (e *DarwinEndpoint) SetLinkAddress(_ tcpip.LinkAddress) { + // not Implemented, as it is not expected GVisor will be asking tun device to be modified +} + +func (e *DarwinEndpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityRXChecksumOffload +} + +func (e *DarwinEndpoint) Attach(dispatcher stack.NetworkDispatcher) { + if e.dispatcherCancel != nil { + e.dispatcherCancel() + e.dispatcherCancel = nil + } + + if dispatcher != nil { + ctx, cancel := context.WithCancel(context.Background()) + go e.dispatchLoop(ctx, dispatcher) + e.dispatcherCancel = cancel + } +} + +func (e *DarwinEndpoint) IsAttached() bool { + return e.dispatcherCancel != nil +} + +func (e *DarwinEndpoint) Wait() { +} + +func (e *DarwinEndpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +func (e *DarwinEndpoint) AddHeader(buffer *stack.PacketBuffer) { + // tun interface doesn't have link layer header, it will be added by the OS +} + +func (e *DarwinEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { + return true +} + +func (e *DarwinEndpoint) Close() { + if e.dispatcherCancel != nil { + e.dispatcherCancel() + e.dispatcherCancel = nil + } +} + +func (e *DarwinEndpoint) SetOnCloseAction(_ func()) { +} + +func (e *DarwinEndpoint) WritePackets(packetBufferList stack.PacketBufferList) (int, tcpip.Error) { + var n int + for _, packetBuffer := range packetBufferList.AsSlice() { + family, err := ipFamilyFromPacket(packetBuffer) + if err != nil { + return n, &tcpip.ErrAborted{} + } + + var headerBytes [utunHeaderSize]byte + binary.BigEndian.PutUint32(headerBytes[:], family) + + writeSlices := append([][]byte{headerBytes[:]}, packetBuffer.AsSlices()...) + if _, err := unix.Writev(e.tunFd, writeSlices); err != nil { + if errors.Is(err, unix.EAGAIN) { + return n, &tcpip.ErrWouldBlock{} + } + return n, &tcpip.ErrAborted{} + } + n++ + } + return n, nil +} + +func (e *DarwinEndpoint) dispatchLoop(ctx context.Context, dispatcher stack.NetworkDispatcher) { + readSize := int(e.mtu) + if readSize <= 0 { + readSize = 65535 + } + readSize += utunHeaderSize + + buf := make([]byte, readSize) + for ctx.Err() == nil { + + n, err := unix.Read(e.tunFd, buf) + if err != nil { + if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) { + continue + } + e.Attach(nil) + return + } + if n <= utunHeaderSize { + continue + } + + networkProtocol, packet, err := parseUTunPacket(buf[:n]) + if errors.Is(err, ErrUnsupportedNetworkProtocol) { + continue + } + if err != nil { + e.Attach(nil) + return + } + + dispatcher.DeliverNetworkPacket(networkProtocol, packet) + packet.DecRef() + } +} + +func parseUTunPacket(packet []byte) (tcpip.NetworkProtocolNumber, *stack.PacketBuffer, error) { + if len(packet) <= utunHeaderSize { + return 0, nil, errors.New("packet too short") + } + + family := binary.BigEndian.Uint32(packet[:utunHeaderSize]) + var networkProtocol tcpip.NetworkProtocolNumber + switch family { + case uint32(unix.AF_INET): + networkProtocol = header.IPv4ProtocolNumber + case uint32(unix.AF_INET6): + networkProtocol = header.IPv6ProtocolNumber + default: + return 0, nil, ErrUnsupportedNetworkProtocol + } + + payload := packet[utunHeaderSize:] + packetBuffer := buffer.MakeWithData(payload) + return networkProtocol, stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: packetBuffer, + IsForwardedPacket: true, + }), nil +} + +func ipFamilyFromPacket(packetBuffer *stack.PacketBuffer) (uint32, error) { + for _, slice := range packetBuffer.AsSlices() { + if len(slice) == 0 { + continue + } + switch header.IPVersion(slice) { + case header.IPv4Version: + return uint32(unix.AF_INET), nil + case header.IPv6Version: + return uint32(unix.AF_INET6), nil + default: + return 0, ErrUnsupportedNetworkProtocol + } + } + return 0, errors.New("empty packet") +} diff --git a/proxy/tun/tun_default.go b/proxy/tun/tun_default.go index 34075fa2..ee061934 100644 --- a/proxy/tun/tun_default.go +++ b/proxy/tun/tun_default.go @@ -1,4 +1,4 @@ -//go:build !linux && !windows && !android +//go:build !linux && !windows && !android && !darwin package tun From 5836f36f69bc4253ef96336d0c129b7f4b99ad2a Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Sun, 18 Jan 2026 04:17:25 +0000 Subject: [PATCH 108/136] Config: Add Warning for deprecated features (allowInsecure, Shadowsocks, VMess, Trojan, VLESS without flow) Accelerate! --- infra/conf/shadowsocks.go | 6 +++++- infra/conf/transport_internet.go | 3 +++ infra/conf/trojan.go | 6 +++++- infra/conf/vless.go | 11 ++++++----- infra/conf/vmess.go | 8 ++++++-- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/infra/conf/shadowsocks.go b/infra/conf/shadowsocks.go index 4a04d977..3163798d 100644 --- a/infra/conf/shadowsocks.go +++ b/infra/conf/shadowsocks.go @@ -50,6 +50,8 @@ type ShadowsocksServerConfig struct { } func (v *ShadowsocksServerConfig) Build() (proto.Message, error) { + errors.PrintDeprecatedFeatureWarning("Shadowsocks", "VLESS Encryption") + if C.Contains(shadowaead_2022.List, v.Cipher) { return buildShadowsocks2022(v) } @@ -185,6 +187,8 @@ type ShadowsocksClientConfig struct { } func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { + errors.PrintDeprecatedFeatureWarning("Shadowsocks", "VLESS Encryption") + if v.Address != nil { v.Servers = []*ShadowsocksServerTarget{ { @@ -255,7 +259,7 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { ss := &protocol.ServerEndpoint{ Address: server.Address.Build(), Port: uint32(server.Port), - User: &protocol.User{ + User: &protocol.User{ Level: uint32(server.Level), Email: server.Email, Account: serial.ToTypedMessage(account), diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index ca7f866f..e24f0688 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -596,6 +596,9 @@ func (c *TLSConfig) Build() (proto.Message, error) { } serverName := c.ServerName config.AllowInsecure = c.Insecure + if config.AllowInsecure { + errors.PrintDeprecatedFeatureWarning("allowInsecure", "pinnedPeerCertSha256") + } if len(c.ServerName) > 0 { config.ServerName = serverName } diff --git a/infra/conf/trojan.go b/infra/conf/trojan.go index d9907019..885504cc 100644 --- a/infra/conf/trojan.go +++ b/infra/conf/trojan.go @@ -39,6 +39,8 @@ type TrojanClientConfig struct { // Build implements Buildable func (c *TrojanClientConfig) Build() (proto.Message, error) { + errors.PrintDeprecatedFeatureWarning("Trojan", "VLESS with flow") + if c.Address != nil { c.Servers = []*TrojanServerTarget{ { @@ -74,7 +76,7 @@ func (c *TrojanClientConfig) Build() (proto.Message, error) { config.Server = &protocol.ServerEndpoint{ Address: rec.Address.Build(), Port: uint32(rec.Port), - User: &protocol.User{ + User: &protocol.User{ Level: uint32(rec.Level), Email: rec.Email, Account: serial.ToTypedMessage(&trojan.Account{ @@ -115,6 +117,8 @@ type TrojanServerConfig struct { // Build implements Buildable func (c *TrojanServerConfig) Build() (proto.Message, error) { + errors.PrintDeprecatedFeatureWarning("Trojan", "VLESS with flow") + config := &trojan.ServerConfig{ Users: make([]*protocol.User, len(c.Clients)), } diff --git a/infra/conf/vless.go b/infra/conf/vless.go index f482c4d8..8a03eaab 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -42,8 +42,6 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { config := new(inbound.Config) config.Clients = make([]*protocol.User, len(c.Clients)) switch c.Flow { - case vless.None: - c.Flow = "" case "", vless.XRV: default: return nil, errors.New(`VLESS "settings.flow" doesn't support "` + c.Flow + `" in this version`) @@ -67,12 +65,13 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { switch account.Flow { case "": account.Flow = c.Flow - case vless.None: - account.Flow = "" case vless.XRV: default: return nil, errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`) } + if account.Flow == "" { + errors.PrintDeprecatedFeatureWarning("VLESS without flow", "VLESS with flow") + } if len(account.Testseed) < 4 { account.Testseed = c.Testseed @@ -280,7 +279,9 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { account.Id = u.String() switch account.Flow { - case "", vless.XRV, vless.XRV + "-udp443": + case "": + errors.PrintDeprecatedFeatureWarning("VLESS without flow", "VLESS with flow") + case vless.XRV, vless.XRV + "-udp443": default: return nil, errors.New(`VLESS users: "flow" doesn't support "` + account.Flow + `" in this version`) } diff --git a/infra/conf/vmess.go b/infra/conf/vmess.go index 4db061de..98c85b43 100644 --- a/infra/conf/vmess.go +++ b/infra/conf/vmess.go @@ -58,12 +58,14 @@ func (c *VMessDefaultConfig) Build() *inbound.DefaultConfig { } type VMessInboundConfig struct { - Users []json.RawMessage `json:"clients"` - Defaults *VMessDefaultConfig `json:"default"` + Users []json.RawMessage `json:"clients"` + Defaults *VMessDefaultConfig `json:"default"` } // Build implements Buildable func (c *VMessInboundConfig) Build() (proto.Message, error) { + errors.PrintDeprecatedFeatureWarning("VMess", "VLESS Encryption") + config := &inbound.Config{} if c.Defaults != nil { @@ -113,6 +115,8 @@ type VMessOutboundConfig struct { // Build implements Buildable func (c *VMessOutboundConfig) Build() (proto.Message, error) { + errors.PrintDeprecatedFeatureWarning("VMess", "VLESS Encryption") + config := new(outbound.Config) if c.Address != nil { c.Receivers = []*VMessOutboundTarget{ From a6aca101d62084ff7f27ee695448610aebc500ba Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Sun, 18 Jan 2026 12:25:36 +0800 Subject: [PATCH 109/136] Hysteria outbound: Fix ContextWithRequireDatagram() (#5558) Fixes https://github.com/XTLS/Xray-core/pull/5549 --- proxy/hysteria/client.go | 5 +---- proxy/hysteria/ctx/ctx.go | 5 ++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/proxy/hysteria/client.go b/proxy/hysteria/client.go index 6325261a..2299fe34 100644 --- a/proxy/hysteria/client.go +++ b/proxy/hysteria/client.go @@ -56,10 +56,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter ob.CanSpliceCopy = 3 target := ob.Target - if target.Network == net.Network_UDP { - hyCtx.ContextWithRequireDatagram(ctx) - } - conn, err := dialer.Dial(ctx, c.server.Destination) + conn, err := dialer.Dial(hyCtx.ContextWithRequireDatagram(ctx, target.Network == net.Network_UDP), c.server.Destination) if err != nil { return errors.New("failed to find an available destination").AtWarning().Base(err) } diff --git a/proxy/hysteria/ctx/ctx.go b/proxy/hysteria/ctx/ctx.go index 6c03a101..610fa065 100644 --- a/proxy/hysteria/ctx/ctx.go +++ b/proxy/hysteria/ctx/ctx.go @@ -10,7 +10,10 @@ const ( requireDatagram key = iota ) -func ContextWithRequireDatagram(ctx context.Context) context.Context { +func ContextWithRequireDatagram(ctx context.Context, udp bool) context.Context { + if !udp { + return ctx + } return context.WithValue(ctx, requireDatagram, struct{}{}) } From 1cf5662949aa32cebeb0f3ae79564819374ced7a Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Sun, 18 Jan 2026 05:11:51 +0000 Subject: [PATCH 110/136] Create SECURITY.md All for the FREE Internet! --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..c83f60bf --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +If you found an issue related to security vulnerability or protocol-identification problem, please report it to us via "[Report a vulnerability](https://github.com/XTLS/Xray-core/security/advisories/new)" privately, instead of publish it publicly before we release the fixed version. + +Thanks for your contribution to the FREE Internet! From 7ff06f65eccdcd2f5722ee369d23b699aeb5cc0f Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Sun, 18 Jan 2026 05:43:07 +0000 Subject: [PATCH 111/136] v26.1.18 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 --- core/core.go | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/core.go b/core/core.go index c6f0edb8..2285176e 100644 --- a/core/core.go +++ b/core/core.go @@ -19,7 +19,7 @@ import ( var ( Version_x byte = 26 Version_y byte = 1 - Version_z byte = 13 + Version_z byte = 18 ) var ( diff --git a/go.mod b/go.mod index fef388ad..f3457b21 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/xtls/xray-core -go 1.25.5 +go 1.25.6 require ( github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 From 66628943d8a31d91378c8d00d58f5ef3e18c132f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:23:59 +0000 Subject: [PATCH 112/136] Bump github.com/pires/go-proxyproto from 0.8.1 to 0.9.0 (#5582) Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.8.1 to 0.9.0. - [Release notes](https://github.com/pires/go-proxyproto/releases) - [Commits](https://github.com/pires/go-proxyproto/compare/v0.8.1...v0.9.0) --- updated-dependencies: - dependency-name: github.com/pires/go-proxyproto dependency-version: 0.9.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f3457b21..7a89de30 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/miekg/dns v1.1.70 github.com/pelletier/go-toml v1.9.5 - github.com/pires/go-proxyproto v0.8.1 + github.com/pires/go-proxyproto v0.9.0 github.com/refraction-networking/utls v1.8.2 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 diff --git a/go.sum b/go.sum index 26bdf1a2..42e637a1 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= -github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pires/go-proxyproto v0.9.0 h1:3Qg3CLxWx4wJOw5uxhTvc0VrgsJeerDbGTvexu4UK1E= +github.com/pires/go-proxyproto v0.9.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= From e813a3744f68a7b7fa5e7ae13d50f634fe9f2127 Mon Sep 17 00:00:00 2001 From: patterniha <71074308+patterniha@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:28:30 +0330 Subject: [PATCH 113/136] TUN inbound: Cancel ctx when handling is done (#5565) https://github.com/XTLS/Xray-core/pull/5565#issuecomment-3777939907 --------- Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- proxy/tun/handler.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/proxy/tun/handler.go b/proxy/tun/handler.go index 5cf21a75..31fc43c6 100644 --- a/proxy/tun/handler.go +++ b/proxy/tun/handler.go @@ -106,8 +106,9 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) { // to close, send completion packets back to the network, and cleanup defer conn.Close() - sid := session.NewID() - ctx := c.ContextWithID(t.ctx, sid) + ctx, cancel := context.WithCancel(t.ctx) + defer cancel() + ctx = c.ContextWithID(ctx, session.NewID()) source := net.DestinationFromAddr(conn.RemoteAddr()) inbound := session.Inbound{ From a778d3d273b6f07f72aa87ff980d2ca50c52542d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Wed, 21 Jan 2026 21:02:04 +0800 Subject: [PATCH 114/136] Tests: Reduce RAM usage (#5577) https://github.com/XTLS/Xray-core/pull/5577#issuecomment-3768963110 --- testing/scenarios/common.go | 47 +++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/testing/scenarios/common.go b/testing/scenarios/common.go index dc54105a..a6eea215 100644 --- a/testing/scenarios/common.go +++ b/testing/scenarios/common.go @@ -213,27 +213,40 @@ func testTCPConn2(conn net.Conn, payloadSize int, timeout time.Duration) func() "\tSys =", units.ByteSize(m.Sys).String(), "\tNumGC =", m.NumGC) }() - payload := make([]byte, payloadSize) - common.Must2(rand.Read(payload)) + singleWrite := func(length int) error { + payload := make([]byte, length) + common.Must2(rand.Read(payload)) - nBytes, err := conn.Write(payload) - if err != nil { - return err - } - if nBytes != len(payload) { - return errors.New("expect ", len(payload), " written, but actually ", nBytes) - } + nBytes, err := conn.Write(payload) + if err != nil { + return err + } + if nBytes != len(payload) { + return errors.New("expect ", len(payload), " written, but actually ", nBytes) + } - response, err := readFrom2(conn, timeout, payloadSize) - if err != nil { - return err - } - _ = response + response, err := readFrom2(conn, timeout, length) + if err != nil { + return err + } + _ = response - if r := bytes.Compare(response, xor(payload)); r != 0 { - return errors.New(r) - } + if r := bytes.Compare(response, xor(payload)); r != 0 { + return errors.New(r) + } + return nil + } + for payloadSize > 0 { + sizeToWrite := 1024 + if payloadSize < 1024 { + sizeToWrite = payloadSize + } + if err := singleWrite(sizeToWrite); err != nil { + return err + } + payloadSize -= sizeToWrite + } return nil } } From 30d6a5221b70544f34d83cf87be21363e086e23a Mon Sep 17 00:00:00 2001 From: gamekiller0010 <91186599+gamekiller0010@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:16:27 +0800 Subject: [PATCH 115/136] README.md: Update links for PassWall & PassWall 2 (#5572) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7625a14f..c24103e4 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ ## GUI Clients - OpenWrt - - [PassWall](https://github.com/xiaorouji/openwrt-passwall), [PassWall 2](https://github.com/xiaorouji/openwrt-passwall2) + - [PassWall](https://github.com/Openwrt-Passwall/openwrt-passwall), [PassWall 2](https://github.com/Openwrt-Passwall/openwrt-passwall2) - [ShadowSocksR Plus+](https://github.com/fw876/helloworld) - [luci-app-xray](https://github.com/yichya/luci-app-xray) ([openwrt-xray](https://github.com/yichya/openwrt-xray)) - Asuswrt-Merlin From 777e31302cf04fedb9c233f2036bec0c90c8be8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Wed, 21 Jan 2026 21:24:51 +0800 Subject: [PATCH 116/136] Router: Fix panic in ProcessNameMatcher when source IPs are empty (#5574) Fixes https://github.com/XTLS/Xray-core/issues/5573 --- app/router/condition.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/router/condition.go b/app/router/condition.go index ca4f9605..c063bae3 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -358,6 +358,9 @@ func NewProcessNameMatcher(names []string) *ProcessNameMatcher { } func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool { + if len(ctx.GetSourceIPs()) == 0 { + return false + } srcPort := ctx.GetSourcePort().String() srcIP := ctx.GetSourceIPs()[0].String() var network string From 262770564f108855caa52ace60c59363e798cc75 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Thu, 22 Jan 2026 03:08:46 +0000 Subject: [PATCH 117/136] README.md: Add fancyss to Asuswrt-Merlin Clients --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c24103e4..b34104bb 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ - [luci-app-xray](https://github.com/yichya/luci-app-xray) ([openwrt-xray](https://github.com/yichya/openwrt-xray)) - Asuswrt-Merlin - [XRAYUI](https://github.com/DanielLavrushin/asuswrt-merlin-xrayui) + - [fancyss](https://github.com/hq450/fancyss) - Windows - [v2rayN](https://github.com/2dust/v2rayN) - [Furious](https://github.com/LorenEteval/Furious) From 2d2102f654155be48fb1f2d31bd0c2bae8c0adf7 Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Thu, 22 Jan 2026 21:37:27 +0800 Subject: [PATCH 118/136] Hysteria transport: Fix speedtest issue (#5587) Fixes https://github.com/XTLS/Xray-core/issues/5546 --- transport/internet/hysteria/conn.go | 1 + 1 file changed, 1 insertion(+) diff --git a/transport/internet/hysteria/conn.go b/transport/internet/hysteria/conn.go index 28c20fec..ffc41a54 100644 --- a/transport/internet/hysteria/conn.go +++ b/transport/internet/hysteria/conn.go @@ -24,6 +24,7 @@ func (i *interConn) Write(b []byte) (int, error) { } func (i *interConn) Close() error { + i.stream.CancelRead(0) return i.stream.Close() } From 48164c826726d28f960aa8fbd731cca198fc487d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:11:20 +0000 Subject: [PATCH 119/136] Bump github.com/cloudflare/circl from 1.6.2 to 1.6.3 (#5589) Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) from 1.6.2 to 1.6.3. - [Release notes](https://github.com/cloudflare/circl/releases) - [Commits](https://github.com/cloudflare/circl/compare/v1.6.2...v1.6.3) --- updated-dependencies: - dependency-name: github.com/cloudflare/circl dependency-version: 1.6.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7a89de30..4a380f6a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.6 require ( github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 - github.com/cloudflare/circl v1.6.2 + github.com/cloudflare/circl v1.6.3 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/golang/mock v1.7.0-rc.1 github.com/google/go-cmp v0.7.0 diff --git a/go.sum b/go.sum index 42e637a1..e7d79840 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sx github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 h1:bSq8n+gX4oO/qnM3MKf4kroW75n+phO9Qp6nigJKZ1E= github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178/go.mod h1:N1WIjPphkqs4efXWuyDNQ6OjjIK04vM3h+bEgwV+eVU= -github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= -github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= From e0ee2350fc9f94d57def83b759ca89610f21dafd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:11:41 +0000 Subject: [PATCH 120/136] Bump github.com/miekg/dns from 1.1.70 to 1.1.72 (#5590) Bumps [github.com/miekg/dns](https://github.com/miekg/dns) from 1.1.70 to 1.1.72. - [Commits](https://github.com/miekg/dns/compare/v1.1.70...v1.1.72) --- updated-dependencies: - dependency-name: github.com/miekg/dns dependency-version: 1.1.72 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4a380f6a..edea5e74 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/golang/mock v1.7.0-rc.1 github.com/google/go-cmp v0.7.0 github.com/gorilla/websocket v1.5.3 - github.com/miekg/dns v1.1.70 + github.com/miekg/dns v1.1.72 github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.9.0 github.com/refraction-networking/utls v1.8.2 diff --git a/go.sum b/go.sum index e7d79840..5ea97096 100644 --- a/go.sum +++ b/go.sum @@ -40,8 +40,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA= -github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= From 5846f947845487e602ac5fa8b17c22c24d7a67d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Fri, 23 Jan 2026 21:49:08 +0800 Subject: [PATCH 121/136] Log config: More flexible `maskAddress` (#5570) https://github.com/XTLS/Xray-core/pull/5566#issuecomment-3765429984 --- app/log/log.go | 116 +++++++++++++++++++++++++++++++------------- app/log/log_test.go | 37 ++++++++++++++ 2 files changed, 120 insertions(+), 33 deletions(-) diff --git a/app/log/log.go b/app/log/log.go index da69d3b6..553862b1 100644 --- a/app/log/log.go +++ b/app/log/log.go @@ -2,8 +2,9 @@ package log import ( "context" - "fmt" + "net" "regexp" + "strconv" "strings" "sync" @@ -20,14 +21,23 @@ type Instance struct { errorLogger log.Handler active bool dns bool + mask4 int + mask6 int } // New creates a new log.Instance based on the given config. func New(ctx context.Context, config *Config) (*Instance, error) { + m4, m6, err := ParseMaskAddress(config.MaskAddress) + if err != nil { + return nil, err + } + g := &Instance{ config: config, active: false, dns: config.EnableDnsLog, + mask4: m4, + mask6: m6, } log.RegisterHandler(g) @@ -104,7 +114,11 @@ func (g *Instance) Handle(msg log.Message) { var Msg log.Message if g.config.MaskAddress != "" { - Msg = &MaskedMsgWrapper{Message: msg, config: g.config} + Msg = &MaskedMsgWrapper{ + Message: msg, + Mask4: g.mask4, + Mask6: g.mask6, + } } else { Msg = msg } @@ -149,51 +163,87 @@ func (g *Instance) Close() error { return nil } +func ParseMaskAddress(c string) (int, int, error) { + var m4, m6 int + switch c { + case "half": + m4, m6 = 16, 32 + case "quarter": + m4, m6 = 8, 16 + case "full": + m4, m6 = 0, 0 + case "": + // do nothing + default: + if parts := strings.Split(c, "+"); len(parts) > 0 { + if len(parts) >= 1 && parts[0] != "" { + i, err := strconv.Atoi(strings.TrimPrefix(parts[0], "/")) + if err != nil { + return 32, 128, err + } + m4 = i + } + if len(parts) >= 2 && parts[1] != "" { + i, err := strconv.Atoi(strings.TrimPrefix(parts[1], "/")) + if err != nil { + return 32, 128, err + } + m6 = i + } + } + } + + if m4%8 != 0 || m4 > 32 || m4 < 0 { + return 32, 128, errors.New("Log Mask: ipv4 mask must be divisible by 8 and between 0-32") + } + + return m4, m6, nil +} + // MaskedMsgWrapper is to wrap the string() method to mask IP addresses in the log. type MaskedMsgWrapper struct { log.Message - config *Config + Mask4 int + Mask6 int } +var ( + ipv4Regex = regexp.MustCompile(`(\d{1,3}\.){3}\d{1,3}`) + ipv6Regex = regexp.MustCompile(`(?:[\da-fA-F]{0,4}:[\da-fA-F]{0,4}){2,7}`) +) + func (m *MaskedMsgWrapper) String() string { str := m.Message.String() - ipv4Regex := regexp.MustCompile(`(\d{1,3}\.){3}\d{1,3}`) - ipv6Regex := regexp.MustCompile(`((?:[\da-fA-F]{0,4}:[\da-fA-F]{0,4}){2,7})(?:[\/\\%](\d{1,3}))?`) - // Process ipv4 - maskedMsg := ipv4Regex.ReplaceAllStringFunc(str, func(ip string) string { - parts := strings.Split(ip, ".") - switch m.config.MaskAddress { - case "half": - return fmt.Sprintf("%s.%s.*.*", parts[0], parts[1]) - case "quarter": - return fmt.Sprintf("%s.*.*.*", parts[0]) - case "full": - return "[Masked IPv4]" - default: - return ip + maskedMsg := ipv4Regex.ReplaceAllStringFunc(str, func(s string) string { + if m.Mask4 == 32 { + return s } + if m.Mask4 == 0 { + return "[Masked IPv4]" + } + + parts := strings.Split(s, ".") + for i := m.Mask4 / 8; i < 4; i++ { + parts[i] = "*" + } + return strings.Join(parts, ".") }) // process ipv6 - maskedMsg = ipv6Regex.ReplaceAllStringFunc(maskedMsg, func(ip string) string { - parts := strings.Split(ip, ":") - switch m.config.MaskAddress { - case "half": - if len(parts) >= 2 { - return fmt.Sprintf("%s:%s::/32", parts[0], parts[1]) - } - case "quarter": - if len(parts) >= 1 { - return fmt.Sprintf("%s::/16", parts[0]) - } - case "full": - return "Masked IPv6" // Do not use [Masked IPv6] like ipv4, or you will get "[[Masked IPv6]]" (v6 address already has []) - default: - return ip + maskedMsg = ipv6Regex.ReplaceAllStringFunc(maskedMsg, func(s string) string { + if m.Mask6 == 128 { + return s } - return ip + if m.Mask6 == 0 { + return "Masked IPv6" + } + ip := net.ParseIP(s) + if ip == nil { + return s + } + return ip.Mask(net.CIDRMask(m.Mask6, 128)).String() + "/" + strconv.Itoa(m.Mask6) }) return maskedMsg diff --git a/app/log/log_test.go b/app/log/log_test.go index cda55f62..9a40e975 100644 --- a/app/log/log_test.go +++ b/app/log/log_test.go @@ -2,6 +2,7 @@ package log_test import ( "context" + "net" "testing" "github.com/golang/mock/gomock" @@ -50,3 +51,39 @@ func TestCustomLogHandler(t *testing.T) { common.Must(logger.Close()) } + +func TestMaskAddress(t *testing.T) { + m4, m6, err := log.ParseMaskAddress("half") + if err != nil { + t.Fatal(err) + } + maskedAddr := log.MaskedMsgWrapper{ + Mask4: m4, + Mask6: m6, + } + maskedAddr.Message = net.ParseIP("11.45.1.4") + if maskedAddr.String() != "11.45.*.*" { + t.Fatal("expected '11.45.*.*', but actually ", maskedAddr.String()) + } + maskedAddr.Message = net.ParseIP("11:45:14:19:19:81:0::") + if maskedAddr.String() != "11:45::/32" { + t.Fatal("expected '11:45::/32', but actually", maskedAddr.String()) + } + + m4, m6, err = log.ParseMaskAddress("/16+/64") + if err != nil { + t.Fatal(err) + } + maskedAddr = log.MaskedMsgWrapper{ + Mask4: m4, + Mask6: m6, + } + maskedAddr.Message = net.ParseIP("11.45.1.4") + if maskedAddr.String() != "11.45.*.*" { + t.Fatal("expected '11.45.*.*', but actually ", maskedAddr.String()) + } + maskedAddr.Message = net.ParseIP("11:45:14:19:19:81:0::") + if maskedAddr.String() != "11:45:14:19::/64" { + t.Fatal("expected '11:45:14:19::/64', but actually", maskedAddr.String()) + } +} From 59dc2cee2ed3ca5e7e8418f36db9477b20d7a991 Mon Sep 17 00:00:00 2001 From: MouMeng <2482924065@qq.com> Date: Fri, 23 Jan 2026 23:44:16 +0800 Subject: [PATCH 122/136] API: Add ListRule() for routing (#5569) https://github.com/XTLS/Xray-core/pull/5569#issuecomment-3766310407 --- app/router/command/command.go | 15 + app/router/command/command.pb.go | 589 ++++++++++++++------------ app/router/command/command.proto | 13 + app/router/command/command_grpc.pb.go | 56 ++- app/router/router.go | 17 +- features/routing/router.go | 6 + main/commands/all/api/api.go | 1 + main/commands/all/api/rules_list.go | 43 ++ 8 files changed, 457 insertions(+), 283 deletions(-) create mode 100644 main/commands/all/api/rules_list.go diff --git a/app/router/command/command.go b/app/router/command/command.go index fd9caa22..c0974553 100644 --- a/app/router/command/command.go +++ b/app/router/command/command.go @@ -60,6 +60,7 @@ func (s *routingServer) AddRule(ctx context.Context, request *AddRuleRequest) (* return nil, errors.New("unsupported router implementation") } + func (s *routingServer) RemoveRule(ctx context.Context, request *RemoveRuleRequest) (*RemoveRuleResponse, error) { if bo, ok := s.router.(routing.Router); ok { return &RemoveRuleResponse{}, bo.RemoveRule(request.RuleTag) @@ -67,6 +68,20 @@ func (s *routingServer) RemoveRule(ctx context.Context, request *RemoveRuleReque return nil, errors.New("unsupported router implementation") } +func (s *routingServer) ListRule(ctx context.Context, request *ListRuleRequest) (*ListRuleResponse, error) { + if bo, ok := s.router.(routing.Router); ok { + response := &ListRuleResponse{} + for _, v := range bo.ListRule() { + response.Rules = append(response.Rules, &ListRuleItem{ + Tag: v.GetOutboundTag(), + RuleTag: v.GetRuleTag(), + }) + } + return response, nil + } + return nil, errors.New("unsupported router implementation") +} + // NewRoutingServer creates a statistics service with statistics manager. func NewRoutingServer(router routing.Router, routingStats stats.Channel) RoutingServiceServer { return &routingServer{ diff --git a/app/router/command/command.pb.go b/app/router/command/command.pb.go index b8b1864d..30fe39cd 100644 --- a/app/router/command/command.pb.go +++ b/app/router/command/command.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.2 +// protoc-gen-go v1.36.11 +// protoc v6.33.2 // source: app/router/command/command.proto package command @@ -13,6 +13,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -26,25 +27,24 @@ const ( // It conforms to the structure of xray.features.routing.Context and // xray.features.routing.Route. type RoutingContext struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - InboundTag string `protobuf:"bytes,1,opt,name=InboundTag,proto3" json:"InboundTag,omitempty"` - Network net.Network `protobuf:"varint,2,opt,name=Network,proto3,enum=xray.common.net.Network" json:"Network,omitempty"` - SourceIPs [][]byte `protobuf:"bytes,3,rep,name=SourceIPs,proto3" json:"SourceIPs,omitempty"` - TargetIPs [][]byte `protobuf:"bytes,4,rep,name=TargetIPs,proto3" json:"TargetIPs,omitempty"` - SourcePort uint32 `protobuf:"varint,5,opt,name=SourcePort,proto3" json:"SourcePort,omitempty"` - TargetPort uint32 `protobuf:"varint,6,opt,name=TargetPort,proto3" json:"TargetPort,omitempty"` - TargetDomain string `protobuf:"bytes,7,opt,name=TargetDomain,proto3" json:"TargetDomain,omitempty"` - Protocol string `protobuf:"bytes,8,opt,name=Protocol,proto3" json:"Protocol,omitempty"` - User string `protobuf:"bytes,9,opt,name=User,proto3" json:"User,omitempty"` - Attributes map[string]string `protobuf:"bytes,10,rep,name=Attributes,proto3" json:"Attributes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - OutboundGroupTags []string `protobuf:"bytes,11,rep,name=OutboundGroupTags,proto3" json:"OutboundGroupTags,omitempty"` - OutboundTag string `protobuf:"bytes,12,opt,name=OutboundTag,proto3" json:"OutboundTag,omitempty"` - LocalIPs [][]byte `protobuf:"bytes,13,rep,name=LocalIPs,proto3" json:"LocalIPs,omitempty"` - LocalPort uint32 `protobuf:"varint,14,opt,name=LocalPort,proto3" json:"LocalPort,omitempty"` - VlessRoute uint32 `protobuf:"varint,15,opt,name=VlessRoute,proto3" json:"VlessRoute,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + InboundTag string `protobuf:"bytes,1,opt,name=InboundTag,proto3" json:"InboundTag,omitempty"` + Network net.Network `protobuf:"varint,2,opt,name=Network,proto3,enum=xray.common.net.Network" json:"Network,omitempty"` + SourceIPs [][]byte `protobuf:"bytes,3,rep,name=SourceIPs,proto3" json:"SourceIPs,omitempty"` + TargetIPs [][]byte `protobuf:"bytes,4,rep,name=TargetIPs,proto3" json:"TargetIPs,omitempty"` + SourcePort uint32 `protobuf:"varint,5,opt,name=SourcePort,proto3" json:"SourcePort,omitempty"` + TargetPort uint32 `protobuf:"varint,6,opt,name=TargetPort,proto3" json:"TargetPort,omitempty"` + TargetDomain string `protobuf:"bytes,7,opt,name=TargetDomain,proto3" json:"TargetDomain,omitempty"` + Protocol string `protobuf:"bytes,8,opt,name=Protocol,proto3" json:"Protocol,omitempty"` + User string `protobuf:"bytes,9,opt,name=User,proto3" json:"User,omitempty"` + Attributes map[string]string `protobuf:"bytes,10,rep,name=Attributes,proto3" json:"Attributes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + OutboundGroupTags []string `protobuf:"bytes,11,rep,name=OutboundGroupTags,proto3" json:"OutboundGroupTags,omitempty"` + OutboundTag string `protobuf:"bytes,12,opt,name=OutboundTag,proto3" json:"OutboundTag,omitempty"` + LocalIPs [][]byte `protobuf:"bytes,13,rep,name=LocalIPs,proto3" json:"LocalIPs,omitempty"` + LocalPort uint32 `protobuf:"varint,14,opt,name=LocalPort,proto3" json:"LocalPort,omitempty"` + VlessRoute uint32 `protobuf:"varint,15,opt,name=VlessRoute,proto3" json:"VlessRoute,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RoutingContext) Reset() { @@ -201,11 +201,10 @@ func (x *RoutingContext) GetVlessRoute() uint32 { // // * If FieldSelectors is left empty, all fields will be returned. type SubscribeRoutingStatsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - FieldSelectors []string `protobuf:"bytes,1,rep,name=FieldSelectors,proto3" json:"FieldSelectors,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + FieldSelectors []string `protobuf:"bytes,1,rep,name=FieldSelectors,proto3" json:"FieldSelectors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SubscribeRoutingStatsRequest) Reset() { @@ -253,13 +252,12 @@ func (x *SubscribeRoutingStatsRequest) GetFieldSelectors() []string { // * PublishResult broadcasts the routing result to routing statistics channel // if set true. type TestRouteRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - RoutingContext *RoutingContext `protobuf:"bytes,1,opt,name=RoutingContext,proto3" json:"RoutingContext,omitempty"` - FieldSelectors []string `protobuf:"bytes,2,rep,name=FieldSelectors,proto3" json:"FieldSelectors,omitempty"` - PublishResult bool `protobuf:"varint,3,opt,name=PublishResult,proto3" json:"PublishResult,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + RoutingContext *RoutingContext `protobuf:"bytes,1,opt,name=RoutingContext,proto3" json:"RoutingContext,omitempty"` + FieldSelectors []string `protobuf:"bytes,2,rep,name=FieldSelectors,proto3" json:"FieldSelectors,omitempty"` + PublishResult bool `protobuf:"varint,3,opt,name=PublishResult,proto3" json:"PublishResult,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *TestRouteRequest) Reset() { @@ -314,11 +312,10 @@ func (x *TestRouteRequest) GetPublishResult() bool { } type PrincipleTargetInfo struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Tag []string `protobuf:"bytes,1,rep,name=tag,proto3" json:"tag,omitempty"` unknownFields protoimpl.UnknownFields - - Tag []string `protobuf:"bytes,1,rep,name=tag,proto3" json:"tag,omitempty"` + sizeCache protoimpl.SizeCache } func (x *PrincipleTargetInfo) Reset() { @@ -359,11 +356,10 @@ func (x *PrincipleTargetInfo) GetTag() []string { } type OverrideInfo struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Target string `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` unknownFields protoimpl.UnknownFields - - Target string `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` + sizeCache protoimpl.SizeCache } func (x *OverrideInfo) Reset() { @@ -404,12 +400,11 @@ func (x *OverrideInfo) GetTarget() string { } type BalancerMsg struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Override *OverrideInfo `protobuf:"bytes,5,opt,name=override,proto3" json:"override,omitempty"` - PrincipleTarget *PrincipleTargetInfo `protobuf:"bytes,6,opt,name=principle_target,json=principleTarget,proto3" json:"principle_target,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Override *OverrideInfo `protobuf:"bytes,5,opt,name=override,proto3" json:"override,omitempty"` + PrincipleTarget *PrincipleTargetInfo `protobuf:"bytes,6,opt,name=principle_target,json=principleTarget,proto3" json:"principle_target,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *BalancerMsg) Reset() { @@ -457,11 +452,10 @@ func (x *BalancerMsg) GetPrincipleTarget() *PrincipleTargetInfo { } type GetBalancerInfoRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` unknownFields protoimpl.UnknownFields - - Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetBalancerInfoRequest) Reset() { @@ -502,11 +496,10 @@ func (x *GetBalancerInfoRequest) GetTag() string { } type GetBalancerInfoResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Balancer *BalancerMsg `protobuf:"bytes,1,opt,name=balancer,proto3" json:"balancer,omitempty"` unknownFields protoimpl.UnknownFields - - Balancer *BalancerMsg `protobuf:"bytes,1,opt,name=balancer,proto3" json:"balancer,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetBalancerInfoResponse) Reset() { @@ -547,12 +540,11 @@ func (x *GetBalancerInfoResponse) GetBalancer() *BalancerMsg { } type OverrideBalancerTargetRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + BalancerTag string `protobuf:"bytes,1,opt,name=balancerTag,proto3" json:"balancerTag,omitempty"` + Target string `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` unknownFields protoimpl.UnknownFields - - BalancerTag string `protobuf:"bytes,1,opt,name=balancerTag,proto3" json:"balancerTag,omitempty"` - Target string `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` + sizeCache protoimpl.SizeCache } func (x *OverrideBalancerTargetRequest) Reset() { @@ -600,9 +592,9 @@ func (x *OverrideBalancerTargetRequest) GetTarget() string { } type OverrideBalancerTargetResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *OverrideBalancerTargetResponse) Reset() { @@ -636,12 +628,11 @@ func (*OverrideBalancerTargetResponse) Descriptor() ([]byte, []int) { } type AddRuleRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Config *serial.TypedMessage `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + ShouldAppend bool `protobuf:"varint,2,opt,name=shouldAppend,proto3" json:"shouldAppend,omitempty"` unknownFields protoimpl.UnknownFields - - Config *serial.TypedMessage `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - ShouldAppend bool `protobuf:"varint,2,opt,name=shouldAppend,proto3" json:"shouldAppend,omitempty"` + sizeCache protoimpl.SizeCache } func (x *AddRuleRequest) Reset() { @@ -689,9 +680,9 @@ func (x *AddRuleRequest) GetShouldAppend() bool { } type AddRuleResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AddRuleResponse) Reset() { @@ -725,11 +716,10 @@ func (*AddRuleResponse) Descriptor() ([]byte, []int) { } type RemoveRuleRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + RuleTag string `protobuf:"bytes,1,opt,name=ruleTag,proto3" json:"ruleTag,omitempty"` unknownFields protoimpl.UnknownFields - - RuleTag string `protobuf:"bytes,1,opt,name=ruleTag,proto3" json:"ruleTag,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RemoveRuleRequest) Reset() { @@ -770,9 +760,9 @@ func (x *RemoveRuleRequest) GetRuleTag() string { } type RemoveRuleResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RemoveRuleResponse) Reset() { @@ -805,15 +795,147 @@ func (*RemoveRuleResponse) Descriptor() ([]byte, []int) { return file_app_router_command_command_proto_rawDescGZIP(), []int{13} } -type Config struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type ListRuleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRuleRequest) Reset() { + *x = ListRuleRequest{} + mi := &file_app_router_command_command_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRuleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRuleRequest) ProtoMessage() {} + +func (x *ListRuleRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[14] + 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 ListRuleRequest.ProtoReflect.Descriptor instead. +func (*ListRuleRequest) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{14} +} + +type ListRuleItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + RuleTag string `protobuf:"bytes,2,opt,name=ruleTag,proto3" json:"ruleTag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRuleItem) Reset() { + *x = ListRuleItem{} + mi := &file_app_router_command_command_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRuleItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRuleItem) ProtoMessage() {} + +func (x *ListRuleItem) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[15] + 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 ListRuleItem.ProtoReflect.Descriptor instead. +func (*ListRuleItem) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{15} +} + +func (x *ListRuleItem) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *ListRuleItem) GetRuleTag() string { + if x != nil { + return x.RuleTag + } + return "" +} + +type ListRuleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rules []*ListRuleItem `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRuleResponse) Reset() { + *x = ListRuleResponse{} + mi := &file_app_router_command_command_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRuleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRuleResponse) ProtoMessage() {} + +func (x *ListRuleResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[16] + 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 ListRuleResponse.ProtoReflect.Descriptor instead. +func (*ListRuleResponse) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{16} +} + +func (x *ListRuleResponse) GetRules() []*ListRuleItem { + if x != nil { + return x.Rules + } + return nil +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Config) Reset() { *x = Config{} - mi := &file_app_router_command_command_proto_msgTypes[14] + mi := &file_app_router_command_command_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -825,7 +947,7 @@ func (x *Config) String() string { func (*Config) ProtoMessage() {} func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_app_router_command_command_proto_msgTypes[14] + mi := &file_app_router_command_command_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -838,187 +960,103 @@ func (x *Config) ProtoReflect() protoreflect.Message { // Deprecated: Use Config.ProtoReflect.Descriptor instead. func (*Config) Descriptor() ([]byte, []int) { - return file_app_router_command_command_proto_rawDescGZIP(), []int{14} + return file_app_router_command_command_proto_rawDescGZIP(), []int{17} } var File_app_router_command_command_proto protoreflect.FileDescriptor -var file_app_router_command_command_proto_rawDesc = []byte{ - 0x0a, 0x20, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x12, 0x17, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, - 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x1a, 0x18, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x21, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x73, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf6, 0x04, 0x0a, 0x0e, 0x52, 0x6f, 0x75, - 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x49, - 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x61, 0x67, 0x12, 0x32, 0x0a, 0x07, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x78, - 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, - 0x1c, 0x0a, 0x09, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x50, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x0c, 0x52, 0x09, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x50, 0x73, 0x12, 0x1c, 0x0a, - 0x09, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x50, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0c, - 0x52, 0x09, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x50, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x53, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x0a, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x54, - 0x61, 0x72, 0x67, 0x65, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x0a, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x54, - 0x61, 0x72, 0x67, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, - 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x55, - 0x73, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, - 0x57, 0x0a, 0x0a, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x0a, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x6f, - 0x75, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x11, 0x4f, 0x75, 0x74, 0x62, - 0x6f, 0x75, 0x6e, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x54, 0x61, 0x67, 0x73, 0x18, 0x0b, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x11, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x54, 0x61, 0x67, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, - 0x6e, 0x64, 0x54, 0x61, 0x67, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x4f, 0x75, 0x74, - 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x61, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x4c, 0x6f, 0x63, 0x61, - 0x6c, 0x49, 0x50, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x08, 0x4c, 0x6f, 0x63, 0x61, - 0x6c, 0x49, 0x50, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x6f, 0x72, - 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x6f, - 0x72, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x1a, 0x3d, 0x0a, 0x0f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0x46, 0x0a, 0x1c, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x6f, - 0x75, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x46, 0x69, 0x65, 0x6c, 0x64, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x22, 0xb1, 0x01, 0x0a, 0x10, 0x54, 0x65, - 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4f, - 0x0a, 0x0e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, - 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, - 0x0e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, - 0x26, 0x0a, 0x0e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x50, 0x75, 0x62, 0x6c, 0x69, - 0x73, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, - 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x27, 0x0a, - 0x13, 0x50, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x6c, 0x65, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, - 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x22, 0x26, 0x0a, 0x0c, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, - 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, 0xa9, - 0x01, 0x0a, 0x0b, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x4d, 0x73, 0x67, 0x12, 0x41, - 0x0a, 0x08, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x4f, 0x76, 0x65, 0x72, 0x72, - 0x69, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, - 0x65, 0x12, 0x57, 0x0a, 0x10, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x6c, 0x65, 0x5f, 0x74, - 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x78, 0x72, - 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x50, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x6c, 0x65, 0x54, - 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x70, 0x72, 0x69, 0x6e, 0x63, - 0x69, 0x70, 0x6c, 0x65, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, 0x2a, 0x0a, 0x16, 0x47, 0x65, - 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x22, 0x5b, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, - 0x61, 0x6e, 0x63, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x40, 0x0a, 0x08, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x42, 0x61, - 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x4d, 0x73, 0x67, 0x52, 0x08, 0x62, 0x61, 0x6c, 0x61, 0x6e, - 0x63, 0x65, 0x72, 0x22, 0x59, 0x0a, 0x1d, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x42, - 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, - 0x54, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x62, 0x61, 0x6c, 0x61, 0x6e, - 0x63, 0x65, 0x72, 0x54, 0x61, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, 0x20, - 0x0a, 0x1e, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, - 0x65, 0x72, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x6e, 0x0a, 0x0e, 0x41, 0x64, 0x64, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x38, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x22, 0x0a, 0x0c, - 0x73, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x41, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0c, 0x73, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x41, 0x70, 0x70, 0x65, 0x6e, 0x64, - 0x22, 0x11, 0x0a, 0x0f, 0x41, 0x64, 0x64, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x2d, 0x0a, 0x11, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x52, 0x75, 0x6c, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x75, 0x6c, 0x65, - 0x54, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x75, 0x6c, 0x65, 0x54, - 0x61, 0x67, 0x22, 0x14, 0x0a, 0x12, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x52, 0x75, 0x6c, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x32, 0xbf, 0x05, 0x0a, 0x0e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7b, 0x0a, 0x15, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x62, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x35, - 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x62, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, - 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, - 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x00, - 0x30, 0x01, 0x12, 0x61, 0x0a, 0x09, 0x54, 0x65, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, - 0x29, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x78, 0x72, 0x61, - 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, - 0x65, 0x78, 0x74, 0x22, 0x00, 0x12, 0x76, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, - 0x6e, 0x63, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, - 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x49, 0x6e, - 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x78, 0x72, 0x61, 0x79, - 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x49, - 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x8b, 0x01, - 0x0a, 0x16, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, - 0x65, 0x72, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x36, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, - 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x2e, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x42, 0x61, 0x6c, 0x61, 0x6e, - 0x63, 0x65, 0x72, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x37, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x4f, 0x76, 0x65, 0x72, 0x72, - 0x69, 0x64, 0x65, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x54, 0x61, 0x72, 0x67, 0x65, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5e, 0x0a, 0x07, 0x41, - 0x64, 0x64, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, - 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x2e, 0x41, 0x64, 0x64, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x41, 0x64, 0x64, 0x52, 0x75, 0x6c, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x67, 0x0a, 0x0a, 0x52, - 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x2a, 0x2e, 0x78, 0x72, 0x61, 0x79, - 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, - 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, - 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x42, 0x67, 0x0a, 0x1b, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, - 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 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, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x17, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x52, - 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_app_router_command_command_proto_rawDesc = "" + + "\n" + + " app/router/command/command.proto\x12\x17xray.app.router.command\x1a\x18common/net/network.proto\x1a!common/serial/typed_message.proto\"\xf6\x04\n" + + "\x0eRoutingContext\x12\x1e\n" + + "\n" + + "InboundTag\x18\x01 \x01(\tR\n" + + "InboundTag\x122\n" + + "\aNetwork\x18\x02 \x01(\x0e2\x18.xray.common.net.NetworkR\aNetwork\x12\x1c\n" + + "\tSourceIPs\x18\x03 \x03(\fR\tSourceIPs\x12\x1c\n" + + "\tTargetIPs\x18\x04 \x03(\fR\tTargetIPs\x12\x1e\n" + + "\n" + + "SourcePort\x18\x05 \x01(\rR\n" + + "SourcePort\x12\x1e\n" + + "\n" + + "TargetPort\x18\x06 \x01(\rR\n" + + "TargetPort\x12\"\n" + + "\fTargetDomain\x18\a \x01(\tR\fTargetDomain\x12\x1a\n" + + "\bProtocol\x18\b \x01(\tR\bProtocol\x12\x12\n" + + "\x04User\x18\t \x01(\tR\x04User\x12W\n" + + "\n" + + "Attributes\x18\n" + + " \x03(\v27.xray.app.router.command.RoutingContext.AttributesEntryR\n" + + "Attributes\x12,\n" + + "\x11OutboundGroupTags\x18\v \x03(\tR\x11OutboundGroupTags\x12 \n" + + "\vOutboundTag\x18\f \x01(\tR\vOutboundTag\x12\x1a\n" + + "\bLocalIPs\x18\r \x03(\fR\bLocalIPs\x12\x1c\n" + + "\tLocalPort\x18\x0e \x01(\rR\tLocalPort\x12\x1e\n" + + "\n" + + "VlessRoute\x18\x0f \x01(\rR\n" + + "VlessRoute\x1a=\n" + + "\x0fAttributesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"F\n" + + "\x1cSubscribeRoutingStatsRequest\x12&\n" + + "\x0eFieldSelectors\x18\x01 \x03(\tR\x0eFieldSelectors\"\xb1\x01\n" + + "\x10TestRouteRequest\x12O\n" + + "\x0eRoutingContext\x18\x01 \x01(\v2'.xray.app.router.command.RoutingContextR\x0eRoutingContext\x12&\n" + + "\x0eFieldSelectors\x18\x02 \x03(\tR\x0eFieldSelectors\x12$\n" + + "\rPublishResult\x18\x03 \x01(\bR\rPublishResult\"'\n" + + "\x13PrincipleTargetInfo\x12\x10\n" + + "\x03tag\x18\x01 \x03(\tR\x03tag\"&\n" + + "\fOverrideInfo\x12\x16\n" + + "\x06target\x18\x02 \x01(\tR\x06target\"\xa9\x01\n" + + "\vBalancerMsg\x12A\n" + + "\boverride\x18\x05 \x01(\v2%.xray.app.router.command.OverrideInfoR\boverride\x12W\n" + + "\x10principle_target\x18\x06 \x01(\v2,.xray.app.router.command.PrincipleTargetInfoR\x0fprincipleTarget\"*\n" + + "\x16GetBalancerInfoRequest\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\"[\n" + + "\x17GetBalancerInfoResponse\x12@\n" + + "\bbalancer\x18\x01 \x01(\v2$.xray.app.router.command.BalancerMsgR\bbalancer\"Y\n" + + "\x1dOverrideBalancerTargetRequest\x12 \n" + + "\vbalancerTag\x18\x01 \x01(\tR\vbalancerTag\x12\x16\n" + + "\x06target\x18\x02 \x01(\tR\x06target\" \n" + + "\x1eOverrideBalancerTargetResponse\"n\n" + + "\x0eAddRuleRequest\x128\n" + + "\x06config\x18\x01 \x01(\v2 .xray.common.serial.TypedMessageR\x06config\x12\"\n" + + "\fshouldAppend\x18\x02 \x01(\bR\fshouldAppend\"\x11\n" + + "\x0fAddRuleResponse\"-\n" + + "\x11RemoveRuleRequest\x12\x18\n" + + "\aruleTag\x18\x01 \x01(\tR\aruleTag\"\x14\n" + + "\x12RemoveRuleResponse\"\x11\n" + + "\x0fListRuleRequest\":\n" + + "\fListRuleItem\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12\x18\n" + + "\aruleTag\x18\x02 \x01(\tR\aruleTag\"O\n" + + "\x10ListRuleResponse\x12;\n" + + "\x05rules\x18\x01 \x03(\v2%.xray.app.router.command.ListRuleItemR\x05rules\"\b\n" + + "\x06Config2\xa2\x06\n" + + "\x0eRoutingService\x12{\n" + + "\x15SubscribeRoutingStats\x125.xray.app.router.command.SubscribeRoutingStatsRequest\x1a'.xray.app.router.command.RoutingContext\"\x000\x01\x12a\n" + + "\tTestRoute\x12).xray.app.router.command.TestRouteRequest\x1a'.xray.app.router.command.RoutingContext\"\x00\x12v\n" + + "\x0fGetBalancerInfo\x12/.xray.app.router.command.GetBalancerInfoRequest\x1a0.xray.app.router.command.GetBalancerInfoResponse\"\x00\x12\x8b\x01\n" + + "\x16OverrideBalancerTarget\x126.xray.app.router.command.OverrideBalancerTargetRequest\x1a7.xray.app.router.command.OverrideBalancerTargetResponse\"\x00\x12^\n" + + "\aAddRule\x12'.xray.app.router.command.AddRuleRequest\x1a(.xray.app.router.command.AddRuleResponse\"\x00\x12g\n" + + "\n" + + "RemoveRule\x12*.xray.app.router.command.RemoveRuleRequest\x1a+.xray.app.router.command.RemoveRuleResponse\"\x00\x12a\n" + + "\bListRule\x12(.xray.app.router.command.ListRuleRequest\x1a).xray.app.router.command.ListRuleResponse\"\x00Bg\n" + + "\x1bcom.xray.app.router.commandP\x01Z,github.com/xtls/xray-core/app/router/command\xaa\x02\x17Xray.App.Router.Commandb\x06proto3" var ( file_app_router_command_command_proto_rawDescOnce sync.Once - file_app_router_command_command_proto_rawDescData = file_app_router_command_command_proto_rawDesc + file_app_router_command_command_proto_rawDescData []byte ) func file_app_router_command_command_proto_rawDescGZIP() []byte { file_app_router_command_command_proto_rawDescOnce.Do(func() { - file_app_router_command_command_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_router_command_command_proto_rawDescData) + file_app_router_command_command_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_router_command_command_proto_rawDesc), len(file_app_router_command_command_proto_rawDesc))) }) return file_app_router_command_command_proto_rawDescData } -var file_app_router_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_app_router_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 19) var file_app_router_command_command_proto_goTypes = []any{ (*RoutingContext)(nil), // 0: xray.app.router.command.RoutingContext (*SubscribeRoutingStatsRequest)(nil), // 1: xray.app.router.command.SubscribeRoutingStatsRequest @@ -1034,36 +1072,42 @@ var file_app_router_command_command_proto_goTypes = []any{ (*AddRuleResponse)(nil), // 11: xray.app.router.command.AddRuleResponse (*RemoveRuleRequest)(nil), // 12: xray.app.router.command.RemoveRuleRequest (*RemoveRuleResponse)(nil), // 13: xray.app.router.command.RemoveRuleResponse - (*Config)(nil), // 14: xray.app.router.command.Config - nil, // 15: xray.app.router.command.RoutingContext.AttributesEntry - (net.Network)(0), // 16: xray.common.net.Network - (*serial.TypedMessage)(nil), // 17: xray.common.serial.TypedMessage + (*ListRuleRequest)(nil), // 14: xray.app.router.command.ListRuleRequest + (*ListRuleItem)(nil), // 15: xray.app.router.command.ListRuleItem + (*ListRuleResponse)(nil), // 16: xray.app.router.command.ListRuleResponse + (*Config)(nil), // 17: xray.app.router.command.Config + nil, // 18: xray.app.router.command.RoutingContext.AttributesEntry + (net.Network)(0), // 19: xray.common.net.Network + (*serial.TypedMessage)(nil), // 20: xray.common.serial.TypedMessage } var file_app_router_command_command_proto_depIdxs = []int32{ - 16, // 0: xray.app.router.command.RoutingContext.Network:type_name -> xray.common.net.Network - 15, // 1: xray.app.router.command.RoutingContext.Attributes:type_name -> xray.app.router.command.RoutingContext.AttributesEntry + 19, // 0: xray.app.router.command.RoutingContext.Network:type_name -> xray.common.net.Network + 18, // 1: xray.app.router.command.RoutingContext.Attributes:type_name -> xray.app.router.command.RoutingContext.AttributesEntry 0, // 2: xray.app.router.command.TestRouteRequest.RoutingContext:type_name -> xray.app.router.command.RoutingContext 4, // 3: xray.app.router.command.BalancerMsg.override:type_name -> xray.app.router.command.OverrideInfo 3, // 4: xray.app.router.command.BalancerMsg.principle_target:type_name -> xray.app.router.command.PrincipleTargetInfo 5, // 5: xray.app.router.command.GetBalancerInfoResponse.balancer:type_name -> xray.app.router.command.BalancerMsg - 17, // 6: xray.app.router.command.AddRuleRequest.config:type_name -> xray.common.serial.TypedMessage - 1, // 7: xray.app.router.command.RoutingService.SubscribeRoutingStats:input_type -> xray.app.router.command.SubscribeRoutingStatsRequest - 2, // 8: xray.app.router.command.RoutingService.TestRoute:input_type -> xray.app.router.command.TestRouteRequest - 6, // 9: xray.app.router.command.RoutingService.GetBalancerInfo:input_type -> xray.app.router.command.GetBalancerInfoRequest - 8, // 10: xray.app.router.command.RoutingService.OverrideBalancerTarget:input_type -> xray.app.router.command.OverrideBalancerTargetRequest - 10, // 11: xray.app.router.command.RoutingService.AddRule:input_type -> xray.app.router.command.AddRuleRequest - 12, // 12: xray.app.router.command.RoutingService.RemoveRule:input_type -> xray.app.router.command.RemoveRuleRequest - 0, // 13: xray.app.router.command.RoutingService.SubscribeRoutingStats:output_type -> xray.app.router.command.RoutingContext - 0, // 14: xray.app.router.command.RoutingService.TestRoute:output_type -> xray.app.router.command.RoutingContext - 7, // 15: xray.app.router.command.RoutingService.GetBalancerInfo:output_type -> xray.app.router.command.GetBalancerInfoResponse - 9, // 16: xray.app.router.command.RoutingService.OverrideBalancerTarget:output_type -> xray.app.router.command.OverrideBalancerTargetResponse - 11, // 17: xray.app.router.command.RoutingService.AddRule:output_type -> xray.app.router.command.AddRuleResponse - 13, // 18: xray.app.router.command.RoutingService.RemoveRule:output_type -> xray.app.router.command.RemoveRuleResponse - 13, // [13:19] is the sub-list for method output_type - 7, // [7:13] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 20, // 6: xray.app.router.command.AddRuleRequest.config:type_name -> xray.common.serial.TypedMessage + 15, // 7: xray.app.router.command.ListRuleResponse.rules:type_name -> xray.app.router.command.ListRuleItem + 1, // 8: xray.app.router.command.RoutingService.SubscribeRoutingStats:input_type -> xray.app.router.command.SubscribeRoutingStatsRequest + 2, // 9: xray.app.router.command.RoutingService.TestRoute:input_type -> xray.app.router.command.TestRouteRequest + 6, // 10: xray.app.router.command.RoutingService.GetBalancerInfo:input_type -> xray.app.router.command.GetBalancerInfoRequest + 8, // 11: xray.app.router.command.RoutingService.OverrideBalancerTarget:input_type -> xray.app.router.command.OverrideBalancerTargetRequest + 10, // 12: xray.app.router.command.RoutingService.AddRule:input_type -> xray.app.router.command.AddRuleRequest + 12, // 13: xray.app.router.command.RoutingService.RemoveRule:input_type -> xray.app.router.command.RemoveRuleRequest + 14, // 14: xray.app.router.command.RoutingService.ListRule:input_type -> xray.app.router.command.ListRuleRequest + 0, // 15: xray.app.router.command.RoutingService.SubscribeRoutingStats:output_type -> xray.app.router.command.RoutingContext + 0, // 16: xray.app.router.command.RoutingService.TestRoute:output_type -> xray.app.router.command.RoutingContext + 7, // 17: xray.app.router.command.RoutingService.GetBalancerInfo:output_type -> xray.app.router.command.GetBalancerInfoResponse + 9, // 18: xray.app.router.command.RoutingService.OverrideBalancerTarget:output_type -> xray.app.router.command.OverrideBalancerTargetResponse + 11, // 19: xray.app.router.command.RoutingService.AddRule:output_type -> xray.app.router.command.AddRuleResponse + 13, // 20: xray.app.router.command.RoutingService.RemoveRule:output_type -> xray.app.router.command.RemoveRuleResponse + 16, // 21: xray.app.router.command.RoutingService.ListRule:output_type -> xray.app.router.command.ListRuleResponse + 15, // [15:22] is the sub-list for method output_type + 8, // [8:15] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_app_router_command_command_proto_init() } @@ -1075,9 +1119,9 @@ func file_app_router_command_command_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_app_router_command_command_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_router_command_command_proto_rawDesc), len(file_app_router_command_command_proto_rawDesc)), NumEnums: 0, - NumMessages: 16, + NumMessages: 19, NumExtensions: 0, NumServices: 1, }, @@ -1086,7 +1130,6 @@ func file_app_router_command_command_proto_init() { MessageInfos: file_app_router_command_command_proto_msgTypes, }.Build() File_app_router_command_command_proto = out.File - file_app_router_command_command_proto_rawDesc = nil file_app_router_command_command_proto_goTypes = nil file_app_router_command_command_proto_depIdxs = nil } diff --git a/app/router/command/command.proto b/app/router/command/command.proto index 17aea575..22ae95b9 100644 --- a/app/router/command/command.proto +++ b/app/router/command/command.proto @@ -104,6 +104,17 @@ message RemoveRuleRequest { message RemoveRuleResponse {} +message ListRuleRequest {} + +message ListRuleItem { + string tag = 1; + string ruleTag = 2; +} + +message ListRuleResponse{ + repeated ListRuleItem rules = 1; +} + service RoutingService { rpc SubscribeRoutingStats(SubscribeRoutingStatsRequest) returns (stream RoutingContext) {} @@ -114,6 +125,8 @@ service RoutingService { rpc AddRule(AddRuleRequest) returns (AddRuleResponse) {} rpc RemoveRule(RemoveRuleRequest) returns (RemoveRuleResponse) {} + + rpc ListRule(ListRuleRequest) returns (ListRuleResponse) {} } message Config {} diff --git a/app/router/command/command_grpc.pb.go b/app/router/command/command_grpc.pb.go index 23e03c8a..f134d621 100644 --- a/app/router/command/command_grpc.pb.go +++ b/app/router/command/command_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v5.28.2 +// - protoc-gen-go-grpc v1.6.0 +// - protoc v6.33.2 // source: app/router/command/command.proto package command @@ -25,6 +25,7 @@ const ( RoutingService_OverrideBalancerTarget_FullMethodName = "/xray.app.router.command.RoutingService/OverrideBalancerTarget" RoutingService_AddRule_FullMethodName = "/xray.app.router.command.RoutingService/AddRule" RoutingService_RemoveRule_FullMethodName = "/xray.app.router.command.RoutingService/RemoveRule" + RoutingService_ListRule_FullMethodName = "/xray.app.router.command.RoutingService/ListRule" ) // RoutingServiceClient is the client API for RoutingService service. @@ -37,6 +38,7 @@ type RoutingServiceClient interface { OverrideBalancerTarget(ctx context.Context, in *OverrideBalancerTargetRequest, opts ...grpc.CallOption) (*OverrideBalancerTargetResponse, error) AddRule(ctx context.Context, in *AddRuleRequest, opts ...grpc.CallOption) (*AddRuleResponse, error) RemoveRule(ctx context.Context, in *RemoveRuleRequest, opts ...grpc.CallOption) (*RemoveRuleResponse, error) + ListRule(ctx context.Context, in *ListRuleRequest, opts ...grpc.CallOption) (*ListRuleResponse, error) } type routingServiceClient struct { @@ -116,6 +118,16 @@ func (c *routingServiceClient) RemoveRule(ctx context.Context, in *RemoveRuleReq return out, nil } +func (c *routingServiceClient) ListRule(ctx context.Context, in *ListRuleRequest, opts ...grpc.CallOption) (*ListRuleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListRuleResponse) + err := c.cc.Invoke(ctx, RoutingService_ListRule_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // RoutingServiceServer is the server API for RoutingService service. // All implementations must embed UnimplementedRoutingServiceServer // for forward compatibility. @@ -126,6 +138,7 @@ type RoutingServiceServer interface { OverrideBalancerTarget(context.Context, *OverrideBalancerTargetRequest) (*OverrideBalancerTargetResponse, error) AddRule(context.Context, *AddRuleRequest) (*AddRuleResponse, error) RemoveRule(context.Context, *RemoveRuleRequest) (*RemoveRuleResponse, error) + ListRule(context.Context, *ListRuleRequest) (*ListRuleResponse, error) mustEmbedUnimplementedRoutingServiceServer() } @@ -137,22 +150,25 @@ type RoutingServiceServer interface { type UnimplementedRoutingServiceServer struct{} func (UnimplementedRoutingServiceServer) SubscribeRoutingStats(*SubscribeRoutingStatsRequest, grpc.ServerStreamingServer[RoutingContext]) error { - return status.Errorf(codes.Unimplemented, "method SubscribeRoutingStats not implemented") + return status.Error(codes.Unimplemented, "method SubscribeRoutingStats not implemented") } func (UnimplementedRoutingServiceServer) TestRoute(context.Context, *TestRouteRequest) (*RoutingContext, error) { - return nil, status.Errorf(codes.Unimplemented, "method TestRoute not implemented") + return nil, status.Error(codes.Unimplemented, "method TestRoute not implemented") } func (UnimplementedRoutingServiceServer) GetBalancerInfo(context.Context, *GetBalancerInfoRequest) (*GetBalancerInfoResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetBalancerInfo not implemented") + return nil, status.Error(codes.Unimplemented, "method GetBalancerInfo not implemented") } func (UnimplementedRoutingServiceServer) OverrideBalancerTarget(context.Context, *OverrideBalancerTargetRequest) (*OverrideBalancerTargetResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method OverrideBalancerTarget not implemented") + return nil, status.Error(codes.Unimplemented, "method OverrideBalancerTarget not implemented") } func (UnimplementedRoutingServiceServer) AddRule(context.Context, *AddRuleRequest) (*AddRuleResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method AddRule not implemented") + return nil, status.Error(codes.Unimplemented, "method AddRule not implemented") } func (UnimplementedRoutingServiceServer) RemoveRule(context.Context, *RemoveRuleRequest) (*RemoveRuleResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method RemoveRule not implemented") + return nil, status.Error(codes.Unimplemented, "method RemoveRule not implemented") +} +func (UnimplementedRoutingServiceServer) ListRule(context.Context, *ListRuleRequest) (*ListRuleResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRule not implemented") } func (UnimplementedRoutingServiceServer) mustEmbedUnimplementedRoutingServiceServer() {} func (UnimplementedRoutingServiceServer) testEmbeddedByValue() {} @@ -165,7 +181,7 @@ type UnsafeRoutingServiceServer interface { } func RegisterRoutingServiceServer(s grpc.ServiceRegistrar, srv RoutingServiceServer) { - // If the following call pancis, it indicates UnimplementedRoutingServiceServer was + // If the following call panics, it indicates UnimplementedRoutingServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. @@ -276,6 +292,24 @@ func _RoutingService_RemoveRule_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _RoutingService_ListRule_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRuleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoutingServiceServer).ListRule(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoutingService_ListRule_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoutingServiceServer).ListRule(ctx, req.(*ListRuleRequest)) + } + return interceptor(ctx, in, info, handler) +} + // RoutingService_ServiceDesc is the grpc.ServiceDesc for RoutingService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -303,6 +337,10 @@ var RoutingService_ServiceDesc = grpc.ServiceDesc{ MethodName: "RemoveRule", Handler: _RoutingService_RemoveRule_Handler, }, + { + MethodName: "ListRule", + Handler: _RoutingService_ListRule_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/app/router/router.go b/app/router/router.go index 2f35b3e7..790bf8e2 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -2,7 +2,7 @@ package router import ( "context" - sync "sync" + "sync" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" @@ -181,6 +181,21 @@ func (r *Router) RemoveRule(tag string) error { return errors.New("empty tag name!") } + +// ListRule implements routing.Router +func (r *Router) ListRule() []routing.Route { + r.mu.Lock() + defer r.mu.Unlock() + ruleList := make([]routing.Route, 0) + for _, rule := range r.rules { + ruleList = append(ruleList, &Route{ + outboundTag: rule.Tag, + ruleTag: rule.RuleTag, + }) + } + return ruleList +} + func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context, error) { // SkipDNSResolve is set from DNS module. // the DOH remote server maybe a domain name, diff --git a/features/routing/router.go b/features/routing/router.go index 174d59fd..46172de7 100644 --- a/features/routing/router.go +++ b/features/routing/router.go @@ -16,6 +16,7 @@ type Router interface { PickRoute(ctx Context) (Route, error) AddRule(config *serial.TypedMessage, shouldAppend bool) error RemoveRule(tag string) error + ListRule() []Route } // Route is the routing result of Router feature. @@ -65,6 +66,11 @@ func (DefaultRouter) RemoveRule(tag string) error { return common.ErrNoClue } +// ListRule implements Router. +func (DefaultRouter) ListRule() []Route { + return nil +} + // Start implements common.Runnable. func (DefaultRouter) Start() error { return nil diff --git a/main/commands/all/api/api.go b/main/commands/all/api/api.go index cd5e97f4..85d7aa9f 100644 --- a/main/commands/all/api/api.go +++ b/main/commands/all/api/api.go @@ -29,6 +29,7 @@ var CmdAPI = &base.Command{ cmdInboundUserCount, cmdAddRules, cmdRemoveRules, + cmdListRules, cmdSourceIpBlock, cmdOnlineStats, cmdOnlineStatsIpList, diff --git a/main/commands/all/api/rules_list.go b/main/commands/all/api/rules_list.go new file mode 100644 index 00000000..6bc1c4aa --- /dev/null +++ b/main/commands/all/api/rules_list.go @@ -0,0 +1,43 @@ +package api + +import ( + routerService "github.com/xtls/xray-core/app/router/command" + "github.com/xtls/xray-core/main/commands/base" +) + +var cmdListRules = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api lsrules [--server=127.0.0.1:8080]", + Short: "List routing rules", + Long: ` +List routing rules in Xray. + +Arguments: + + -s, -server + The API server address. Default 127.0.0.1:8080 + + -t, -timeout + Timeout in seconds for calling API. Default 3 + +Example: + + {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 +`, + Run: executeListRules, +} + +func executeListRules(cmd *base.Command, args []string) { + setSharedFlags(cmd) + cmd.Flag.Parse(args) + + conn, ctx, close := dialAPIServer() + defer close() + + client := routerService.NewRoutingServiceClient(conn) + resp, err := client.ListRule(ctx, &routerService.ListRuleRequest{}) + if err != nil { + base.Fatalf("failed to list rules: %s", err) + } + showJSONResponse(resp) +} From cd8aab9544bc35fbc9990f61cb3613d0abbbe775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=90=B2=93=F0=90=B3=9B=F0=90=B3=AA=F0=90=B3=82?= =?UTF-8?q?=F0=90=B3=90=20=F0=90=B2=80=F0=90=B3=A2=F0=90=B3=A6=F0=90=B3=AB?= =?UTF-8?q?=F0=90=B3=A2=20=F0=90=B2=A5=F0=90=B3=94=F0=90=B3=9B=F0=90=B3=AA?= =?UTF-8?q?=F0=90=B3=8C=F0=90=B3=91=F0=90=B3=96=F0=90=B3=87?= <26771058+KobeArthurScofield@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:45:20 +0800 Subject: [PATCH 123/136] common/errors/feature_errors.go: Add PrintNonRemovalDeprecatedFeatureWarning() (#5567) And https://github.com/XTLS/Xray-core/pull/5567#issuecomment-3765466219 --------- Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com> --- common/errors/feature_errors.go | 13 +++++-------- infra/conf/shadowsocks.go | 4 ++-- infra/conf/transport_internet.go | 6 +++--- infra/conf/trojan.go | 4 ++-- infra/conf/vless.go | 4 ++-- infra/conf/vmess.go | 4 ++-- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/common/errors/feature_errors.go b/common/errors/feature_errors.go index 40c0ee9b..8c443f27 100644 --- a/common/errors/feature_errors.go +++ b/common/errors/feature_errors.go @@ -4,22 +4,19 @@ import ( "context" ) -// PrintMigrateFeatureInfo prints a notice of the upcoming feature migration. -// Place it after the source feature related config file pharser code. -// Important note: Only use this when the target migrating feature is under construction. -// Important note: Even when the target migrating feature has finished its construction, this notice can still be used yet before announcing deprecation of the old feature. +// PrintNonRemovalDeprecatedFeatureWarning prints a warning of the deprecated feature that won't be removed in the near future. // Do not remove this function even there is no reference to it. -func PrintMigrateFeatureInfo(sourceFeature string, targetFeature string) { - LogInfo(context.Background(), "The feature "+sourceFeature+" will be migrated to "+targetFeature+" in the future.") +func PrintNonRemovalDeprecatedFeatureWarning(sourceFeature string, targetFeature string) { + LogWarning(context.Background(), "The feature "+sourceFeature+" is deprecated, not recommended for using and might be removed. Please migrate to "+targetFeature+" as soon as possible.") } // PrintDeprecatedFeatureWarning prints a warning for deprecated and going to be removed feature. // Do not remove this function even there is no reference to it. func PrintDeprecatedFeatureWarning(feature string, migrateFeature string) { if len(migrateFeature) > 0 { - LogWarning(context.Background(), "This feature "+feature+" is deprecated and being migrated to "+migrateFeature+". Please update your config(s) according to release note and documentation before removal.") + LogWarning(context.Background(), "This feature "+feature+" is deprecated, will be removed soon and being migrated to "+migrateFeature+". Please update your config(s) according to release note and documentation before removal.") } else { - LogWarning(context.Background(), "This feature "+feature+" is deprecated. Please update your config(s) according to release note and documentation before removal.") + LogWarning(context.Background(), "This feature "+feature+" is deprecated and will be removed soon. Please update your config(s) according to release note and documentation before removal.") } } diff --git a/infra/conf/shadowsocks.go b/infra/conf/shadowsocks.go index 3163798d..a1a0b89e 100644 --- a/infra/conf/shadowsocks.go +++ b/infra/conf/shadowsocks.go @@ -50,7 +50,7 @@ type ShadowsocksServerConfig struct { } func (v *ShadowsocksServerConfig) Build() (proto.Message, error) { - errors.PrintDeprecatedFeatureWarning("Shadowsocks", "VLESS Encryption") + errors.PrintNonRemovalDeprecatedFeatureWarning("Shadowsocks (with no Forward Secrecy, etc.)", "VLESS Encryption") if C.Contains(shadowaead_2022.List, v.Cipher) { return buildShadowsocks2022(v) @@ -187,7 +187,7 @@ type ShadowsocksClientConfig struct { } func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { - errors.PrintDeprecatedFeatureWarning("Shadowsocks", "VLESS Encryption") + errors.PrintNonRemovalDeprecatedFeatureWarning("Shadowsocks (with no Forward Secrecy, etc.)", "VLESS Encryption") if v.Address != nil { v.Servers = []*ShadowsocksServerTarget{ diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index e24f0688..b7070be7 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -903,13 +903,13 @@ func (p TransportProtocol) Build() (string, error) { case "kcp", "mkcp": return "mkcp", nil case "grpc": - errors.PrintDeprecatedFeatureWarning("gRPC transport (with unnecessary costs, etc.)", "XHTTP stream-up H2") + errors.PrintNonRemovalDeprecatedFeatureWarning("gRPC transport (with unnecessary costs, etc.)", "XHTTP stream-up H2") return "grpc", nil case "ws", "websocket": - errors.PrintDeprecatedFeatureWarning("WebSocket transport (with ALPN http/1.1, etc.)", "XHTTP H2 & H3") + errors.PrintNonRemovalDeprecatedFeatureWarning("WebSocket transport (with ALPN http/1.1, etc.)", "XHTTP H2 & H3") return "websocket", nil case "httpupgrade": - errors.PrintDeprecatedFeatureWarning("HTTPUpgrade transport (with ALPN http/1.1, etc.)", "XHTTP H2 & H3") + errors.PrintNonRemovalDeprecatedFeatureWarning("HTTPUpgrade transport (with ALPN http/1.1, etc.)", "XHTTP H2 & H3") return "httpupgrade", nil case "h2", "h3", "http": return "", errors.PrintRemovedFeatureError("HTTP transport (without header padding, etc.)", "XHTTP stream-one H2 & H3") diff --git a/infra/conf/trojan.go b/infra/conf/trojan.go index 885504cc..b78b6ffc 100644 --- a/infra/conf/trojan.go +++ b/infra/conf/trojan.go @@ -39,7 +39,7 @@ type TrojanClientConfig struct { // Build implements Buildable func (c *TrojanClientConfig) Build() (proto.Message, error) { - errors.PrintDeprecatedFeatureWarning("Trojan", "VLESS with flow") + errors.PrintNonRemovalDeprecatedFeatureWarning("Trojan (with no Flow, etc.)", "VLESS with Flow & Seed") if c.Address != nil { c.Servers = []*TrojanServerTarget{ @@ -117,7 +117,7 @@ type TrojanServerConfig struct { // Build implements Buildable func (c *TrojanServerConfig) Build() (proto.Message, error) { - errors.PrintDeprecatedFeatureWarning("Trojan", "VLESS with flow") + errors.PrintNonRemovalDeprecatedFeatureWarning("Trojan (with no Flow, etc.)", "VLESS with Flow & Seed") config := &trojan.ServerConfig{ Users: make([]*protocol.User, len(c.Clients)), diff --git a/infra/conf/vless.go b/infra/conf/vless.go index 8a03eaab..d34906b0 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -70,7 +70,7 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { return nil, errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`) } if account.Flow == "" { - errors.PrintDeprecatedFeatureWarning("VLESS without flow", "VLESS with flow") + errors.PrintNonRemovalDeprecatedFeatureWarning("VLESS (with no Flow, etc.)", "VLESS with Flow & Seed") } if len(account.Testseed) < 4 { @@ -280,7 +280,7 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) { switch account.Flow { case "": - errors.PrintDeprecatedFeatureWarning("VLESS without flow", "VLESS with flow") + errors.PrintNonRemovalDeprecatedFeatureWarning("VLESS (with no Flow, etc.)", "VLESS with Flow & Seed") case vless.XRV, vless.XRV + "-udp443": default: return nil, errors.New(`VLESS users: "flow" doesn't support "` + account.Flow + `" in this version`) diff --git a/infra/conf/vmess.go b/infra/conf/vmess.go index 98c85b43..cb8ff9c9 100644 --- a/infra/conf/vmess.go +++ b/infra/conf/vmess.go @@ -64,7 +64,7 @@ type VMessInboundConfig struct { // Build implements Buildable func (c *VMessInboundConfig) Build() (proto.Message, error) { - errors.PrintDeprecatedFeatureWarning("VMess", "VLESS Encryption") + errors.PrintNonRemovalDeprecatedFeatureWarning("VMess (with no Forward Secrecy, etc.)", "VLESS Encryption") config := &inbound.Config{} @@ -115,7 +115,7 @@ type VMessOutboundConfig struct { // Build implements Buildable func (c *VMessOutboundConfig) Build() (proto.Message, error) { - errors.PrintDeprecatedFeatureWarning("VMess", "VLESS Encryption") + errors.PrintNonRemovalDeprecatedFeatureWarning("VMess (with no Forward Secrecy, etc.)", "VLESS Encryption") config := new(outbound.Config) if c.Address != nil { From 0a42dba13e33153ac7714771454c8b201661645d Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:48:16 +0000 Subject: [PATCH 124/136] v26.1.23 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 --- core/core.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/core.go b/core/core.go index 2285176e..05bfa38a 100644 --- a/core/core.go +++ b/core/core.go @@ -19,7 +19,7 @@ import ( var ( Version_x byte = 26 Version_y byte = 1 - Version_z byte = 18 + Version_z byte = 23 ) var ( From 445c0d456c39377f0dee08cd296d6f70c81d3fbd Mon Sep 17 00:00:00 2001 From: ki <59279014+KiGamji@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:39:07 +0500 Subject: [PATCH 125/136] TUN inbound: Disable RACK/TLP recovery to fix connection stalls (#5600) https://github.com/XTLS/Xray-core/issues/5599#issuecomment-3794495254 Fixes https://github.com/XTLS/Xray-core/issues/5599 --- proxy/tun/stack_gvisor.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/proxy/tun/stack_gvisor.go b/proxy/tun/stack_gvisor.go index 952150a8..ab767c61 100644 --- a/proxy/tun/stack_gvisor.go +++ b/proxy/tun/stack_gvisor.go @@ -236,6 +236,10 @@ func createStack(ep stack.LinkEndpoint) (*stack.Stack, error) { mOpt := tcpip.TCPModerateReceiveBufferOption(true) gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &mOpt) + // Disable RACK/TLP loss recovery to fix connection stalls under high load + rOpt := tcpip.TCPRecovery(0) + gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &rOpt) + tcpRXBufOpt := tcpip.TCPReceiveBufferSizeRangeOption{ Min: tcpRXBufMinSize, Default: tcpRXBufDefSize, From daf9cba29f014903fc55b9151cc29d4618b83c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sat, 24 Jan 2026 22:19:07 +0800 Subject: [PATCH 126/136] XUDP client: Initialize Global ID's BaseKey correctly (#5602) https://t.me/projectXray/4624679 --- common/xudp/xudp.go | 1 + 1 file changed, 1 insertion(+) diff --git a/common/xudp/xudp.go b/common/xudp/xudp.go index e5da49f4..afaa602d 100644 --- a/common/xudp/xudp.go +++ b/common/xudp/xudp.go @@ -35,6 +35,7 @@ func init() { if strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true" { Show = true } + BaseKey = make([]byte, 32) rand.Read(BaseKey) go func() { time.Sleep(100 * time.Millisecond) // this is not nice, but need to give some time for Android to setup ENV From 5173e5c15d43302ce936f8129f0c9a2bf0f38e95 Mon Sep 17 00:00:00 2001 From: Owersun <4807375+Owersun@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:19:05 +0100 Subject: [PATCH 127/136] TUN inbound: Enhance Darwin interface support (#5598) * Proxy: TUN: Enhance Darwin interface support. - reduce number of actions done to create/configure the interface in the system - assign synthetic static link-local ipv4/ipv6 addresses to the interface, that are required by the OS for the routing to work - make tun_darwin_endpoint be implemented significantly more similar to tun_windows_enpoint, preparing them for potential unification * Proxy: TUN: Unify Darwin/Windows endpoint, which are now extremely similar, into one GVisorEndpoint. Making darwin/windows tun implement GVisorDevice with simple readpacket/writepacket methods that GVisorEndpoint untilise --- proxy/tun/README.md | 22 +++ proxy/tun/stack_gvisor_endpoint.go | 155 +++++++++++++++ proxy/tun/tun_darwin.go | 306 ++++++++++++++++++++++------- proxy/tun/tun_darwin_endpoint.go | 201 ------------------- proxy/tun/tun_windows.go | 80 +++++++- proxy/tun/tun_windows_endpoint.go | 180 ----------------- 6 files changed, 478 insertions(+), 466 deletions(-) create mode 100644 proxy/tun/stack_gvisor_endpoint.go delete mode 100644 proxy/tun/tun_darwin_endpoint.go delete mode 100644 proxy/tun/tun_windows_endpoint.go diff --git a/proxy/tun/README.md b/proxy/tun/README.md index 684747b8..ea5d799d 100644 --- a/proxy/tun/README.md +++ b/proxy/tun/README.md @@ -172,3 +172,25 @@ route add 1.1.1.1 mask 255.0.0.0 0.0.0.0 if 47 Note on ipv6 support. \ Despite Windows also giving the adapter autoconfigured ipv6 address, the ipv6 is not possible until the interface has any _routable_ ipv6 address (given link-local address will not accept traffic from external addresses). \ So everything applicable for ipv4 above also works for ipv6, you only need to give the interface some address manually, e.g. anything private like fc00::a:b:c:d/64 will do just fine + +## MAC OS X SUPPORT + +Darwin (Mac OS X) support of the same functionality is implemented through utun (userspace tunnel). + +Interface name in the configuration must comply to the scheme "utunN", where N is some number. \ +Most running OS'es create some amount of utun interfaces in advance for own needs. Please either check the interfaces you already have occupied by issuing following command: +``` +ifconfig +``` +Produced list will have all system interfaces listed, from which you will see how many "utun" ones already exists. +It's not required to select next available number, e.g. if you have utun1-utun7 interfaces, it's not required to have "utun8" in the config. You can choose any available name, even utun20, to get surely available interface number. + +To attach routing to the interface, route command like following can be executed: +``` +sudo route add -net 1.1.1.0/24 -iface utun10 +``` +``` +sudo route add -inet6 -host 2606:4700:4700::1111 -iface utun10 +sudo route add -inet6 -host 2606:4700:4700::1001 -iface utun10 +``` +Important to remember that everything written above about Linux routing concept, also apply to Mac OS X. If you simply route default route through utun interface, that will result network loop and immediate network failure. diff --git a/proxy/tun/stack_gvisor_endpoint.go b/proxy/tun/stack_gvisor_endpoint.go new file mode 100644 index 00000000..31def35e --- /dev/null +++ b/proxy/tun/stack_gvisor_endpoint.go @@ -0,0 +1,155 @@ +package tun + +import ( + "context" + "errors" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +var ErrQueueEmpty = errors.New("queue is empty") + +type GVisorDevice interface { + WritePacket(packet *stack.PacketBuffer) tcpip.Error + ReadPacket() (byte, *stack.PacketBuffer, error) + Wait() +} + +// LinkEndpoint implements GVisor stack.LinkEndpoint +var _ stack.LinkEndpoint = (*LinkEndpoint)(nil) + +type LinkEndpoint struct { + deviceMTU uint32 + device GVisorDevice + dispatcherCancel context.CancelFunc +} + +func (e *LinkEndpoint) MTU() uint32 { + return e.deviceMTU +} + +func (e *LinkEndpoint) SetMTU(_ uint32) { + // not Implemented, as it is not expected GVisor will be asking tun device to be modified +} + +func (e *LinkEndpoint) MaxHeaderLength() uint16 { + return 0 +} + +func (e *LinkEndpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +func (e *LinkEndpoint) SetLinkAddress(_ tcpip.LinkAddress) { + // not Implemented, as it is not expected GVisor will be asking tun device to be modified +} + +func (e *LinkEndpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityRXChecksumOffload +} + +func (e *LinkEndpoint) Attach(dispatcher stack.NetworkDispatcher) { + if e.dispatcherCancel != nil { + e.dispatcherCancel() + e.dispatcherCancel = nil + } + + if dispatcher != nil { + ctx, cancel := context.WithCancel(context.Background()) + go e.dispatchLoop(ctx, dispatcher) + e.dispatcherCancel = cancel + } +} + +func (e *LinkEndpoint) IsAttached() bool { + return e.dispatcherCancel != nil +} + +func (e *LinkEndpoint) Wait() { + +} + +func (e *LinkEndpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +func (e *LinkEndpoint) AddHeader(buffer *stack.PacketBuffer) { + // tun interface doesn't have link layer header, it will be added by the OS +} + +func (e *LinkEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { + return true +} + +func (e *LinkEndpoint) Close() { + if e.dispatcherCancel != nil { + e.dispatcherCancel() + e.dispatcherCancel = nil + } +} + +func (e *LinkEndpoint) SetOnCloseAction(_ func()) { + +} + +func (e *LinkEndpoint) WritePackets(packetBufferList stack.PacketBufferList) (int, tcpip.Error) { + var n int + var err tcpip.Error + + for _, packetBuffer := range packetBufferList.AsSlice() { + err = e.device.WritePacket(packetBuffer) + if err != nil { + return n, &tcpip.ErrAborted{} + } + n++ + } + + return n, nil +} + +func (e *LinkEndpoint) dispatchLoop(ctx context.Context, dispatcher stack.NetworkDispatcher) { + var networkProtocolNumber tcpip.NetworkProtocolNumber + var version byte + var packet *stack.PacketBuffer + var err error + + for { + select { + case <-ctx.Done(): + return + default: + version, packet, err = e.device.ReadPacket() + // on "queue empty", ask device to yield slightly and continue + if errors.Is(err, ErrQueueEmpty) { + e.device.Wait() + continue + } + // stop dispatcher loop on any other interface failure + if err != nil { + e.Attach(nil) + return + } + + // extract network protocol number from the packet first byte + // (which is returned separately, since it is so incredibly hard to extract one byte from + // stack.PacketBuffer without additional memory allocation and full copying it back and forth) + switch version { + case 4: + networkProtocolNumber = header.IPv4ProtocolNumber + case 6: + networkProtocolNumber = header.IPv6ProtocolNumber + default: + // discard unknown network protocol packet + packet.DecRef() + continue + } + + // dispatch the buffer to the stack + dispatcher.DeliverNetworkPacket(networkProtocolNumber, packet) + // signal the buffer that it can be released + packet.DecRef() + } + } +} diff --git a/proxy/tun/tun_darwin.go b/proxy/tun/tun_darwin.go index b2d6f89f..a1b24dee 100644 --- a/proxy/tun/tun_darwin.go +++ b/proxy/tun/tun_darwin.go @@ -5,163 +5,319 @@ package tun import ( "errors" "fmt" - "strings" + "net" + "net/netip" + "os" + "syscall" "unsafe" + "github.com/xtls/xray-core/common/buf" "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/stack" ) const ( utunControlName = "com.apple.net.utun_control" - utunOptIfName = 2 sysprotoControl = 2 + gateway = "169.254.10.1/30" + utunHeaderSize = 4 ) +const ( + SIOCAIFADDR6 = 2155899162 // netinet6/in6_var.h + IN6_IFF_NODAD = 0x0020 // netinet6/in6_var.h + IN6_IFF_SECURED = 0x0400 // netinet6/in6_var.h + ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h +) + +//go:linkname procyield runtime.procyield +func procyield(cycles uint32) + type DarwinTun struct { - tunFd int - name string + tunFile *os.File options TunOptions } var _ Tun = (*DarwinTun)(nil) var _ GVisorTun = (*DarwinTun)(nil) +var _ GVisorDevice = (*DarwinTun)(nil) func NewTun(options TunOptions) (Tun, error) { - tunFd, name, err := openUTun(options.Name) + tunFile, err := open(options.Name) if err != nil { return nil, err } + err = setup(options.Name, options.MTU) + if err != nil { + _ = tunFile.Close() + return nil, err + } + return &DarwinTun{ - tunFd: tunFd, - name: name, + tunFile: tunFile, options: options, }, nil } func (t *DarwinTun) Start() error { - if t.options.MTU > 0 { - if err := setMTU(t.name, int(t.options.MTU)); err != nil { - return err - } - } - return setState(t.name, true) + return nil } func (t *DarwinTun) Close() error { - _ = setState(t.name, false) - return unix.Close(t.tunFd) + return t.tunFile.Close() +} + +// WritePacket implements GVisorDevice method to write one packet to the tun device +func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error { + // request memory to write from reusable buffer pool + b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize) + defer b.Release() + + // prepare Darwin specific packet header + _, _ = b.Write([]byte{0x0, 0x0, 0x0, 0x0}) + // copy the bytes of slices that compose the packet into the allocated buffer + for _, packetElement := range packet.AsSlices() { + _, _ = b.Write(packetElement) + } + // fill Darwin specific header from the first raw packet byte, that we can access now + var family byte + switch b.Byte(4) >> 4 { + case 4: + family = unix.AF_INET + case 6: + family = unix.AF_INET6 + default: + return &tcpip.ErrAborted{} + } + b.SetByte(3, family) + + if _, err := t.tunFile.Write(b.Bytes()); err != nil { + if errors.Is(err, unix.EAGAIN) { + return &tcpip.ErrWouldBlock{} + } + return &tcpip.ErrAborted{} + } + return nil +} + +// ReadPacket implements GVisorDevice method to read one packet from the tun device +// It is expected that the method will not block, rather return ErrQueueEmpty when there is nothing on the line, +// which will make the stack call Wait which should implement desired push-back +func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) { + // request memory to write from reusable buffer pool + b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize) + + // read the bytes to the interface file + n, err := b.ReadFrom(t.tunFile) + if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) { + b.Release() + return 0, nil, ErrQueueEmpty + } + if err != nil { + b.Release() + return 0, nil, err + } + + // discard empty or sub-empty packets + if n <= utunHeaderSize { + b.Release() + return 0, nil, ErrQueueEmpty + } + + // network protocol version from first byte of the raw packet, the one that follows Darwin specific header + version := b.Byte(utunHeaderSize) >> 4 + packetBuffer := buffer.MakeWithData(b.BytesFrom(utunHeaderSize)) + return version, stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: packetBuffer, + IsForwardedPacket: true, + OnRelease: func() { + b.Release() + }, + }), nil +} + +// Wait some cpu cycles +func (t *DarwinTun) Wait() { + procyield(1) } func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) { - return newDarwinEndpoint(t.tunFd, t.options.MTU), nil + return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil } -func openUTun(name string) (int, string, error) { +// open the interface, by creating new utunN if in the system and returning its file descriptor +func open(name string) (*os.File, error) { + ifIndex := -1 + _, err := fmt.Sscanf(name, "utun%d", &ifIndex) + if err != nil || ifIndex < 0 { + return nil, errors.New("interface name must be utunN, where N is a number, e.g. utun9, utun11 and so on") + } + fd, err := unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, sysprotoControl) if err != nil { - return -1, "", err + return nil, err } ctlInfo := &unix.CtlInfo{} copy(ctlInfo.Name[:], utunControlName) if err := unix.IoctlCtlInfo(fd, ctlInfo); err != nil { _ = unix.Close(fd) - return -1, "", err + return nil, err } sockaddr := &unix.SockaddrCtl{ ID: ctlInfo.Id, - Unit: parseUTunUnit(name), + Unit: uint32(ifIndex) + 1, } - if err := unix.Connect(fd, sockaddr); err != nil { _ = unix.Close(fd) - return -1, "", err + return nil, err } if err := unix.SetNonblock(fd, true); err != nil { _ = unix.Close(fd) - return -1, "", err + return nil, err } - tunName, err := unix.GetsockoptString(fd, sysprotoControl, utunOptIfName) - if err != nil { - _ = unix.Close(fd) - return -1, "", err - } - - tunName = strings.TrimRight(tunName, "\x00") - if tunName == "" { - _ = unix.Close(fd) - return -1, "", errors.New("empty utun name") - } - - return fd, tunName, nil + return os.NewFile(uintptr(fd), name), nil } -func parseUTunUnit(name string) uint32 { - var unit uint32 - if _, err := fmt.Sscanf(name, "utun%d", &unit); err != nil { - return 0 - } - return unit + 1 -} - -type ifreqMTU struct { - Name [unix.IFNAMSIZ]byte - MTU int32 - _ [12]byte -} - -type ifreqFlags struct { - Name [unix.IFNAMSIZ]byte - Flags int16 - _ [14]byte -} - -func setMTU(name string, mtu int) error { - if mtu <= 0 { - return nil +// setup the interface by name +func setup(name string, MTU uint32) error { + if err := setMTU(name, MTU); err != nil { + return err } - fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) + /* + * Darwin routing require tunnel type interface to have local and remote address, to be routable. + * To simplify inevitable task, assign the interface static ip address, which in current implementation + * is just some random ip from link-local pool, allowing to not bother about existing routing intersection. + */ + syntheticIP, _ := netip.ParsePrefix(gateway) + if err := setIPAddress(name, syntheticIP); err != nil { + return err + } + + return nil +} + +// setMTU sets MTU on the interface by given name +func setMTU(name string, mtu uint32) error { + socket, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) if err != nil { return err } - defer func() { _ = unix.Close(fd) }() + defer unix.Close(socket) - ifr := ifreqMTU{MTU: int32(mtu)} + ifr := unix.IfreqMTU{MTU: int32(mtu)} copy(ifr.Name[:], name) - return ioctlPtr(fd, unix.SIOCSIFMTU, unsafe.Pointer(&ifr)) + return unix.IoctlSetIfreqMTU(socket, &ifr) } -func setState(name string, up bool) error { - fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) +type ifAliasReq4 struct { + Name [unix.IFNAMSIZ]byte + Addr unix.RawSockaddrInet4 + Dstaddr unix.RawSockaddrInet4 + Mask unix.RawSockaddrInet4 +} + +type ifAliasReq6 struct { + Name [unix.IFNAMSIZ]byte + Addr unix.RawSockaddrInet6 + Dstaddr unix.RawSockaddrInet6 + Mask unix.RawSockaddrInet6 + Flags uint32 + Lifetime addrLifetime6 +} + +type addrLifetime6 struct { + Expire float64 + Preferred float64 + Vltime uint32 + Pltime uint32 +} + +// setIPAddress sets ipv4 and ipv6 addresses to the interface, required for the routing to work +func setIPAddress(name string, gateway netip.Prefix) error { + socket4, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) if err != nil { return err } - defer func() { _ = unix.Close(fd) }() + defer unix.Close(socket4) - ifr := ifreqFlags{} - copy(ifr.Name[:], name) + // assume local ip address is next one from the remote address + local4 := gateway.Addr().As4() + local4[3]++ - if err := ioctlPtr(fd, unix.SIOCGIFFLAGS, unsafe.Pointer(&ifr)); err != nil { + // fill the configuration for ipv4 + 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: netip.MustParseAddr(net.IP(net.CIDRMask(gateway.Bits(), 32)).String()).As4(), + }, + } + 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) - if up { - ifr.Flags |= unix.IFF_UP - } else { - ifr.Flags &^= unix.IFF_UP + // link-local ipv6 address with suffix from ipv6 + local6 := netip.AddrFrom16([16]byte{0: 0xfe, 1: 0x80, 12: local4[0], 13: local4[1], 14: local4[2], 15: local4[3]}) + + // fill the configuration for ipv6 + // only link-local address without the destination is enough for it + ifReq6 := ifAliasReq6{ + Addr: unix.RawSockaddrInet6{ + Len: unix.SizeofSockaddrInet6, + Family: unix.AF_INET6, + Addr: local6.As16(), + }, + Mask: unix.RawSockaddrInet6{ + Len: unix.SizeofSockaddrInet6, + Family: unix.AF_INET6, + Addr: netip.MustParseAddr(net.IP(net.CIDRMask(64, 128)).String()).As16(), + }, + Flags: IN6_IFF_NODAD, + Lifetime: addrLifetime6{ + Vltime: ND6_INFINITE_LIFETIME, + Pltime: ND6_INFINITE_LIFETIME, + }, + } + // assign link-local ipv6 address to the interface. + // this will additionally trigger OS level autoconfiguration, which will result two different link-local + // addresses - the requested one, and autoconfigured one. + // this really has no known side effects, just look excessive. and actually considered pretty normal way to + // enable the ipv6 on the interface by macOS concepts. + copy(ifReq6.Name[:], name) + if err = ioctlPtr(socket6, SIOCAIFADDR6, unsafe.Pointer(&ifReq6)); err != nil { + return os.NewSyscallError("SIOCAIFADDR6", err) } - return ioctlPtr(fd, unix.SIOCSIFFLAGS, unsafe.Pointer(&ifr)) + return nil } func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error { - _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg)) + _, _, errno := unix.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg)) if errno != 0 { return errno } diff --git a/proxy/tun/tun_darwin_endpoint.go b/proxy/tun/tun_darwin_endpoint.go deleted file mode 100644 index a0af543b..00000000 --- a/proxy/tun/tun_darwin_endpoint.go +++ /dev/null @@ -1,201 +0,0 @@ -//go:build darwin - -package tun - -import ( - "context" - "encoding/binary" - "errors" - - "golang.org/x/sys/unix" - "gvisor.dev/gvisor/pkg/buffer" - "gvisor.dev/gvisor/pkg/tcpip" - "gvisor.dev/gvisor/pkg/tcpip/header" - "gvisor.dev/gvisor/pkg/tcpip/stack" -) - -const utunHeaderSize = 4 - -var ErrUnsupportedNetworkProtocol = errors.New("unsupported ip version") - -// DarwinEndpoint implements GVisor stack.LinkEndpoint -var _ stack.LinkEndpoint = (*DarwinEndpoint)(nil) - -type DarwinEndpoint struct { - tunFd int - mtu uint32 - dispatcherCancel context.CancelFunc -} - -func newDarwinEndpoint(tunFd int, mtu uint32) *DarwinEndpoint { - return &DarwinEndpoint{ - tunFd: tunFd, - mtu: mtu, - } -} - -func (e *DarwinEndpoint) MTU() uint32 { - return e.mtu -} - -func (e *DarwinEndpoint) SetMTU(_ uint32) { - // not Implemented, as it is not expected GVisor will be asking tun device to be modified -} - -func (e *DarwinEndpoint) MaxHeaderLength() uint16 { - return 0 -} - -func (e *DarwinEndpoint) LinkAddress() tcpip.LinkAddress { - return "" -} - -func (e *DarwinEndpoint) SetLinkAddress(_ tcpip.LinkAddress) { - // not Implemented, as it is not expected GVisor will be asking tun device to be modified -} - -func (e *DarwinEndpoint) Capabilities() stack.LinkEndpointCapabilities { - return stack.CapabilityRXChecksumOffload -} - -func (e *DarwinEndpoint) Attach(dispatcher stack.NetworkDispatcher) { - if e.dispatcherCancel != nil { - e.dispatcherCancel() - e.dispatcherCancel = nil - } - - if dispatcher != nil { - ctx, cancel := context.WithCancel(context.Background()) - go e.dispatchLoop(ctx, dispatcher) - e.dispatcherCancel = cancel - } -} - -func (e *DarwinEndpoint) IsAttached() bool { - return e.dispatcherCancel != nil -} - -func (e *DarwinEndpoint) Wait() { -} - -func (e *DarwinEndpoint) ARPHardwareType() header.ARPHardwareType { - return header.ARPHardwareNone -} - -func (e *DarwinEndpoint) AddHeader(buffer *stack.PacketBuffer) { - // tun interface doesn't have link layer header, it will be added by the OS -} - -func (e *DarwinEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { - return true -} - -func (e *DarwinEndpoint) Close() { - if e.dispatcherCancel != nil { - e.dispatcherCancel() - e.dispatcherCancel = nil - } -} - -func (e *DarwinEndpoint) SetOnCloseAction(_ func()) { -} - -func (e *DarwinEndpoint) WritePackets(packetBufferList stack.PacketBufferList) (int, tcpip.Error) { - var n int - for _, packetBuffer := range packetBufferList.AsSlice() { - family, err := ipFamilyFromPacket(packetBuffer) - if err != nil { - return n, &tcpip.ErrAborted{} - } - - var headerBytes [utunHeaderSize]byte - binary.BigEndian.PutUint32(headerBytes[:], family) - - writeSlices := append([][]byte{headerBytes[:]}, packetBuffer.AsSlices()...) - if _, err := unix.Writev(e.tunFd, writeSlices); err != nil { - if errors.Is(err, unix.EAGAIN) { - return n, &tcpip.ErrWouldBlock{} - } - return n, &tcpip.ErrAborted{} - } - n++ - } - return n, nil -} - -func (e *DarwinEndpoint) dispatchLoop(ctx context.Context, dispatcher stack.NetworkDispatcher) { - readSize := int(e.mtu) - if readSize <= 0 { - readSize = 65535 - } - readSize += utunHeaderSize - - buf := make([]byte, readSize) - for ctx.Err() == nil { - - n, err := unix.Read(e.tunFd, buf) - if err != nil { - if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) { - continue - } - e.Attach(nil) - return - } - if n <= utunHeaderSize { - continue - } - - networkProtocol, packet, err := parseUTunPacket(buf[:n]) - if errors.Is(err, ErrUnsupportedNetworkProtocol) { - continue - } - if err != nil { - e.Attach(nil) - return - } - - dispatcher.DeliverNetworkPacket(networkProtocol, packet) - packet.DecRef() - } -} - -func parseUTunPacket(packet []byte) (tcpip.NetworkProtocolNumber, *stack.PacketBuffer, error) { - if len(packet) <= utunHeaderSize { - return 0, nil, errors.New("packet too short") - } - - family := binary.BigEndian.Uint32(packet[:utunHeaderSize]) - var networkProtocol tcpip.NetworkProtocolNumber - switch family { - case uint32(unix.AF_INET): - networkProtocol = header.IPv4ProtocolNumber - case uint32(unix.AF_INET6): - networkProtocol = header.IPv6ProtocolNumber - default: - return 0, nil, ErrUnsupportedNetworkProtocol - } - - payload := packet[utunHeaderSize:] - packetBuffer := buffer.MakeWithData(payload) - return networkProtocol, stack.NewPacketBuffer(stack.PacketBufferOptions{ - Payload: packetBuffer, - IsForwardedPacket: true, - }), nil -} - -func ipFamilyFromPacket(packetBuffer *stack.PacketBuffer) (uint32, error) { - for _, slice := range packetBuffer.AsSlices() { - if len(slice) == 0 { - continue - } - switch header.IPVersion(slice) { - case header.IPv4Version: - return uint32(unix.AF_INET), nil - case header.IPv6Version: - return uint32(unix.AF_INET6), nil - default: - return 0, ErrUnsupportedNetworkProtocol - } - } - return 0, errors.New("empty packet") -} diff --git a/proxy/tun/tun_windows.go b/proxy/tun/tun_windows.go index 92d200e9..1452441c 100644 --- a/proxy/tun/tun_windows.go +++ b/proxy/tun/tun_windows.go @@ -3,19 +3,28 @@ package tun import ( + "errors" + _ "unsafe" + "golang.org/x/sys/windows" "golang.zx2c4.com/wintun" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/stack" ) +//go:linkname procyield runtime.procyield +func procyield(cycles uint32) + // WindowsTun is an object that handles tun network interface on Windows // current version is heavily stripped to do nothing more, // then create a network interface, to be provided as endpoint to gVisor ip stack type WindowsTun struct { - options TunOptions - adapter *wintun.Adapter - session wintun.Session - MTU uint32 + options TunOptions + adapter *wintun.Adapter + session wintun.Session + readWait windows.Handle + MTU uint32 } // WindowsTun implements Tun @@ -24,6 +33,9 @@ var _ Tun = (*WindowsTun)(nil) // WindowsTun implements GVisorTun var _ GVisorTun = (*WindowsTun)(nil) +// WindowsTun implements GVisorDevice +var _ GVisorDevice = (*WindowsTun)(nil) + // NewTun creates a Wintun interface with the given name. Should a Wintun // interface with the same name exist, it tried to be reused. func NewTun(options TunOptions) (Tun, error) { @@ -41,9 +53,10 @@ func NewTun(options TunOptions) (Tun, error) { } tun := &WindowsTun{ - options: options, - adapter: adapter, - session: session, + options: options, + adapter: adapter, + session: session, + readWait: session.ReadWaitEvent(), // there is currently no iphndl.dll support, which is the netlink library for windows // so there is nowhere to change MTU for the Wintun interface, and we take its default value MTU: wintun.PacketSizeMax, @@ -78,7 +91,54 @@ func (t *WindowsTun) Close() error { return nil } -// newEndpoint builds new gVisor stack.LinkEndpoint (WintunEndpoint) on top of WindowsTun -func (t *WindowsTun) newEndpoint() (stack.LinkEndpoint, error) { - return &WintunEndpoint{tun: t}, nil +// WritePacket implements GVisorDevice method to write one packet to the tun device +func (t *WindowsTun) WritePacket(packetBuffer *stack.PacketBuffer) tcpip.Error { + // request buffer from Wintun + packet, err := t.session.AllocateSendPacket(packetBuffer.Size()) + if err != nil { + return &tcpip.ErrAborted{} + } + + // copy the bytes of slices that compose the packet into the allocated buffer + var index int + for _, packetElement := range packetBuffer.AsSlices() { + index += copy(packet[index:], packetElement) + } + + // signal Wintun to send that buffer as the packet + t.session.SendPacket(packet) + + return nil +} + +// ReadPacket implements GVisorDevice method to read one packet from the tun device +// It is expected that the method will not block, rather return ErrQueueEmpty when there is nothing on the line, +// which will make the stack call Wait which should implement desired push-back +func (t *WindowsTun) ReadPacket() (byte, *stack.PacketBuffer, error) { + packet, err := t.session.ReceivePacket() + if errors.Is(err, windows.ERROR_NO_MORE_ITEMS) { + return 0, nil, ErrQueueEmpty + } + if err != nil { + return 0, nil, err + } + + version := packet[0] >> 4 + packetBuffer := buffer.MakeWithView(buffer.NewViewWithData(packet)) + return version, stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: packetBuffer, + IsForwardedPacket: true, + OnRelease: func() { + t.session.ReleaseReceivePacket(packet) + }, + }), nil +} + +func (t *WindowsTun) Wait() { + procyield(1) + _, _ = windows.WaitForSingleObject(t.readWait, windows.INFINITE) +} + +func (t *WindowsTun) newEndpoint() (stack.LinkEndpoint, error) { + return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil } diff --git a/proxy/tun/tun_windows_endpoint.go b/proxy/tun/tun_windows_endpoint.go deleted file mode 100644 index f8769788..00000000 --- a/proxy/tun/tun_windows_endpoint.go +++ /dev/null @@ -1,180 +0,0 @@ -//go:build windows - -package tun - -import ( - "context" - "errors" - _ "unsafe" - - "golang.org/x/sys/windows" - "gvisor.dev/gvisor/pkg/buffer" - "gvisor.dev/gvisor/pkg/tcpip" - "gvisor.dev/gvisor/pkg/tcpip/header" - "gvisor.dev/gvisor/pkg/tcpip/stack" -) - -// WintunEndpoint implements GVisor stack.LinkEndpoint -var _ stack.LinkEndpoint = (*WintunEndpoint)(nil) - -type WintunEndpoint struct { - tun *WindowsTun - dispatcherCancel context.CancelFunc -} - -var ErrUnsupportedNetworkProtocol = errors.New("unsupported ip version") - -//go:linkname procyield runtime.procyield -func procyield(cycles uint32) - -func (e *WintunEndpoint) MTU() uint32 { - return e.tun.MTU -} - -func (e *WintunEndpoint) SetMTU(mtu uint32) { - // not Implemented, as it is not expected GVisor will be asking tun device to be modified -} - -func (e *WintunEndpoint) MaxHeaderLength() uint16 { - return 0 -} - -func (e *WintunEndpoint) LinkAddress() tcpip.LinkAddress { - return "" -} - -func (e *WintunEndpoint) SetLinkAddress(addr tcpip.LinkAddress) { - // not Implemented, as it is not expected GVisor will be asking tun device to be modified -} - -func (e *WintunEndpoint) Capabilities() stack.LinkEndpointCapabilities { - return stack.CapabilityRXChecksumOffload -} - -func (e *WintunEndpoint) Attach(dispatcher stack.NetworkDispatcher) { - if e.dispatcherCancel != nil { - e.dispatcherCancel() - e.dispatcherCancel = nil - } - - if dispatcher != nil { - ctx, cancel := context.WithCancel(context.Background()) - go e.dispatchLoop(ctx, dispatcher) - e.dispatcherCancel = cancel - } -} - -func (e *WintunEndpoint) IsAttached() bool { - return e.dispatcherCancel != nil -} - -func (e *WintunEndpoint) Wait() { - -} - -func (e *WintunEndpoint) ARPHardwareType() header.ARPHardwareType { - return header.ARPHardwareNone -} - -func (e *WintunEndpoint) AddHeader(buffer *stack.PacketBuffer) { - // tun interface doesn't have link layer header, it will be added by the OS -} - -func (e *WintunEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { - return true -} - -func (e *WintunEndpoint) Close() { - if e.dispatcherCancel != nil { - e.dispatcherCancel() - e.dispatcherCancel = nil - } -} - -func (e *WintunEndpoint) SetOnCloseAction(f func()) { - -} - -func (e *WintunEndpoint) WritePackets(packetBufferList stack.PacketBufferList) (int, tcpip.Error) { - var n int - // for all packets in the list to send - for _, packetBuffer := range packetBufferList.AsSlice() { - // request buffer from Wintun - packet, err := e.tun.session.AllocateSendPacket(packetBuffer.Size()) - if err != nil { - return n, &tcpip.ErrAborted{} - } - - // copy the bytes of slices that compose the packet into the allocated buffer - var index int - for _, packetElement := range packetBuffer.AsSlices() { - index += copy(packet[index:], packetElement) - } - - // signal Wintun to send that buffer as the packet - e.tun.session.SendPacket(packet) - n++ - } - return n, nil -} - -func (e *WintunEndpoint) readPacket() (tcpip.NetworkProtocolNumber, *stack.PacketBuffer, error) { - packet, err := e.tun.session.ReceivePacket() - if err != nil { - return 0, nil, err - } - - var networkProtocol tcpip.NetworkProtocolNumber - switch header.IPVersion(packet) { - case header.IPv4Version: - networkProtocol = header.IPv4ProtocolNumber - case header.IPv6Version: - networkProtocol = header.IPv6ProtocolNumber - default: - e.tun.session.ReleaseReceivePacket(packet) - return 0, nil, ErrUnsupportedNetworkProtocol - } - - packetBuffer := buffer.MakeWithView(buffer.NewViewWithData(packet)) - pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ - Payload: packetBuffer, - IsForwardedPacket: true, - OnRelease: func() { - e.tun.session.ReleaseReceivePacket(packet) - }, - }) - return networkProtocol, pkt, nil -} - -func (e *WintunEndpoint) dispatchLoop(ctx context.Context, dispatcher stack.NetworkDispatcher) { - readWait := e.tun.session.ReadWaitEvent() - - for { - select { - case <-ctx.Done(): - return - default: - networkProtocolNumber, packet, err := e.readPacket() - // read queue empty, yield slightly, wait for the spinlock, retry - if errors.Is(err, windows.ERROR_NO_MORE_ITEMS) { - procyield(1) - _, _ = windows.WaitForSingleObject(readWait, windows.INFINITE) - continue - } - // discard unknown network protocol packet - if errors.Is(err, ErrUnsupportedNetworkProtocol) { - continue - } - // stop dispatcher loop on any other interface failure - if err != nil { - e.Attach(nil) - continue - } - - // dispatch the buffer to the stack - dispatcher.DeliverNetworkPacket(networkProtocolNumber, packet) - // signal the buffer that it can be released - packet.DecRef() - } - } -} From f1aee0b7c514d37722a2bc931652b6de4f92d91b Mon Sep 17 00:00:00 2001 From: LjhAUMEM Date: Mon, 26 Jan 2026 02:28:51 +0800 Subject: [PATCH 128/136] Hysteria transport: Support range & random for `interval` in `udphop` as well (#5603) https://github.com/XTLS/Xray-core/pull/5560#issuecomment-3794621260 --- infra/conf/transport_internet.go | 13 ++++-- transport/internet/hysteria/config.pb.go | 51 +++++++++++++--------- transport/internet/hysteria/config.proto | 17 ++++---- transport/internet/hysteria/dialer.go | 2 +- transport/internet/hysteria/udphop/conn.go | 43 ++++++++++-------- 5 files changed, 75 insertions(+), 51 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index b7070be7..c23ed8dc 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -387,7 +387,7 @@ func (b Bandwidth) Bps() (uint64, error) { type UdpHop struct { PortList json.RawMessage `json:"port"` - Interval int64 `json:"interval"` + Interval *Int32Range `json:"interval"` } type HysteriaConfig struct { @@ -431,13 +431,19 @@ func (c *HysteriaConfig) Build() (proto.Message, error) { hop = &PortList{} } + var inertvalMin, inertvalMax int64 + if c.UdpHop.Interval != nil { + inertvalMin = int64(c.UdpHop.Interval.From) + inertvalMax = int64(c.UdpHop.Interval.To) + } + if up > 0 && up < 65536 { return nil, errors.New("Up must be at least 65536 Bps") } if down > 0 && down < 65536 { return nil, errors.New("Down must be at least 65536 Bps") } - if c.UdpHop.Interval != 0 && c.UdpHop.Interval < 5 { + if (inertvalMin != 0 && inertvalMin < 5) || (inertvalMax != 0 && inertvalMax < 5) { return nil, errors.New("Interval must be at least 5") } @@ -467,7 +473,8 @@ func (c *HysteriaConfig) Build() (proto.Message, error) { config.Up = up config.Down = down config.Ports = hop.Build().Ports() - config.Interval = c.UdpHop.Interval + config.IntervalMin = inertvalMin + config.IntervalMax = inertvalMax config.InitStreamReceiveWindow = c.InitStreamReceiveWindow config.MaxStreamReceiveWindow = c.MaxStreamReceiveWindow config.InitConnReceiveWindow = c.InitConnectionReceiveWindow diff --git a/transport/internet/hysteria/config.pb.go b/transport/internet/hysteria/config.pb.go index 8ec3a88e..ed5cbeaa 100644 --- a/transport/internet/hysteria/config.pb.go +++ b/transport/internet/hysteria/config.pb.go @@ -29,14 +29,15 @@ type Config struct { Up uint64 `protobuf:"varint,4,opt,name=up,proto3" json:"up,omitempty"` Down uint64 `protobuf:"varint,5,opt,name=down,proto3" json:"down,omitempty"` Ports []uint32 `protobuf:"varint,6,rep,packed,name=ports,proto3" json:"ports,omitempty"` - Interval int64 `protobuf:"varint,7,opt,name=interval,proto3" json:"interval,omitempty"` - InitStreamReceiveWindow uint64 `protobuf:"varint,8,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"` - MaxStreamReceiveWindow uint64 `protobuf:"varint,9,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"` - InitConnReceiveWindow uint64 `protobuf:"varint,10,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"` - MaxConnReceiveWindow uint64 `protobuf:"varint,11,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"` - MaxIdleTimeout int64 `protobuf:"varint,12,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"` - KeepAlivePeriod int64 `protobuf:"varint,13,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"` - DisablePathMtuDiscovery bool `protobuf:"varint,14,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"` + IntervalMin int64 `protobuf:"varint,7,opt,name=interval_min,json=intervalMin,proto3" json:"interval_min,omitempty"` + IntervalMax int64 `protobuf:"varint,8,opt,name=interval_max,json=intervalMax,proto3" json:"interval_max,omitempty"` + InitStreamReceiveWindow uint64 `protobuf:"varint,9,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"` + MaxStreamReceiveWindow uint64 `protobuf:"varint,10,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"` + InitConnReceiveWindow uint64 `protobuf:"varint,11,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"` + MaxConnReceiveWindow uint64 `protobuf:"varint,12,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"` + MaxIdleTimeout int64 `protobuf:"varint,13,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"` + KeepAlivePeriod int64 `protobuf:"varint,14,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"` + DisablePathMtuDiscovery bool `protobuf:"varint,15,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -113,9 +114,16 @@ func (x *Config) GetPorts() []uint32 { return nil } -func (x *Config) GetInterval() int64 { +func (x *Config) GetIntervalMin() int64 { if x != nil { - return x.Interval + return x.IntervalMin + } + return 0 +} + +func (x *Config) GetIntervalMax() int64 { + if x != nil { + return x.IntervalMax } return 0 } @@ -173,7 +181,7 @@ var File_transport_internet_hysteria_config_proto protoreflect.FileDescriptor const file_transport_internet_hysteria_config_proto_rawDesc = "" + "\n" + - "(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\xa7\x04\n" + + "(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\xd1\x04\n" + "\x06Config\x12\x18\n" + "\aversion\x18\x01 \x01(\x05R\aversion\x12\x12\n" + "\x04auth\x18\x02 \x01(\tR\x04auth\x12\x1e\n" + @@ -182,16 +190,17 @@ const file_transport_internet_hysteria_config_proto_rawDesc = "" + "congestion\x12\x0e\n" + "\x02up\x18\x04 \x01(\x04R\x02up\x12\x12\n" + "\x04down\x18\x05 \x01(\x04R\x04down\x12\x14\n" + - "\x05ports\x18\x06 \x03(\rR\x05ports\x12\x1a\n" + - "\binterval\x18\a \x01(\x03R\binterval\x12;\n" + - "\x1ainit_stream_receive_window\x18\b \x01(\x04R\x17initStreamReceiveWindow\x129\n" + - "\x19max_stream_receive_window\x18\t \x01(\x04R\x16maxStreamReceiveWindow\x127\n" + - "\x18init_conn_receive_window\x18\n" + - " \x01(\x04R\x15initConnReceiveWindow\x125\n" + - "\x17max_conn_receive_window\x18\v \x01(\x04R\x14maxConnReceiveWindow\x12(\n" + - "\x10max_idle_timeout\x18\f \x01(\x03R\x0emaxIdleTimeout\x12*\n" + - "\x11keep_alive_period\x18\r \x01(\x03R\x0fkeepAlivePeriod\x12;\n" + - "\x1adisable_path_mtu_discovery\x18\x0e \x01(\bR\x17disablePathMtuDiscoveryB\x82\x01\n" + + "\x05ports\x18\x06 \x03(\rR\x05ports\x12!\n" + + "\finterval_min\x18\a \x01(\x03R\vintervalMin\x12!\n" + + "\finterval_max\x18\b \x01(\x03R\vintervalMax\x12;\n" + + "\x1ainit_stream_receive_window\x18\t \x01(\x04R\x17initStreamReceiveWindow\x129\n" + + "\x19max_stream_receive_window\x18\n" + + " \x01(\x04R\x16maxStreamReceiveWindow\x127\n" + + "\x18init_conn_receive_window\x18\v \x01(\x04R\x15initConnReceiveWindow\x125\n" + + "\x17max_conn_receive_window\x18\f \x01(\x04R\x14maxConnReceiveWindow\x12(\n" + + "\x10max_idle_timeout\x18\r \x01(\x03R\x0emaxIdleTimeout\x12*\n" + + "\x11keep_alive_period\x18\x0e \x01(\x03R\x0fkeepAlivePeriod\x12;\n" + + "\x1adisable_path_mtu_discovery\x18\x0f \x01(\bR\x17disablePathMtuDiscoveryB\x82\x01\n" + "$com.xray.transport.internet.hysteriaP\x01Z5github.com/xtls/xray-core/transport/internet/hysteria\xaa\x02 Xray.Transport.Internet.Hysteriab\x06proto3" var ( diff --git a/transport/internet/hysteria/config.proto b/transport/internet/hysteria/config.proto index 787a2b40..09f97298 100644 --- a/transport/internet/hysteria/config.proto +++ b/transport/internet/hysteria/config.proto @@ -13,14 +13,15 @@ message Config { uint64 up = 4; uint64 down = 5; repeated uint32 ports = 6; - int64 interval = 7; + int64 interval_min = 7; + int64 interval_max = 8; - uint64 init_stream_receive_window = 8; - uint64 max_stream_receive_window = 9; - uint64 init_conn_receive_window = 10; - uint64 max_conn_receive_window = 11; - int64 max_idle_timeout = 12; - int64 keep_alive_period = 13; - bool disable_path_mtu_discovery = 14; + uint64 init_stream_receive_window = 9; + uint64 max_stream_receive_window = 10; + uint64 init_conn_receive_window = 11; + uint64 max_conn_receive_window = 12; + int64 max_idle_timeout = 13; + int64 keep_alive_period = 14; + bool disable_path_mtu_discovery = 15; } diff --git a/transport/internet/hysteria/dialer.go b/transport/internet/hysteria/dialer.go index d4723a1b..45ea580c 100644 --- a/transport/internet/hysteria/dialer.go +++ b/transport/internet/hysteria/dialer.go @@ -174,7 +174,7 @@ func (c *client) dial() error { IP: remote.(*net.UDPAddr).IP, Ports: c.config.Ports, } - pktConn, err = udphop.NewUDPHopPacketConn(addr, time.Duration(c.config.Interval)*time.Second, c.udphopDialer, pktConn, index) + pktConn, err = udphop.NewUDPHopPacketConn(addr, c.config.IntervalMin, c.config.IntervalMax, c.udphopDialer, pktConn, index) if err != nil { return errors.New("udphop err").Base(err) } diff --git a/transport/internet/hysteria/udphop/conn.go b/transport/internet/hysteria/udphop/conn.go index 5615ec5b..c19de476 100644 --- a/transport/internet/hysteria/udphop/conn.go +++ b/transport/internet/hysteria/udphop/conn.go @@ -7,6 +7,8 @@ import ( "sync" "syscall" "time" + + "github.com/xtls/xray-core/common/crypto" ) const ( @@ -17,10 +19,11 @@ const ( ) type udpHopPacketConn struct { - Addr net.Addr - Addrs []net.Addr - HopInterval time.Duration - ListenUDPFunc ListenUDPFunc + Addr net.Addr + Addrs []net.Addr + HopIntervalMin int64 + HopIntervalMax int64 + ListenUDPFunc ListenUDPFunc connMutex sync.RWMutex prevConn net.PacketConn @@ -46,10 +49,12 @@ type udpPacket struct { type ListenUDPFunc = func(*net.UDPAddr) (net.PacketConn, error) -func NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval time.Duration, listenUDPFunc ListenUDPFunc, pktConn net.PacketConn, index int) (net.PacketConn, error) { - if hopInterval == 0 { - hopInterval = defaultHopInterval - } else if hopInterval < 5*time.Second { +func NewUDPHopPacketConn(addr *UDPHopAddr, intervalMin int64, intervalMax int64, listenUDPFunc ListenUDPFunc, pktConn net.PacketConn, index int) (net.PacketConn, error) { + if intervalMin == 0 || intervalMax == 0 { + intervalMin = int64(defaultHopInterval) + intervalMax = int64(defaultHopInterval) + } + if intervalMin < 5 || intervalMax < 5 { return nil, errors.New("hop interval must be at least 5 seconds") } // if listenUDPFunc == nil { @@ -69,15 +74,16 @@ func NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval time.Duration, listenUDPF // return nil, err // } hConn := &udpHopPacketConn{ - Addr: addr, - Addrs: addrs, - HopInterval: hopInterval, - ListenUDPFunc: listenUDPFunc, - prevConn: nil, - currentConn: pktConn, - addrIndex: index, - recvQueue: make(chan *udpPacket, packetQueueSize), - closeChan: make(chan struct{}), + Addr: addr, + Addrs: addrs, + HopIntervalMin: intervalMin, + HopIntervalMax: intervalMax, + ListenUDPFunc: listenUDPFunc, + prevConn: nil, + currentConn: pktConn, + addrIndex: index, + recvQueue: make(chan *udpPacket, packetQueueSize), + closeChan: make(chan struct{}), bufPool: sync.Pool{ New: func() interface{} { return make([]byte, udpBufferSize) @@ -115,12 +121,13 @@ func (u *udpHopPacketConn) recvLoop(conn net.PacketConn) { } func (u *udpHopPacketConn) hopLoop() { - ticker := time.NewTicker(u.HopInterval) + ticker := time.NewTicker(time.Duration(crypto.RandBetween(u.HopIntervalMin, u.HopIntervalMax)) * time.Second) defer ticker.Stop() for { select { case <-ticker.C: u.hop() + ticker.Reset(time.Duration(crypto.RandBetween(u.HopIntervalMin, u.HopIntervalMax)) * time.Second) case <-u.closeChan: return } From 1951a278ac08d39dff70d036116837767076fc2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:31:34 +0000 Subject: [PATCH 129/136] Bump github.com/pires/go-proxyproto from 0.9.0 to 0.9.1 (#5608) Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.9.0 to 0.9.1. - [Release notes](https://github.com/pires/go-proxyproto/releases) - [Commits](https://github.com/pires/go-proxyproto/compare/v0.9.0...v0.9.1) --- updated-dependencies: - dependency-name: github.com/pires/go-proxyproto dependency-version: 0.9.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index edea5e74..7dc03d32 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/miekg/dns v1.1.72 github.com/pelletier/go-toml v1.9.5 - github.com/pires/go-proxyproto v0.9.0 + github.com/pires/go-proxyproto v0.9.1 github.com/refraction-networking/utls v1.8.2 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 diff --git a/go.sum b/go.sum index 5ea97096..1da1fc23 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pires/go-proxyproto v0.9.0 h1:3Qg3CLxWx4wJOw5uxhTvc0VrgsJeerDbGTvexu4UK1E= -github.com/pires/go-proxyproto v0.9.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pires/go-proxyproto v0.9.1 h1:wTPjpyk41pJm1Im9BqHtPLuhxfjxL+qNfSikx9ux0WY= +github.com/pires/go-proxyproto v0.9.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= From 9a04eecaf95c4d04e3ba43840d1898f0192c22e7 Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:45:25 +0800 Subject: [PATCH 130/136] Geodat: Reduce peak memory usage (#5581) Fixes https://github.com/XTLS/Xray-core/commit/5f7474120f523ad1e36174481e0b16c3446cc29c --- app/dns/hosts.go | 5 +- app/dns/nameserver.go | 10 +- app/router/condition.go | 3 +- app/router/condition_geoip.go | 3 +- app/router/config.go | 9 ++ common/platform/filesystem/file.go | 4 + infra/conf/router.go | 179 ++++++++++++++--------------- 7 files changed, 118 insertions(+), 95 deletions(-) diff --git a/app/dns/hosts.go b/app/dns/hosts.go index 0ee4fdd0..7c9cdee3 100644 --- a/app/dns/hosts.go +++ b/app/dns/hosts.go @@ -2,6 +2,7 @@ package dns import ( "context" + "runtime" "strconv" "github.com/xtls/xray-core/common/errors" @@ -24,7 +25,9 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) { matchers: g, } - for _, mapping := range hosts { + defer runtime.GC() + for i, mapping := range hosts { + hosts[i] = nil matcher, err := toStrMatcher(mapping.Type, mapping.Domain) if err != nil { errors.LogErrorInner(context.Background(), err, "failed to create domain matcher, ignore domain rule [type: ", mapping.Type, ", domain: ", mapping.Domain, "]") diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go index bad9277c..dbab5e8a 100644 --- a/app/dns/nameserver.go +++ b/app/dns/nameserver.go @@ -3,6 +3,7 @@ package dns import ( "context" "net/url" + "runtime" "strings" "time" @@ -131,7 +132,8 @@ func NewClient( var rules []string ruleCurr := 0 ruleIter := 0 - for _, domain := range ns.PrioritizedDomain { + for i, domain := range ns.PrioritizedDomain { + ns.PrioritizedDomain[i] = nil domainRule, err := toStrMatcher(domain.Type, domain.Domain) if err != nil { errors.LogErrorInner(ctx, err, "failed to create domain matcher, ignore domain rule [type: ", domain.Type, ", domain: ", domain.Domain, "]") @@ -154,6 +156,8 @@ func NewClient( } updateDomainRule(domainRule, originalRuleIdx, *matcherInfos) } + ns.PrioritizedDomain = nil + runtime.GC() // Establish expected IPs var expectedMatcher router.GeoIPMatcher @@ -162,6 +166,8 @@ func NewClient( if err != nil { return errors.New("failed to create expected ip matcher").Base(err).AtWarning() } + ns.ExpectedGeoip = nil + runtime.GC() } // Establish unexpected IPs @@ -171,6 +177,8 @@ func NewClient( if err != nil { return errors.New("failed to create unexpected ip matcher").Base(err).AtWarning() } + ns.UnexpectedGeoip = nil + runtime.GC() } if len(clientIP) > 0 { diff --git a/app/router/condition.go b/app/router/condition.go index c063bae3..873f121d 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -57,7 +57,8 @@ type DomainMatcher struct { func NewMphMatcherGroup(domains []*Domain) (*DomainMatcher, error) { g := strmatcher.NewMphMatcherGroup() - for _, d := range domains { + for i, d := range domains { + domains[i] = nil matcherType, f := matcherTypeMap[d.Type] if !f { errors.LogError(context.Background(), "ignore unsupported domain type ", d.Type, " of rule ", d.Value) diff --git a/app/router/condition_geoip.go b/app/router/condition_geoip.go index 823548cf..cdfcb9fe 100644 --- a/app/router/condition_geoip.go +++ b/app/router/condition_geoip.go @@ -822,7 +822,8 @@ func (f *GeoIPSetFactory) Create(cidrGroups ...[]*CIDR) (*GeoIPSet, error) { var ipv4Builder, ipv6Builder netipx.IPSetBuilder for _, cidrGroup := range cidrGroups { - for _, cidrEntry := range cidrGroup { + for i, cidrEntry := range cidrGroup { + cidrGroup[i] = nil ipBytes := cidrEntry.GetIp() prefixLen := int(cidrEntry.GetPrefix()) diff --git a/app/router/config.go b/app/router/config.go index 14310641..c41e6cfc 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -3,6 +3,7 @@ package router import ( "context" "regexp" + "runtime" "strings" "github.com/xtls/xray-core/common/errors" @@ -78,6 +79,8 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { return nil, err } conds.Add(cond) + rr.Geoip = nil + runtime.GC() } if len(rr.SourceGeoip) > 0 { @@ -86,6 +89,8 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { return nil, err } conds.Add(cond) + rr.SourceGeoip = nil + runtime.GC() } if len(rr.LocalGeoip) > 0 { @@ -95,6 +100,8 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } conds.Add(cond) errors.LogWarning(context.Background(), "Due to some limitations, in UDP connections, localIP is always equal to listen interface IP, so \"localIP\" rule condition does not work properly on UDP inbound connections that listen on all interfaces") + rr.LocalGeoip = nil + runtime.GC() } if len(rr.Domain) > 0 { @@ -104,6 +111,8 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)") conds.Add(matcher) + rr.Domain = nil + runtime.GC() } if len(rr.Process) > 0 { diff --git a/common/platform/filesystem/file.go b/common/platform/filesystem/file.go index e4fe2a9a..f36838ce 100644 --- a/common/platform/filesystem/file.go +++ b/common/platform/filesystem/file.go @@ -29,6 +29,10 @@ func ReadAsset(file string) ([]byte, error) { return ReadFile(platform.GetAssetLocation(file)) } +func OpenAsset(file string) (io.ReadCloser, error) { + return NewFileReader(platform.GetAssetLocation(file)) +} + func ReadCert(file string) ([]byte, error) { if filepath.IsAbs(file) { return ReadFile(file) diff --git a/infra/conf/router.go b/infra/conf/router.go index 69217c7a..1e1f6b80 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -1,7 +1,10 @@ package conf import ( + "bufio" + "bytes" "encoding/json" + "io" "runtime" "strconv" "strings" @@ -102,7 +105,7 @@ func (c *RouterConfig) Build() (*router.Config, error) { } for _, rawRule := range rawRuleList { - rule, err := ParseRule(rawRule) + rule, err := parseRule(rawRule) if err != nil { return nil, err } @@ -125,7 +128,7 @@ type RouterRule struct { BalancerTag string `json:"balancerTag"` } -func ParseIP(s string) (*router.CIDR, error) { +func parseIP(s string) (*router.CIDR, error) { var addr, mask string i := strings.Index(s, "/") if i < 0 { @@ -173,125 +176,119 @@ func ParseIP(s string) (*router.CIDR, error) { } } -func loadGeoIP(code string) ([]*router.CIDR, error) { - return loadIP("geoip.dat", code) -} - -var ( - FileCache = make(map[string][]byte) - IPCache = make(map[string]*router.GeoIP) - SiteCache = make(map[string]*router.GeoSite) -) - -func loadFile(file string) ([]byte, error) { - if FileCache[file] == nil { - bs, err := filesystem.ReadAsset(file) - if err != nil { - return nil, errors.New("failed to open file: ", file).Base(err) - } - if len(bs) == 0 { - return nil, errors.New("empty file: ", file) - } - // Do not cache file, may save RAM when there - // are many files, but consume CPU each time. - return bs, nil - FileCache[file] = bs +func loadFile(file, code string) ([]byte, error) { + runtime.GC() + r, err := filesystem.OpenAsset(file) + defer r.Close() + if err != nil { + return nil, errors.New("failed to open file: ", file).Base(err) } - return FileCache[file], nil + bs := find(r, []byte(code)) + if bs == nil { + return nil, errors.New("code not found in ", file, ": ", code) + } + return bs, nil } func loadIP(file, code string) ([]*router.CIDR, error) { - index := file + ":" + code - if IPCache[index] == nil { - bs, err := loadFile(file) - if err != nil { - return nil, errors.New("failed to load file: ", file).Base(err) - } - bs = find(bs, []byte(code)) - if bs == nil { - return nil, errors.New("code not found in ", file, ": ", code) - } - var geoip router.GeoIP - if err := proto.Unmarshal(bs, &geoip); err != nil { - return nil, errors.New("error unmarshal IP in ", file, ": ", code).Base(err) - } - defer runtime.GC() // or debug.FreeOSMemory() - return geoip.Cidr, nil // do not cache geoip - IPCache[index] = &geoip + bs, err := loadFile(file, code) + if err != nil { + return nil, err } - return IPCache[index].Cidr, nil + var geoip router.GeoIP + if err := proto.Unmarshal(bs, &geoip); err != nil { + return nil, errors.New("error unmarshal IP in ", file, ": ", code).Base(err) + } + defer runtime.GC() // or debug.FreeOSMemory() + return geoip.Cidr, nil } func loadSite(file, code string) ([]*router.Domain, error) { - index := file + ":" + code - if SiteCache[index] == nil { - bs, err := loadFile(file) - if err != nil { - return nil, errors.New("failed to load file: ", file).Base(err) - } - bs = find(bs, []byte(code)) - if bs == nil { - return nil, errors.New("list not found in ", file, ": ", code) - } - var geosite router.GeoSite - if err := proto.Unmarshal(bs, &geosite); err != nil { - return nil, errors.New("error unmarshal Site in ", file, ": ", code).Base(err) - } - defer runtime.GC() // or debug.FreeOSMemory() - return geosite.Domain, nil // do not cache geosite - SiteCache[index] = &geosite + bs, err := loadFile(file, code) + if err != nil { + return nil, err } - return SiteCache[index].Domain, nil + var geosite router.GeoSite + if err := proto.Unmarshal(bs, &geosite); err != nil { + return nil, errors.New("error unmarshal Site in ", file, ": ", code).Base(err) + } + defer runtime.GC() // or debug.FreeOSMemory() + return geosite.Domain, nil } -func DecodeVarint(buf []byte) (x uint64, n int) { +func decodeVarint(r *bufio.Reader) (uint64, error) { + var x uint64 for shift := uint(0); shift < 64; shift += 7 { - if n >= len(buf) { - return 0, 0 + b, err := r.ReadByte() + if err != nil { + return 0, err } - b := uint64(buf[n]) - n++ - x |= (b & 0x7F) << shift + x |= (uint64(b) & 0x7F) << shift if (b & 0x80) == 0 { - return x, n + return x, nil } } - // The number is too large to represent in a 64-bit value. - return 0, 0 + return 0, errors.New("varint overflow") } -func find(data, code []byte) []byte { +func find(r io.Reader, code []byte) []byte { codeL := len(code) if codeL == 0 { return nil } + + br := bufio.NewReaderSize(r, 64*1024) + need := 2 + codeL + prefixBuf := make([]byte, need) + for { - dataL := len(data) - if dataL < 2 { + if _, err := br.ReadByte(); err != nil { return nil } - x, y := DecodeVarint(data[1:]) - if x == 0 && y == 0 { + + x, err := decodeVarint(br) + if err != nil { return nil } - headL, bodyL := 1+y, int(x) - dataL -= headL - if dataL < bodyL { + bodyL := int(x) + if bodyL <= 0 { return nil } - data = data[headL:] - if int(data[1]) == codeL { - for i := 0; i < codeL && data[2+i] == code[i]; i++ { - if i+1 == codeL { - return data[:bodyL] - } + + prefixL := bodyL + if prefixL > need { + prefixL = need + } + prefix := prefixBuf[:prefixL] + if _, err := io.ReadFull(br, prefix); err != nil { + return nil + } + + match := false + if bodyL >= need { + if int(prefix[1]) == codeL && bytes.Equal(prefix[2:need], code) { + match = true } } - if dataL == bodyL { - return nil + + remain := bodyL - prefixL + if match { + out := make([]byte, bodyL) + copy(out, prefix) + if remain > 0 { + if _, err := io.ReadFull(br, out[prefixL:]); err != nil { + return nil + } + } + return out + } + + if remain > 0 { + if _, err := br.Discard(remain); err != nil { + return nil + } } - data = data[bodyL:] } } @@ -447,7 +444,7 @@ func ToCidrList(ips StringList) ([]*router.GeoIP, error) { if len(country) == 0 { return nil, errors.New("empty country name in rule") } - geoip, err := loadGeoIP(strings.ToUpper(country)) + geoip, err := loadIP("geoip.dat", strings.ToUpper(country)) if err != nil { return nil, errors.New("failed to load GeoIP: ", country).Base(err) } @@ -501,7 +498,7 @@ func ToCidrList(ips StringList) ([]*router.GeoIP, error) { continue } - ipRule, err := ParseIP(ip) + ipRule, err := parseIP(ip) if err != nil { return nil, errors.New("invalid IP: ", ip).Base(err) } @@ -655,7 +652,7 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) { return rule, nil } -func ParseRule(msg json.RawMessage) (*router.RoutingRule, error) { +func parseRule(msg json.RawMessage) (*router.RoutingRule, error) { rawRule := new(RouterRule) err := json.Unmarshal(msg, rawRule) if err != nil { From c3af657c0ef31997c5fb475d414334c3d8c21b58 Mon Sep 17 00:00:00 2001 From: Evozi Team <47205808+evozi-team@users.noreply.github.com> Date: Mon, 26 Jan 2026 20:43:10 +0800 Subject: [PATCH 131/136] TUN inbound: Add iOS support (#5612) And https://github.com/XTLS/Xray-core/pull/5612#issuecomment-3799070838 --- proxy/tun/README.md | 38 +++++++++++++++++++++++++++++++++++++- proxy/tun/tun_darwin.go | 31 ++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/proxy/tun/README.md b/proxy/tun/README.md index ea5d799d..ca081f5a 100644 --- a/proxy/tun/README.md +++ b/proxy/tun/README.md @@ -4,7 +4,7 @@ TUN interface support bridges the gap between network layer 3 and layer 7, intro This functionality is targeted to assist applications/end devices that don't have proxy support, or can't run external applications (like Smart TV's). Making it possible to run Xray proxy right on network edge devices (routers) with support to route raw network traffic. \ Primary targets are Linux based router devices. Like most popular OpenWRT option. \ -Although support for Windows is also implemented (see below). +Support for Windows, macOS, Android and iOS is also implemented (see below). ## PLEASE READ FOLLOWING CAREFULLY @@ -194,3 +194,39 @@ sudo route add -inet6 -host 2606:4700:4700::1111 -iface utun10 sudo route add -inet6 -host 2606:4700:4700::1001 -iface utun10 ``` Important to remember that everything written above about Linux routing concept, also apply to Mac OS X. If you simply route default route through utun interface, that will result network loop and immediate network failure. + +## ANDROID SUPPORT + +Android uses the VpnService API which provides a TUN file descriptor to the application. + +Obtain the fd from VpnService: +```kotlin +val tunFd = vpnInterface.fd +``` + +Set the environment variable `xray.tun.fd` (or `XRAY_TUN_FD`) to the fd number before starting Xray. This can be done from Kotlin/Java or by exposing a Go function via gomobile bindings. + +Build using gomobile for Android library integration: +``` +gomobile bind -target=android +``` + +## iOS SUPPORT + +iOS uses the same utun packet format as macOS, but the file descriptor is provided by NetworkExtension. + +Obtain the fd from NetworkExtension: +```swift +var buf = [CChar](repeating: 0, count: Int(IFNAMSIZ)) +let utunPrefix = "utun".utf8CString.dropLast() +let tunFd = ((0 ... 1024).first { (_ fd: Int32) -> Bool in var len = socklen_t(buf.count) + return getsockopt(fd, 2, 2, &buf, &len) == 0 && buf.starts(with: utunPrefix) +}! +``` + +Set the environment variable `xray.tun.fd` (or `XRAY_TUN_FD`) to the fd number before starting Xray. This can be done from Swift/Objective-C or by exposing a Go function via gomobile bindings. + +Build using gomobile for iOS framework integration: +``` +gomobile bind -target=ios +``` \ No newline at end of file diff --git a/proxy/tun/tun_darwin.go b/proxy/tun/tun_darwin.go index a1b24dee..ad9f4783 100644 --- a/proxy/tun/tun_darwin.go +++ b/proxy/tun/tun_darwin.go @@ -8,10 +8,12 @@ import ( "net" "net/netip" "os" + "strconv" "syscall" "unsafe" "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/platform" "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/buffer" "gvisor.dev/gvisor/pkg/tcpip" @@ -38,6 +40,7 @@ func procyield(cycles uint32) type DarwinTun struct { tunFile *os.File options TunOptions + ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system) } var _ Tun = (*DarwinTun)(nil) @@ -45,6 +48,27 @@ var _ GVisorTun = (*DarwinTun)(nil) var _ GVisorDevice = (*DarwinTun)(nil) func NewTun(options TunOptions) (Tun, error) { + // Check if fd is provided via environment (iOS mode) + fdStr := platform.NewEnvFlag(platform.TunFdKey).GetValue(func() string { return "" }) + if fdStr != "" { + // iOS: use provided fd from NetworkExtension + fd, err := strconv.Atoi(fdStr) + if err != nil { + return nil, err + } + + if err = unix.SetNonblock(fd, true); err != nil { + return nil, err + } + + return &DarwinTun{ + tunFile: os.NewFile(uintptr(fd), "utun"), + options: options, + ownsFd: false, + }, nil + } + + // macOS: create our own utun interface tunFile, err := open(options.Name) if err != nil { return nil, err @@ -59,6 +83,7 @@ func NewTun(options TunOptions) (Tun, error) { return &DarwinTun{ tunFile: tunFile, options: options, + ownsFd: true, }, nil } @@ -67,7 +92,11 @@ func (t *DarwinTun) Start() error { } func (t *DarwinTun) Close() error { - return t.tunFile.Close() + if t.ownsFd { + return t.tunFile.Close() + } + // iOS: don't close the fd, it's owned by NetworkExtension + return nil } // WritePacket implements GVisorDevice method to write one packet to the tun device From 077070dbe0a8a3e431d20987c43a1b05a134364f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:11:28 +0000 Subject: [PATCH 132/136] Bump github.com/pires/go-proxyproto from 0.9.1 to 0.9.2 (#5614) Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.9.1 to 0.9.2. - [Release notes](https://github.com/pires/go-proxyproto/releases) - [Commits](https://github.com/pires/go-proxyproto/compare/v0.9.1...v0.9.2) --- updated-dependencies: - dependency-name: github.com/pires/go-proxyproto dependency-version: 0.9.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7dc03d32..fecdc7b0 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/miekg/dns v1.1.72 github.com/pelletier/go-toml v1.9.5 - github.com/pires/go-proxyproto v0.9.1 + github.com/pires/go-proxyproto v0.9.2 github.com/refraction-networking/utls v1.8.2 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 diff --git a/go.sum b/go.sum index 1da1fc23..ff116067 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pires/go-proxyproto v0.9.1 h1:wTPjpyk41pJm1Im9BqHtPLuhxfjxL+qNfSikx9ux0WY= -github.com/pires/go-proxyproto v0.9.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U= +github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= From f6a7e939231e5ec6b167628bf730dc70a3c36707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Wed, 28 Jan 2026 02:34:46 +0800 Subject: [PATCH 133/136] VMess inbound: Optimize replay filter (#5562) And https://github.com/XTLS/Xray-core/pull/5562#issuecomment-3765387903 --- common/antireplay/antireplay.go | 6 --- common/antireplay/antireplay_test.go | 33 ++++++++++++++++ common/antireplay/bloomring.go | 36 ----------------- common/antireplay/mapfilter.go | 46 ++++++++++++++++++++++ common/antireplay/replayfilter.go | 58 ---------------------------- go.mod | 4 -- go.sum | 13 ------- infra/conf/shadowsocks.go | 8 ---- proxy/shadowsocks/config.go | 23 +---------- proxy/shadowsocks/protocol.go | 10 ----- proxy/shadowsocks/validator.go | 1 - proxy/vmess/aead/authid.go | 6 +-- 12 files changed, 84 insertions(+), 160 deletions(-) delete mode 100644 common/antireplay/antireplay.go create mode 100644 common/antireplay/antireplay_test.go delete mode 100644 common/antireplay/bloomring.go create mode 100644 common/antireplay/mapfilter.go delete mode 100644 common/antireplay/replayfilter.go diff --git a/common/antireplay/antireplay.go b/common/antireplay/antireplay.go deleted file mode 100644 index 4d1a9319..00000000 --- a/common/antireplay/antireplay.go +++ /dev/null @@ -1,6 +0,0 @@ -package antireplay - -type GeneralizedReplayFilter interface { - Interval() int64 - Check(sum []byte) bool -} diff --git a/common/antireplay/antireplay_test.go b/common/antireplay/antireplay_test.go new file mode 100644 index 00000000..57e97184 --- /dev/null +++ b/common/antireplay/antireplay_test.go @@ -0,0 +1,33 @@ +package antireplay + +import ( + "bufio" + "crypto/rand" + "testing" +) + +func BenchmarkMapFilter(b *testing.B) { + filter := NewMapFilter[[16]byte](120) + var sample [16]byte + reader := bufio.NewReader(rand.Reader) + reader.Read(sample[:]) + b.ResetTimer() + for range b.N { + reader.Read(sample[:]) + filter.Check(sample) + } +} + +func TestMapFilter(t *testing.T) { + filter := NewMapFilter[[16]byte](120) + var sample [16]byte + rand.Read(sample[:]) + filter.Check(sample) + if filter.Check(sample) { + t.Error("Unexpected true negative") + } + sample[0]++ + if !filter.Check(sample) { + t.Error("Unexpected false positive") + } +} diff --git a/common/antireplay/bloomring.go b/common/antireplay/bloomring.go deleted file mode 100644 index 50fbbffe..00000000 --- a/common/antireplay/bloomring.go +++ /dev/null @@ -1,36 +0,0 @@ -package antireplay - -import ( - "sync" - - ss_bloomring "github.com/v2fly/ss-bloomring" -) - -type BloomRing struct { - *ss_bloomring.BloomRing - lock *sync.Mutex -} - -func (b BloomRing) Interval() int64 { - return 9999999 -} - -func (b BloomRing) Check(sum []byte) bool { - b.lock.Lock() - defer b.lock.Unlock() - if b.Test(sum) { - return false - } - b.Add(sum) - return true -} - -func NewBloomRing() BloomRing { - const ( - DefaultSFCapacity = 1e6 - // FalsePositiveRate - DefaultSFFPR = 1e-6 - DefaultSFSlot = 10 - ) - return BloomRing{ss_bloomring.NewBloomRing(DefaultSFSlot, DefaultSFCapacity, DefaultSFFPR), &sync.Mutex{}} -} diff --git a/common/antireplay/mapfilter.go b/common/antireplay/mapfilter.go new file mode 100644 index 00000000..8cfef953 --- /dev/null +++ b/common/antireplay/mapfilter.go @@ -0,0 +1,46 @@ +package antireplay + +import ( + "sync" + "time" +) + +// ReplayFilter checks for replay attacks. +type ReplayFilter[T comparable] struct { + lock sync.Mutex + poolA map[T]struct{} + poolB map[T]struct{} + interval time.Duration + lastClean time.Time +} + +// NewMapFilter create a new filter with specifying the expiration time interval in seconds. +func NewMapFilter[T comparable](interval int64) *ReplayFilter[T] { + filter := &ReplayFilter[T]{ + poolA: make(map[T]struct{}), + poolB: make(map[T]struct{}), + interval: time.Duration(interval) * time.Second, + lastClean: time.Now(), + } + return filter +} + +// Check determines if there are duplicate records. +func (filter *ReplayFilter[T]) Check(sum T) bool { + filter.lock.Lock() + defer filter.lock.Unlock() + + now := time.Now() + if now.Sub(filter.lastClean) >= filter.interval { + filter.poolB = filter.poolA + filter.poolA = make(map[T]struct{}) + filter.lastClean = now + } + + _, existsA := filter.poolA[sum] + _, existsB := filter.poolB[sum] + if !existsA && !existsB { + filter.poolA[sum] = struct{}{} + } + return !(existsA || existsB) +} diff --git a/common/antireplay/replayfilter.go b/common/antireplay/replayfilter.go deleted file mode 100644 index ffd7d68d..00000000 --- a/common/antireplay/replayfilter.go +++ /dev/null @@ -1,58 +0,0 @@ -package antireplay - -import ( - "sync" - "time" - - cuckoo "github.com/seiflotfy/cuckoofilter" -) - -const replayFilterCapacity = 100000 - -// ReplayFilter checks for replay attacks. -type ReplayFilter struct { - lock sync.Mutex - poolA *cuckoo.Filter - poolB *cuckoo.Filter - poolSwap bool - lastSwap int64 - interval int64 -} - -// NewReplayFilter create a new filter with specifying the expiration time interval in seconds. -func NewReplayFilter(interval int64) *ReplayFilter { - filter := &ReplayFilter{} - filter.interval = interval - return filter -} - -// Interval in second for expiration time for duplicate records. -func (filter *ReplayFilter) Interval() int64 { - return filter.interval -} - -// Check determines if there are duplicate records. -func (filter *ReplayFilter) Check(sum []byte) bool { - filter.lock.Lock() - defer filter.lock.Unlock() - - now := time.Now().Unix() - if filter.lastSwap == 0 { - filter.lastSwap = now - filter.poolA = cuckoo.NewFilter(replayFilterCapacity) - filter.poolB = cuckoo.NewFilter(replayFilterCapacity) - } - - elapsed := now - filter.lastSwap - if elapsed >= filter.Interval() { - if filter.poolSwap { - filter.poolA.Reset() - } else { - filter.poolB.Reset() - } - filter.poolSwap = !filter.poolSwap - filter.lastSwap = now - } - - return filter.poolA.InsertUnique(sum) && filter.poolB.InsertUnique(sum) -} diff --git a/go.mod b/go.mod index fecdc7b0..fee03452 100644 --- a/go.mod +++ b/go.mod @@ -15,9 +15,7 @@ require ( github.com/refraction-networking/utls v1.8.2 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 - github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 github.com/stretchr/testify v1.11.1 - github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e github.com/vishvananda/netlink v1.3.1 github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba @@ -38,7 +36,6 @@ require ( require ( github.com/andybalholm/brotli v1.0.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 // indirect github.com/google/btree v1.1.2 // indirect github.com/juju/ratelimit v1.0.2 // indirect github.com/klauspost/compress v1.17.4 // indirect @@ -46,7 +43,6 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect - github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/vishvananda/netns v0.0.5 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/text v0.33.0 // indirect diff --git a/go.sum b/go.sum index ff116067..c21537b5 100644 --- a/go.sum +++ b/go.sum @@ -5,11 +5,8 @@ github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178/go.mod h1:N1WIj github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 h1:BS21ZUJ/B5X2UVUbczfmdWH7GapPWAhxcMsDnjJTU1E= -github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -54,22 +51,14 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= -github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= -github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= -github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4= -github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= -github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= @@ -154,8 +143,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2 h1:fr6L00yGG2RP5NMea6njWpdC+bm+cMdFClrSpaicp1c= diff --git a/infra/conf/shadowsocks.go b/infra/conf/shadowsocks.go index a1a0b89e..490c2997 100644 --- a/infra/conf/shadowsocks.go +++ b/infra/conf/shadowsocks.go @@ -46,7 +46,6 @@ type ShadowsocksServerConfig struct { Email string `json:"email"` Users []*ShadowsocksUserConfig `json:"clients"` NetworkList *NetworkList `json:"network"` - IVCheck bool `json:"ivCheck"` } func (v *ShadowsocksServerConfig) Build() (proto.Message, error) { @@ -64,7 +63,6 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) { account := &shadowsocks.Account{ Password: user.Password, CipherType: cipherFromString(user.Cipher), - IvCheck: v.IVCheck, } if account.Password == "" { return nil, errors.New("Shadowsocks password is not specified.") @@ -83,7 +81,6 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) { account := &shadowsocks.Account{ Password: v.Password, CipherType: cipherFromString(v.Cipher), - IvCheck: v.IVCheck, } if account.Password == "" { return nil, errors.New("Shadowsocks password is not specified.") @@ -168,7 +165,6 @@ type ShadowsocksServerTarget struct { Email string `json:"email"` Cipher string `json:"method"` Password string `json:"password"` - IVCheck bool `json:"ivCheck"` UoT bool `json:"uot"` UoTVersion int `json:"uotVersion"` } @@ -180,7 +176,6 @@ type ShadowsocksClientConfig struct { Email string `json:"email"` Cipher string `json:"method"` Password string `json:"password"` - IVCheck bool `json:"ivCheck"` UoT bool `json:"uot"` UoTVersion int `json:"uotVersion"` Servers []*ShadowsocksServerTarget `json:"servers"` @@ -198,7 +193,6 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { Email: v.Email, Cipher: v.Cipher, Password: v.Password, - IVCheck: v.IVCheck, UoT: v.UoT, UoTVersion: v.UoTVersion, }, @@ -254,8 +248,6 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { return nil, errors.New("unknown cipher method: ", server.Cipher) } - account.IvCheck = server.IVCheck - ss := &protocol.ServerEndpoint{ Address: server.Address.Build(), Port: uint32(server.Port), diff --git a/proxy/shadowsocks/config.go b/proxy/shadowsocks/config.go index 39c397fa..dc8bff0b 100644 --- a/proxy/shadowsocks/config.go +++ b/proxy/shadowsocks/config.go @@ -5,11 +5,11 @@ import ( "crypto/cipher" "crypto/md5" "crypto/sha1" - "google.golang.org/protobuf/proto" "io" + "google.golang.org/protobuf/proto" + "github.com/xtls/xray-core/common" - "github.com/xtls/xray-core/common/antireplay" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/errors" @@ -24,8 +24,6 @@ type MemoryAccount struct { CipherType CipherType Key []byte Password string - - replayFilter antireplay.GeneralizedReplayFilter } var ErrIVNotUnique = errors.New("IV is not unique") @@ -42,20 +40,9 @@ func (a *MemoryAccount) ToProto() proto.Message { return &Account{ CipherType: a.CipherType, Password: a.Password, - IvCheck: a.replayFilter != nil, } } -func (a *MemoryAccount) CheckIV(iv []byte) error { - if a.replayFilter == nil { - return nil - } - if a.replayFilter.Check(iv) { - return nil - } - return ErrIVNotUnique -} - func createAesGcm(key []byte) cipher.AEAD { return crypto.NewAesGcm(key) } @@ -116,12 +103,6 @@ func (a *Account) AsAccount() (protocol.Account, error) { CipherType: a.CipherType, Key: passwordToCipherKey([]byte(a.Password), Cipher.KeySize()), Password: a.Password, - replayFilter: func() antireplay.GeneralizedReplayFilter { - if a.IvCheck { - return antireplay.NewBloomRing() - } - return nil - }(), }, nil } diff --git a/proxy/shadowsocks/protocol.go b/proxy/shadowsocks/protocol.go index c992f619..88006cde 100644 --- a/proxy/shadowsocks/protocol.go +++ b/proxy/shadowsocks/protocol.go @@ -139,9 +139,6 @@ func WriteTCPRequest(request *protocol.RequestHeader, writer io.Writer) (buf.Wri if account.Cipher.IVSize() > 0 { iv = make([]byte, account.Cipher.IVSize()) common.Must2(rand.Read(iv)) - if ivError := account.CheckIV(iv); ivError != nil { - return nil, errors.New("failed to mark outgoing iv").Base(ivError) - } if err := buf.WriteAllBytes(writer, iv, nil); err != nil { return nil, errors.New("failed to write IV") } @@ -188,10 +185,6 @@ func ReadTCPResponse(user *protocol.MemoryUser, reader io.Reader) (buf.Reader, e } } - if ivError := account.CheckIV(iv); ivError != nil { - return nil, drain.WithError(drainer, reader, errors.New("failed iv check").Base(ivError)) - } - return account.Cipher.NewDecryptionReader(account.Key, iv, reader) } @@ -203,9 +196,6 @@ func WriteTCPResponse(request *protocol.RequestHeader, writer io.Writer) (buf.Wr if account.Cipher.IVSize() > 0 { iv = make([]byte, account.Cipher.IVSize()) common.Must2(rand.Read(iv)) - if ivError := account.CheckIV(iv); ivError != nil { - return nil, errors.New("failed to mark outgoing iv").Base(ivError) - } if err := buf.WriteAllBytes(writer, iv, nil); err != nil { return nil, errors.New("failed to write IV.").Base(err) } diff --git a/proxy/shadowsocks/validator.go b/proxy/shadowsocks/validator.go index 84e59dc1..2c48334f 100644 --- a/proxy/shadowsocks/validator.go +++ b/proxy/shadowsocks/validator.go @@ -140,7 +140,6 @@ func (v *Validator) Get(bs []byte, command protocol.RequestCommand) (u *protocol if matchErr == nil { u = user - err = account.CheckIV(iv) return } } else { diff --git a/proxy/vmess/aead/authid.go b/proxy/vmess/aead/authid.go index 7309010a..478bb19c 100644 --- a/proxy/vmess/aead/authid.go +++ b/proxy/vmess/aead/authid.go @@ -68,12 +68,12 @@ func (aidd *AuthIDDecoder) Decode(data [16]byte) (int64, uint32, int32, []byte) } func NewAuthIDDecoderHolder() *AuthIDDecoderHolder { - return &AuthIDDecoderHolder{make(map[string]*AuthIDDecoderItem), antireplay.NewReplayFilter(120)} + return &AuthIDDecoderHolder{make(map[string]*AuthIDDecoderItem), antireplay.NewMapFilter[[16]byte](120)} } type AuthIDDecoderHolder struct { decoders map[string]*AuthIDDecoderItem - filter *antireplay.ReplayFilter + filter *antireplay.ReplayFilter[[16]byte] } type AuthIDDecoderItem struct { @@ -111,7 +111,7 @@ func (a *AuthIDDecoderHolder) Match(authID [16]byte) (interface{}, error) { return nil, ErrInvalidTime } - if !a.filter.Check(authID[:]) { + if !a.filter.Check(authID) { return nil, ErrReplay } From 19186edfa13b677672daaba4b40dbba301c36355 Mon Sep 17 00:00:00 2001 From: nasaboy Date: Fri, 30 Jan 2026 22:06:17 +0800 Subject: [PATCH 134/136] README.md: Add Egern & Quantumult X to Others (#5624) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b34104bb..53c94277 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,8 @@ - iOS & macOS arm64 & tvOS - [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118) - [Loon](https://apps.apple.com/us/app/loon/id1373567447) + - [Egern](https://apps.apple.com/us/app/egern/id1616105820) + - [Quantumult X](https://apps.apple.com/us/app/quantumult-x/id1443988620) - Xray Tools - [xray-knife](https://github.com/lilendian0x00/xray-knife) - [xray-checker](https://github.com/kutovoys/xray-checker) From 9c46a2d55a46490867589c03aada2dd6b5ffb53f Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:39:13 +0000 Subject: [PATCH 135/136] Upgrade gVisor to latest version v0.0.0-20260122175437-89a5d21be8f0 https://github.com/XTLS/Xray-core/issues/5561#issuecomment-3767618362 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index fee03452..77d8780c 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 google.golang.org/grpc v1.78.0 google.golang.org/protobuf v1.36.11 - gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2 + gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h12.io/socks v1.0.3 lukechampine.com/blake3 v1.4.1 ) diff --git a/go.sum b/go.sum index c21537b5..5cfde437 100644 --- a/go.sum +++ b/go.sum @@ -145,8 +145,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2 h1:fr6L00yGG2RP5NMea6njWpdC+bm+cMdFClrSpaicp1c= -gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= +gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk= +gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo= h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= From 2c92339f95fe9aa493b6ae51d3b07017a44c4014 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Fri, 30 Jan 2026 22:15:46 +0000 Subject: [PATCH 136/136] TLS config: `allowInsecure`->`pinnedPeerCertSha256`; `verifyPeerCertInNames`->`verifyPeerCertByName` And use `,` as the separator instead of `~`/array https://github.com/XTLS/Xray-core/pull/5567#issuecomment-3766081805 https://t.me/projectXtls/1464 https://t.me/projectXtls/1465 https://t.me/projectXtls/1466 https://github.com/XTLS/Xray-core/pull/5625#issuecomment-3824855736 --- common/protocol/tls/cert/cert.go | 5 +- infra/conf/transport_internet.go | 35 ++-- testing/scenarios/tls_test.go | 42 ++-- testing/scenarios/vless_test.go | 20 +- .../internet/httpupgrade/httpupgrade_test.go | 6 +- .../internet/splithttp/splithttp_test.go | 14 +- transport/internet/tcp/dialer.go | 12 +- transport/internet/tls/config.go | 37 ++-- transport/internet/tls/config.pb.go | 181 +++++++----------- transport/internet/tls/config.proto | 21 +- transport/internet/tls/config_test.go | 10 +- transport/internet/tls/pin_test.go | 14 +- transport/internet/websocket/ws_test.go | 6 +- 13 files changed, 177 insertions(+), 226 deletions(-) diff --git a/common/protocol/tls/cert/cert.go b/common/protocol/tls/cert/cert.go index 00498579..0a581e07 100644 --- a/common/protocol/tls/cert/cert.go +++ b/common/protocol/tls/cert/cert.go @@ -6,6 +6,7 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" "encoding/asn1" "encoding/pem" @@ -87,10 +88,10 @@ func Organization(org string) Option { } } -func MustGenerate(parent *Certificate, opts ...Option) *Certificate { +func MustGenerate(parent *Certificate, opts ...Option) (*Certificate, [32]byte) { cert, err := Generate(parent, opts...) common.Must(err) - return cert + return cert, sha256.Sum256(cert.Certificate) } func publicKey(priv interface{}) interface{} { diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index c23ed8dc..a451a310 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -568,7 +568,7 @@ func (c *TLSCertConfig) Build() (*tls.Certificate, error) { } type TLSConfig struct { - Insecure bool `json:"allowInsecure"` + AllowInsecure bool `json:"allowInsecure"` Certs []*TLSCertConfig `json:"certificates"` ServerName string `json:"serverName"` ALPN *StringList `json:"alpn"` @@ -579,10 +579,10 @@ type TLSConfig struct { CipherSuites string `json:"cipherSuites"` Fingerprint string `json:"fingerprint"` RejectUnknownSNI bool `json:"rejectUnknownSni"` - PinnedPeerCertSha256 string `json:"pinnedPeerCertSha256"` CurvePreferences *StringList `json:"curvePreferences"` MasterKeyLog string `json:"masterKeyLog"` - ServerNameToVerify string `json:"serverNameToVerify"` + PinnedPeerCertSha256 string `json:"pinnedPeerCertSha256"` + VerifyPeerCertByName string `json:"verifyPeerCertByName"` VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` ECHServerKeys string `json:"echServerKeys"` ECHConfigList string `json:"echConfigList"` @@ -602,10 +602,6 @@ func (c *TLSConfig) Build() (proto.Message, error) { config.Certificate[idx] = cert } serverName := c.ServerName - config.AllowInsecure = c.Insecure - if config.AllowInsecure { - errors.PrintDeprecatedFeatureWarning("allowInsecure", "pinnedPeerCertSha256") - } if len(c.ServerName) > 0 { config.ServerName = serverName } @@ -632,12 +628,13 @@ func (c *TLSConfig) Build() (proto.Message, error) { return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint) } config.RejectUnknownSni = c.RejectUnknownSNI + config.MasterKeyLog = c.MasterKeyLog + if c.AllowInsecure { + return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"`) + } if c.PinnedPeerCertSha256 != "" { - config.PinnedPeerCertSha256 = [][]byte{} - // Split by tilde separator - hashes := strings.Split(c.PinnedPeerCertSha256, "~") - for _, v := range hashes { + for v := range strings.SplitSeq(c.PinnedPeerCertSha256, ",") { v = strings.TrimSpace(v) if v == "" { continue @@ -650,12 +647,18 @@ func (c *TLSConfig) Build() (proto.Message, error) { } } - config.MasterKeyLog = c.MasterKeyLog - - if c.ServerNameToVerify != "" { - return nil, errors.PrintRemovedFeatureError(`"serverNameToVerify"`, `"verifyPeerCertInNames"`) + if c.VerifyPeerCertInNames != nil { + return nil, errors.PrintRemovedFeatureError(`"verifyPeerCertInNames"`, `"verifyPeerCertByName"`) + } + if c.VerifyPeerCertByName != "" { + for v := range strings.SplitSeq(c.VerifyPeerCertByName, ",") { + v = strings.TrimSpace(v) + if v == "" { + continue + } + config.VerifyPeerCertByName = append(config.VerifyPeerCertByName, v) + } } - config.VerifyPeerCertInNames = c.VerifyPeerCertInNames if c.ECHServerKeys != "" { EchPrivateKey, err := base64.StdEncoding.DecodeString(c.ECHServerKeys) diff --git a/testing/scenarios/tls_test.go b/testing/scenarios/tls_test.go index 69b38288..1a2f9661 100644 --- a/testing/scenarios/tls_test.go +++ b/testing/scenarios/tls_test.go @@ -36,6 +36,8 @@ func TestSimpleTLSConnection(t *testing.T) { common.Must(err) defer tcpServer.Close() + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + userID := protocol.NewID(uuid.New()) serverPort := tcp.PickPort() serverConfig := &core.Config{ @@ -48,7 +50,7 @@ func TestSimpleTLSConnection(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, }), }, }, @@ -104,7 +106,7 @@ func TestSimpleTLSConnection(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - AllowInsecure: true, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, }), }, }, @@ -247,6 +249,8 @@ func TestTLSOverKCP(t *testing.T) { common.Must(err) defer tcpServer.Close() + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + userID := protocol.NewID(uuid.New()) serverPort := udp.PickPort() serverConfig := &core.Config{ @@ -260,7 +264,7 @@ func TestTLSOverKCP(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, }), }, }, @@ -317,7 +321,7 @@ func TestTLSOverKCP(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - AllowInsecure: true, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, }), }, }, @@ -343,6 +347,8 @@ func TestTLSOverWebSocket(t *testing.T) { common.Must(err) defer tcpServer.Close() + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + userID := protocol.NewID(uuid.New()) serverPort := tcp.PickPort() serverConfig := &core.Config{ @@ -356,7 +362,7 @@ func TestTLSOverWebSocket(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, }), }, }, @@ -419,7 +425,7 @@ func TestTLSOverWebSocket(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - AllowInsecure: true, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, }), }, }, @@ -449,6 +455,8 @@ func TestGRPC(t *testing.T) { common.Must(err) defer tcpServer.Close() + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + userID := protocol.NewID(uuid.New()) serverPort := tcp.PickPort() serverConfig := &core.Config{ @@ -468,7 +476,7 @@ func TestGRPC(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, }), }, }, @@ -531,7 +539,7 @@ func TestGRPC(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - AllowInsecure: true, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, }), }, }, @@ -561,6 +569,8 @@ func TestGRPCMultiMode(t *testing.T) { common.Must(err) defer tcpServer.Close() + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + userID := protocol.NewID(uuid.New()) serverPort := tcp.PickPort() serverConfig := &core.Config{ @@ -580,7 +590,7 @@ func TestGRPCMultiMode(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, }), }, }, @@ -643,7 +653,7 @@ func TestGRPCMultiMode(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - AllowInsecure: true, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, }), }, }, @@ -672,7 +682,7 @@ func TestSimpleTLSConnectionPinned(t *testing.T) { dest, err := tcpServer.Start() common.Must(err) defer tcpServer.Close() - certificateDer := cert.MustGenerate(nil) + certificateDer, _ := cert.MustGenerate(nil) certificate := tls.ParseCertificate(certificateDer) certHash := tls.GenerateCertHash(certificateDer.Certificate) userID := protocol.NewID(uuid.New()) @@ -743,7 +753,6 @@ func TestSimpleTLSConnectionPinned(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - AllowInsecure: true, PinnedPeerCertSha256: [][]byte{certHash}, }), }, @@ -769,7 +778,7 @@ func TestSimpleTLSConnectionPinnedWrongCert(t *testing.T) { dest, err := tcpServer.Start() common.Must(err) defer tcpServer.Close() - certificateDer := cert.MustGenerate(nil) + certificateDer, _ := cert.MustGenerate(nil) certificate := tls.ParseCertificate(certificateDer) certHash := tls.GenerateCertHash(certificateDer.Certificate) certHash[1] += 1 @@ -841,7 +850,6 @@ func TestSimpleTLSConnectionPinnedWrongCert(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - AllowInsecure: true, PinnedPeerCertSha256: [][]byte{certHash}, }), }, @@ -867,7 +875,7 @@ func TestUTLSConnectionPinned(t *testing.T) { dest, err := tcpServer.Start() common.Must(err) defer tcpServer.Close() - certificateDer := cert.MustGenerate(nil) + certificateDer, _ := cert.MustGenerate(nil) certificate := tls.ParseCertificate(certificateDer) certHash := tls.GenerateCertHash(certificateDer.Certificate) userID := protocol.NewID(uuid.New()) @@ -939,7 +947,6 @@ func TestUTLSConnectionPinned(t *testing.T) { SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ Fingerprint: "random", - AllowInsecure: true, PinnedPeerCertSha256: [][]byte{certHash}, }), }, @@ -965,7 +972,7 @@ func TestUTLSConnectionPinnedWrongCert(t *testing.T) { dest, err := tcpServer.Start() common.Must(err) defer tcpServer.Close() - certificateDer := cert.MustGenerate(nil) + certificateDer, _ := cert.MustGenerate(nil) certificate := tls.ParseCertificate(certificateDer) certHash := tls.GenerateCertHash(certificateDer.Certificate) certHash[1] += 1 @@ -1038,7 +1045,6 @@ func TestUTLSConnectionPinnedWrongCert(t *testing.T) { SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ Fingerprint: "random", - AllowInsecure: true, PinnedPeerCertSha256: [][]byte{certHash}, }), }, diff --git a/testing/scenarios/vless_test.go b/testing/scenarios/vless_test.go index b699f497..446ce7b9 100644 --- a/testing/scenarios/vless_test.go +++ b/testing/scenarios/vless_test.go @@ -97,7 +97,7 @@ func TestVless(t *testing.T) { Vnext: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vless.Account{ Id: userID.String(), }), @@ -129,6 +129,8 @@ func TestVlessTls(t *testing.T) { common.Must(err) defer tcpServer.Close() + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + userID := protocol.NewID(uuid.New()) serverPort := tcp.PickPort() serverConfig := &core.Config{ @@ -148,7 +150,7 @@ func TestVlessTls(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, }), }, }, @@ -198,7 +200,7 @@ func TestVlessTls(t *testing.T) { Vnext: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vless.Account{ Id: userID.String(), }), @@ -217,7 +219,7 @@ func TestVlessTls(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - AllowInsecure: true, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, }), }, }, @@ -247,6 +249,8 @@ func TestVlessXtlsVision(t *testing.T) { common.Must(err) defer tcpServer.Close() + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + userID := protocol.NewID(uuid.New()) serverPort := tcp.PickPort() serverConfig := &core.Config{ @@ -266,7 +270,7 @@ func TestVlessXtlsVision(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, }), }, }, @@ -317,7 +321,7 @@ func TestVlessXtlsVision(t *testing.T) { Vnext: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vless.Account{ Id: userID.String(), Flow: vless.XRV, @@ -337,7 +341,7 @@ func TestVlessXtlsVision(t *testing.T) { SecurityType: serial.GetMessageType(&tls.Config{}), SecuritySettings: []*serial.TypedMessage{ serial.ToTypedMessage(&tls.Config{ - AllowInsecure: true, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, }), }, }, @@ -447,7 +451,7 @@ func TestVlessXtlsVisionReality(t *testing.T) { Vnext: &protocol.ServerEndpoint{ Address: net.NewIPOrDomain(net.LocalHostIP), Port: uint32(serverPort), - User: &protocol.User{ + User: &protocol.User{ Account: serial.ToTypedMessage(&vless.Account{ Id: userID.String(), Flow: vless.XRV, diff --git a/transport/internet/httpupgrade/httpupgrade_test.go b/transport/internet/httpupgrade/httpupgrade_test.go index a8108fe6..c0310b95 100644 --- a/transport/internet/httpupgrade/httpupgrade_test.go +++ b/transport/internet/httpupgrade/httpupgrade_test.go @@ -182,6 +182,8 @@ func Test_listenHTTPUpgradeAndDial_TLS(t *testing.T) { start := time.Now() + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + streamSettings := &internet.MemoryStreamConfig{ ProtocolName: "httpupgrade", ProtocolSettings: &Config{ @@ -189,8 +191,8 @@ func Test_listenHTTPUpgradeAndDial_TLS(t *testing.T) { }, SecurityType: "tls", SecuritySettings: &tls.Config{ - AllowInsecure: true, - Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil, cert.CommonName("localhost")))}, + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, }, } listen, err := ListenHTTPUpgrade(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) { diff --git a/transport/internet/splithttp/splithttp_test.go b/transport/internet/splithttp/splithttp_test.go index c1bb8580..ab02b619 100644 --- a/transport/internet/splithttp/splithttp_test.go +++ b/transport/internet/splithttp/splithttp_test.go @@ -132,6 +132,8 @@ func Test_ListenXHAndDial_TLS(t *testing.T) { start := time.Now() + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + streamSettings := &internet.MemoryStreamConfig{ ProtocolName: "splithttp", ProtocolSettings: &Config{ @@ -139,8 +141,8 @@ func Test_ListenXHAndDial_TLS(t *testing.T) { }, SecurityType: "tls", SecuritySettings: &tls.Config{ - AllowInsecure: true, - Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil, cert.CommonName("localhost")))}, + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, }, } listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) { @@ -228,6 +230,8 @@ func Test_ListenXHAndDial_QUIC(t *testing.T) { start := time.Now() + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + streamSettings := &internet.MemoryStreamConfig{ ProtocolName: "splithttp", ProtocolSettings: &Config{ @@ -235,9 +239,9 @@ func Test_ListenXHAndDial_QUIC(t *testing.T) { }, SecurityType: "tls", SecuritySettings: &tls.Config{ - AllowInsecure: true, - Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil, cert.CommonName("localhost")))}, - NextProtocol: []string{"h3"}, + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, + NextProtocol: []string{"h3"}, }, } diff --git a/transport/internet/tcp/dialer.go b/transport/internet/tcp/dialer.go index 65bacf3a..5b966a00 100644 --- a/transport/internet/tcp/dialer.go +++ b/transport/internet/tcp/dialer.go @@ -35,23 +35,23 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me } isFromMitmVerify := false - if r, ok := tlsConfig.Rand.(*tls.RandCarrier); ok && len(r.VerifyPeerCertInNames) > 0 { - for i, name := range r.VerifyPeerCertInNames { + if r, ok := tlsConfig.Rand.(*tls.RandCarrier); ok && len(r.VerifyPeerCertByName) > 0 { + for i, name := range r.VerifyPeerCertByName { if tls.IsFromMitm(name) { isFromMitmVerify = true - r.VerifyPeerCertInNames[0], r.VerifyPeerCertInNames[i] = r.VerifyPeerCertInNames[i], r.VerifyPeerCertInNames[0] - r.VerifyPeerCertInNames = r.VerifyPeerCertInNames[1:] + r.VerifyPeerCertByName[0], r.VerifyPeerCertByName[i] = r.VerifyPeerCertByName[i], r.VerifyPeerCertByName[0] + r.VerifyPeerCertByName = r.VerifyPeerCertByName[1:] after := mitmServerName for { if len(after) > 0 { - r.VerifyPeerCertInNames = append(r.VerifyPeerCertInNames, after) + r.VerifyPeerCertByName = append(r.VerifyPeerCertByName, after) } _, after, _ = strings.Cut(after, ".") if !strings.Contains(after, ".") { break } } - slices.Reverse(r.VerifyPeerCertInNames) + slices.Reverse(r.VerifyPeerCertByName) break } } diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index a68ad9a5..1b80f439 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -294,7 +294,7 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509 } // directly return success if pinned cert is leaf - // or replace RootCAs if pinned cert is CA (and can be used in VerifyPeerCertInNames) + // or replace RootCAs if pinned cert is CA (and can be used in VerifyPeerCertByName) CAs := r.RootCAs var verifyResult verifyResult var verifiedCert *x509.Certificate @@ -302,7 +302,7 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509 verifyResult, verifiedCert = verifyChain(certs, r.PinnedPeerCertSha256) switch verifyResult { case certNotFound: - return errors.New("peer cert is unrecognized (againsts pinnedPeerCertSha256)") + return errors.New("peer cert is unrecognized (against pinnedPeerCertSha256)") case foundLeaf: return nil case foundCA: @@ -313,7 +313,7 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509 } } - if r.VerifyPeerCertInNames != nil { // RAW's Dial() may make it empty but not nil + if r.VerifyPeerCertByName != nil { // RAW's Dial() may make it empty but not nil opts := x509.VerifyOptions{ Roots: CAs, CurrentTime: time.Now(), @@ -322,15 +322,15 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509 for _, cert := range certs[1:] { opts.Intermediates.AddCert(cert) } - for _, opts.DNSName = range r.VerifyPeerCertInNames { + for _, opts.DNSName = range r.VerifyPeerCertByName { if _, err := certs[0].Verify(opts); err == nil { return nil } } if verifyResult == foundCA { - errors.New("peer cert is invalid (againsts pinned CA and verifyPeerCertInNames)") + errors.New("peer cert is invalid (against pinned CA and verifyPeerCertByName)") } - return errors.New("peer cert is invalid (againsts root CAs and verifyPeerCertInNames)") + return errors.New("peer cert is invalid (against root CAs and verifyPeerCertByName)") } if verifyResult == foundCA { // if found CA, we need to verify here @@ -346,17 +346,17 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509 if _, err := certs[0].Verify(opts); err == nil { return nil } - return errors.New("peer cert is invalid (againsts pinned CA and serverName)") + return errors.New("peer cert is invalid (against pinned CA and serverName)") } - return nil // len(r.PinnedPeerCertSha256)==nil && len(r.VerifyPeerCertInNames)==nil + return nil // r.PinnedPeerCertSha256==nil && r.verifyPeerCertByName==nil } type RandCarrier struct { - Config *tls.Config - RootCAs *x509.CertPool - VerifyPeerCertInNames []string - PinnedPeerCertSha256 [][]byte + Config *tls.Config + RootCAs *x509.CertPool + VerifyPeerCertByName []string + PinnedPeerCertSha256 [][]byte } func (r *RandCarrier) Read(p []byte) (n int, err error) { @@ -374,31 +374,28 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { return &tls.Config{ ClientSessionCache: globalSessionCache, RootCAs: root, - InsecureSkipVerify: false, - NextProtos: nil, SessionTicketsDisabled: true, } } randCarrier := &RandCarrier{ - RootCAs: root, - VerifyPeerCertInNames: slices.Clone(c.VerifyPeerCertInNames), - PinnedPeerCertSha256: c.PinnedPeerCertSha256, + RootCAs: root, + VerifyPeerCertByName: slices.Clone(c.VerifyPeerCertByName), + PinnedPeerCertSha256: c.PinnedPeerCertSha256, } config := &tls.Config{ Rand: randCarrier, ClientSessionCache: globalSessionCache, RootCAs: root, - InsecureSkipVerify: c.AllowInsecure, NextProtos: slices.Clone(c.NextProtocol), SessionTicketsDisabled: !c.EnableSessionResumption, VerifyPeerCertificate: randCarrier.verifyPeerCert, } randCarrier.Config = config - if len(c.VerifyPeerCertInNames) > 0 { + if len(c.VerifyPeerCertByName) > 0 { config.InsecureSkipVerify = true } else { - randCarrier.VerifyPeerCertInNames = nil + randCarrier.VerifyPeerCertByName = nil } if len(c.PinnedPeerCertSha256) > 0 { config.InsecureSkipVerify = true diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index 2abd0c3e..764ad33d 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -181,8 +181,6 @@ type Config struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Whether or not to allow self-signed certificates. - AllowInsecure bool `protobuf:"varint,1,opt,name=allow_insecure,json=allowInsecure,proto3" json:"allow_insecure,omitempty"` // List of certificates to be served on server. Certificate []*Certificate `protobuf:"bytes,2,rep,name=certificate,proto3" json:"certificate,omitempty"` // Override server name. @@ -203,26 +201,15 @@ type Config struct { // TLS Client Hello fingerprint (uTLS). Fingerprint string `protobuf:"bytes,11,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"` RejectUnknownSni bool `protobuf:"varint,12,opt,name=reject_unknown_sni,json=rejectUnknownSni,proto3" json:"reject_unknown_sni,omitempty"` - // @Document Some certificate chain sha256 hashes. - // @Document After normal validation or allow_insecure, if the server's cert chain hash does not match any of these values, the connection will be aborted. - // @Critical - PinnedPeerCertificateChainSha256 [][]byte `protobuf:"bytes,13,rep,name=pinned_peer_certificate_chain_sha256,json=pinnedPeerCertificateChainSha256,proto3" json:"pinned_peer_certificate_chain_sha256,omitempty"` - // @Document Some certificate public key sha256 hashes. - // @Document After normal validation (required), if one of certs in verified chain matches one of these values, the connection will be eventually accepted. - // @Critical - PinnedPeerCertificatePublicKeySha256 [][]byte `protobuf:"bytes,14,rep,name=pinned_peer_certificate_public_key_sha256,json=pinnedPeerCertificatePublicKeySha256,proto3" json:"pinned_peer_certificate_public_key_sha256,omitempty"` - MasterKeyLog string `protobuf:"bytes,15,opt,name=master_key_log,json=masterKeyLog,proto3" json:"master_key_log,omitempty"` + MasterKeyLog string `protobuf:"bytes,15,opt,name=master_key_log,json=masterKeyLog,proto3" json:"master_key_log,omitempty"` // Lists of string as CurvePreferences values. - CurvePreferences []string `protobuf:"bytes,16,rep,name=curve_preferences,json=curvePreferences,proto3" json:"curve_preferences,omitempty"` - // @Document Replaces server_name to verify the peer cert. - // @Document After allow_insecure (automatically), if the server's cert can't be verified by any of these names, pinned_peer_certificate_chain_sha256 will be tried. - // @Critical - VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"` - EchServerKeys []byte `protobuf:"bytes,18,opt,name=ech_server_keys,json=echServerKeys,proto3" json:"ech_server_keys,omitempty"` - EchConfigList string `protobuf:"bytes,19,opt,name=ech_config_list,json=echConfigList,proto3" json:"ech_config_list,omitempty"` - EchForceQuery string `protobuf:"bytes,20,opt,name=ech_force_query,json=echForceQuery,proto3" json:"ech_force_query,omitempty"` - EchSocketSettings *internet.SocketConfig `protobuf:"bytes,21,opt,name=ech_socket_settings,json=echSocketSettings,proto3" json:"ech_socket_settings,omitempty"` - PinnedPeerCertSha256 [][]byte `protobuf:"bytes,22,rep,name=pinned_peer_cert_sha256,json=pinnedPeerCertSha256,proto3" json:"pinned_peer_cert_sha256,omitempty"` + CurvePreferences []string `protobuf:"bytes,16,rep,name=curve_preferences,json=curvePreferences,proto3" json:"curve_preferences,omitempty"` + VerifyPeerCertByName []string `protobuf:"bytes,17,rep,name=verify_peer_cert_by_name,json=verifyPeerCertByName,proto3" json:"verify_peer_cert_by_name,omitempty"` + EchServerKeys []byte `protobuf:"bytes,18,opt,name=ech_server_keys,json=echServerKeys,proto3" json:"ech_server_keys,omitempty"` + EchConfigList string `protobuf:"bytes,19,opt,name=ech_config_list,json=echConfigList,proto3" json:"ech_config_list,omitempty"` + EchForceQuery string `protobuf:"bytes,20,opt,name=ech_force_query,json=echForceQuery,proto3" json:"ech_force_query,omitempty"` + EchSocketSettings *internet.SocketConfig `protobuf:"bytes,21,opt,name=ech_socket_settings,json=echSocketSettings,proto3" json:"ech_socket_settings,omitempty"` + PinnedPeerCertSha256 [][]byte `protobuf:"bytes,22,rep,name=pinned_peer_cert_sha256,json=pinnedPeerCertSha256,proto3" json:"pinned_peer_cert_sha256,omitempty"` } func (x *Config) Reset() { @@ -255,13 +242,6 @@ func (*Config) Descriptor() ([]byte, []int) { return file_transport_internet_tls_config_proto_rawDescGZIP(), []int{1} } -func (x *Config) GetAllowInsecure() bool { - if x != nil { - return x.AllowInsecure - } - return false -} - func (x *Config) GetCertificate() []*Certificate { if x != nil { return x.Certificate @@ -332,20 +312,6 @@ func (x *Config) GetRejectUnknownSni() bool { return false } -func (x *Config) GetPinnedPeerCertificateChainSha256() [][]byte { - if x != nil { - return x.PinnedPeerCertificateChainSha256 - } - return nil -} - -func (x *Config) GetPinnedPeerCertificatePublicKeySha256() [][]byte { - if x != nil { - return x.PinnedPeerCertificatePublicKeySha256 - } - return nil -} - func (x *Config) GetMasterKeyLog() string { if x != nil { return x.MasterKeyLog @@ -360,9 +326,9 @@ func (x *Config) GetCurvePreferences() []string { return nil } -func (x *Config) GetVerifyPeerCertInNames() []string { +func (x *Config) GetVerifyPeerCertByName() []string { if x != nil { - return x.VerifyPeerCertInNames + return x.VerifyPeerCertByName } return nil } @@ -435,81 +401,68 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x45, 0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46, 0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, - 0x59, 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xa0, 0x08, 0x0a, 0x06, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, - 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, - 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, - 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x28, 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, 0x74, 0x6c, 0x73, 0x2e, 0x43, - 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x63, 0x65, 0x72, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6e, 0x65, 0x78, 0x74, - 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x0c, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3a, 0x0a, - 0x19, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, - 0x72, 0x65, 0x73, 0x75, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x17, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x75, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x72, 0x6f, 0x6f, 0x74, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x6f, 0x6f, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x69, 0x6e, - 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, - 0x6d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, - 0x78, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x6d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x63, - 0x69, 0x70, 0x68, 0x65, 0x72, 0x5f, 0x73, 0x75, 0x69, 0x74, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, - 0x12, 0x20, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, - 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, - 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x6e, 0x6b, - 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x6e, 0x69, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, - 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x53, 0x6e, 0x69, - 0x12, 0x4e, 0x0a, 0x24, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, - 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x69, - 0x6e, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x20, - 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, - 0x12, 0x57, 0x0a, 0x29, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, - 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x0e, 0x20, - 0x03, 0x28, 0x0c, 0x52, 0x24, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x43, - 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x4b, 0x65, 0x79, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x73, + 0x59, 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xce, 0x06, 0x0a, 0x06, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 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, 0x74, 0x6c, 0x73, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x52, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3a, 0x0a, 0x19, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6d, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x17, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x11, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x6f, + 0x6f, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x69, 0x6e, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x78, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x5f, 0x73, + 0x75, 0x69, 0x74, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x69, 0x70, + 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x66, 0x69, 0x6e, + 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x72, + 0x65, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x6e, + 0x69, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x55, + 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x53, 0x6e, 0x69, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6c, 0x6f, 0x67, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x4c, 0x6f, 0x67, 0x12, 0x2b, 0x0a, 0x11, 0x63, 0x75, 0x72, 0x76, 0x65, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x63, 0x75, 0x72, 0x76, - 0x65, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x38, 0x0a, 0x19, + 0x65, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x18, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, - 0x5f, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x15, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, - 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x0d, 0x65, 0x63, 0x68, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x26, - 0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x6c, 0x69, 0x73, - 0x74, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x66, 0x6f, - 0x72, 0x63, 0x65, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x65, 0x63, 0x68, 0x46, 0x6f, 0x72, 0x63, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x55, - 0x0a, 0x13, 0x65, 0x63, 0x68, 0x5f, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x73, 0x65, 0x74, - 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 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, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x11, 0x65, 0x63, 0x68, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x53, 0x65, 0x74, - 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x35, 0x0a, 0x17, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x5f, - 0x70, 0x65, 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, - 0x18, 0x16, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x14, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x65, - 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x42, 0x73, 0x0a, 0x1f, - 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, 0x2e, 0x74, 0x6c, 0x73, 0x50, - 0x01, 0x5a, 0x30, 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, 0x2f, - 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, - 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x5f, 0x62, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, + 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x42, 0x79, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x65, + 0x63, 0x68, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x26, 0x0a, 0x0f, + 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, + 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x4c, 0x69, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x66, 0x6f, 0x72, 0x63, + 0x65, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, + 0x63, 0x68, 0x46, 0x6f, 0x72, 0x63, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x55, 0x0a, 0x13, + 0x65, 0x63, 0x68, 0x5f, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 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, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x11, 0x65, 0x63, 0x68, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x53, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x12, 0x35, 0x0a, 0x17, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x65, + 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x16, + 0x20, 0x03, 0x28, 0x0c, 0x52, 0x14, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, + 0x43, 0x65, 0x72, 0x74, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x42, 0x73, 0x0a, 0x1f, 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, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, + 0x30, 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, 0x2f, 0x74, 0x6c, + 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index cb7a225e..225206b2 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -38,9 +38,6 @@ message Certificate { } message Config { - // Whether or not to allow self-signed certificates. - bool allow_insecure = 1; - // List of certificates to be served on server. repeated Certificate certificate = 2; @@ -70,29 +67,13 @@ message Config { string fingerprint = 11; bool reject_unknown_sni = 12; - - /* @Document Some certificate chain sha256 hashes. - @Document After normal validation or allow_insecure, if the server's cert chain hash does not match any of these values, the connection will be aborted. - @Critical - */ - repeated bytes pinned_peer_certificate_chain_sha256 = 13; - - /* @Document Some certificate public key sha256 hashes. - @Document After normal validation (required), if one of certs in verified chain matches one of these values, the connection will be eventually accepted. - @Critical - */ - repeated bytes pinned_peer_certificate_public_key_sha256 = 14; string master_key_log = 15; // Lists of string as CurvePreferences values. repeated string curve_preferences = 16; - /* @Document Replaces server_name to verify the peer cert. - @Document After allow_insecure (automatically), if the server's cert can't be verified by any of these names, pinned_peer_certificate_chain_sha256 will be tried. - @Critical - */ - repeated string verify_peer_cert_in_names = 17; + repeated string verify_peer_cert_by_name = 17; bytes ech_server_keys = 18; diff --git a/transport/internet/tls/config_test.go b/transport/internet/tls/config_test.go index bc23add3..7265ed27 100644 --- a/transport/internet/tls/config_test.go +++ b/transport/internet/tls/config_test.go @@ -12,7 +12,8 @@ import ( ) func TestCertificateIssuing(t *testing.T) { - certificate := ParseCertificate(cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign))) + ct, _ := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign)) + certificate := ParseCertificate(ct) certificate.Usage = Certificate_AUTHORITY_ISSUE c := &Config{ @@ -35,8 +36,8 @@ func TestCertificateIssuing(t *testing.T) { } func TestExpiredCertificate(t *testing.T) { - caCert := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign)) - expiredCert := cert.MustGenerate(caCert, cert.NotAfter(time.Now().Add(time.Minute*-2)), cert.CommonName("www.example.com"), cert.DNSNames("www.example.com")) + caCert, _ := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign)) + expiredCert, _ := cert.MustGenerate(caCert, cert.NotAfter(time.Now().Add(time.Minute*-2)), cert.CommonName("www.example.com"), cert.DNSNames("www.example.com")) certificate := ParseCertificate(caCert) certificate.Usage = Certificate_AUTHORITY_ISSUE @@ -73,7 +74,8 @@ func TestInsecureCertificates(t *testing.T) { } func BenchmarkCertificateIssuing(b *testing.B) { - certificate := ParseCertificate(cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign))) + ct, _ := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign)) + certificate := ParseCertificate(ct) certificate.Usage = Certificate_AUTHORITY_ISSUE c := &Config{ diff --git a/transport/internet/tls/pin_test.go b/transport/internet/tls/pin_test.go index a13b12da..50568df6 100644 --- a/transport/internet/tls/pin_test.go +++ b/transport/internet/tls/pin_test.go @@ -100,16 +100,14 @@ uI6HqHFD3iEct8fBkYfQiwH2e1eu9OwgujiWHsutyK8VvzVB3/YnhQ/TzciRjPqz } func TestVerifyPeerLeafCert(t *testing.T) { - leafCert := cert.MustGenerate(nil, cert.DNSNames("example.com")) + leafCert, leafHash := cert.MustGenerate(nil, cert.DNSNames("example.com")) leaf := common.Must2(x509.ParseCertificate(leafCert.Certificate)) - caHash := GenerateCertHash(leafCert.Certificate) - r := &RandCarrier{ Config: &tls.Config{ ServerName: "example.com", }, - PinnedPeerCertSha256: [][]byte{caHash}, + PinnedPeerCertSha256: [][]byte{leafHash[:]}, } rawCerts := [][]byte{leaf.Raw} @@ -127,19 +125,17 @@ func TestVerifyPeerLeafCert(t *testing.T) { } func TestVerifyPeerCACert(t *testing.T) { - caCert := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign)) + caCert, caHash := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign)) ca := common.Must2(x509.ParseCertificate(caCert.Certificate)) - leafCert := cert.MustGenerate(caCert, cert.DNSNames("example.com")) + leafCert, _ := cert.MustGenerate(caCert, cert.DNSNames("example.com")) leaf := common.Must2(x509.ParseCertificate(leafCert.Certificate)) - caHash := GenerateCertHash(ca) - r := &RandCarrier{ Config: &tls.Config{ ServerName: "example.com", }, - PinnedPeerCertSha256: [][]byte{caHash}, + PinnedPeerCertSha256: [][]byte{caHash[:]}, } rawCerts := [][]byte{leaf.Raw, ca.Raw} diff --git a/transport/internet/websocket/ws_test.go b/transport/internet/websocket/ws_test.go index a9a2b885..aab28af6 100644 --- a/transport/internet/websocket/ws_test.go +++ b/transport/internet/websocket/ws_test.go @@ -123,6 +123,8 @@ func Test_listenWSAndDial_TLS(t *testing.T) { start := time.Now() + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + streamSettings := &internet.MemoryStreamConfig{ ProtocolName: "websocket", ProtocolSettings: &Config{ @@ -130,8 +132,8 @@ func Test_listenWSAndDial_TLS(t *testing.T) { }, SecurityType: "tls", SecuritySettings: &tls.Config{ - AllowInsecure: true, - Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil, cert.CommonName("localhost")))}, + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, }, } listen, err := ListenWS(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) {