From c78198cede20f6532dd88326932315d655ec28b8 Mon Sep 17 00:00:00 2001 From: seally Date: Thu, 25 Jun 2026 02:43:46 +0500 Subject: [PATCH 1/7] Hysteria: support VLESS route auth IDs --- proxy/hysteria/account/config.go | 44 +++++++- proxy/hysteria/account/config_test.go | 28 +++++ proxy/hysteria/server.go | 11 ++ proxy/hysteria/server_test.go | 147 ++++++++++++++++++++++++++ transport/internet/hysteria/conn.go | 12 +++ transport/internet/hysteria/hub.go | 8 +- 6 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 proxy/hysteria/account/config_test.go create mode 100644 proxy/hysteria/server_test.go diff --git a/proxy/hysteria/account/config.go b/proxy/hysteria/account/config.go index 63ecaf652956..49d2ac07f56f 100644 --- a/proxy/hysteria/account/config.go +++ b/proxy/hysteria/account/config.go @@ -4,7 +4,9 @@ import ( "sync" "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/uuid" "google.golang.org/protobuf/proto" ) @@ -39,6 +41,32 @@ type Validator struct { mutex sync.Mutex } +func vlessRouteAuthKey(auth string) ([16]byte, bool) { + var key [16]byte + if len(auth) != 32 && len(auth) != 36 { + return key, false + } + id, err := uuid.ParseString(auth) + if err != nil { + return key, false + } + copy(key[:], id.Bytes()) + key[6] = 0 + key[7] = 0 + return key, true +} + +func VlessRouteFromAuth(auth string) net.Port { + if len(auth) != 32 && len(auth) != 36 { + return 0 + } + id, err := uuid.ParseString(auth) + if err != nil { + return 0 + } + return net.PortFromBytes(id.Bytes()[6:8]) +} + func NewValidator() *Validator { return &Validator{ emails: make(map[string]struct{}), @@ -87,7 +115,21 @@ func (v *Validator) Get(auth string) *protocol.MemoryUser { v.mutex.Lock() defer v.mutex.Unlock() - return v.users[auth] + if user := v.users[auth]; user != nil { + return user + } + + key, ok := vlessRouteAuthKey(auth) + if !ok { + return nil + } + for storedAuth, user := range v.users { + storedKey, ok := vlessRouteAuthKey(storedAuth) + if ok && storedKey == key { + return user + } + } + return nil } func (v *Validator) GetByEmail(email string) *protocol.MemoryUser { diff --git a/proxy/hysteria/account/config_test.go b/proxy/hysteria/account/config_test.go new file mode 100644 index 000000000000..f547504ed48c --- /dev/null +++ b/proxy/hysteria/account/config_test.go @@ -0,0 +1,28 @@ +package account + +import ( + "testing" + + "github.com/xtls/xray-core/common/protocol" +) + +func TestValidatorGetAcceptsVlessRouteAuthVariant(t *testing.T) { + user := &protocol.MemoryUser{ + Email: "user@example.com", + Account: &MemoryAccount{Auth: "00000000-0000-1234-8000-000000000000"}, + } + validator := NewValidator() + if err := validator.Add(user); err != nil { + t.Fatal(err) + } + + if got := validator.Get("00000000-0000-0001-8000-000000000000"); got != user { + t.Fatal("validator did not accept UUID auth variant with a different VLESS route") + } + if got := validator.Get("00000000-0000-0001-9000-000000000000"); got != nil { + t.Fatalf("validator accepted auth with non-route bytes changed: %v", got) + } + if got := validator.Get("password"); got != nil { + t.Fatalf("validator accepted unrelated password auth: %v", got) + } +} diff --git a/proxy/hysteria/server.go b/proxy/hysteria/server.go index d7456dc304b8..1b480dc9a50a 100644 --- a/proxy/hysteria/server.go +++ b/proxy/hysteria/server.go @@ -92,8 +92,19 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con iConn := stat.TryUnwrapStatsConn(conn) type User interface{ User() *protocol.MemoryUser } + type Auth interface{ Auth() string } if v, ok := iConn.(User); ok && v.User() != nil { inbound.User = v.User() + auth := "" + if v, ok := iConn.(Auth); ok { + auth = v.Auth() + } + if hysteriaAccount, ok := inbound.User.Account.(*account.MemoryAccount); ok { + if auth == "" { + auth = hysteriaAccount.Auth + } + inbound.VlessRoute = account.VlessRouteFromAuth(auth) + } } if _, ok := iConn.(*hysteria.InterConn); ok { diff --git a/proxy/hysteria/server_test.go b/proxy/hysteria/server_test.go new file mode 100644 index 000000000000..b95a3b9103b6 --- /dev/null +++ b/proxy/hysteria/server_test.go @@ -0,0 +1,147 @@ +package hysteria + +import ( + "context" + "io" + stdnet "net" + "testing" + "time" + + "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/features/policy" + "github.com/xtls/xray-core/proxy/hysteria/account" +) + +func TestVlessRouteFromAuth(t *testing.T) { + const routeID = net.Port(0x1234) + + tests := []struct { + name string + auth string + want net.Port + }{ + { + name: "hyphenated UUID", + auth: "00000000-0000-1234-8000-000000000000", + want: routeID, + }, + { + name: "plain UUID", + auth: "00000000000012348000000000000000", + want: routeID, + }, + { + name: "password auth", + auth: "password", + want: 0, + }, + { + name: "invalid UUID", + auth: "00000000-0000-123k-8000-000000000000", + want: 0, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := account.VlessRouteFromAuth(test.auth); got != test.want { + t.Fatalf("VlessRouteFromAuth(%q) = %d, want %d", test.auth, got, test.want) + } + }) + } +} + +type testPolicyManager struct{} + +func (*testPolicyManager) Type() interface{} { + return policy.ManagerType() +} + +func (*testPolicyManager) Start() error { + return nil +} + +func (*testPolicyManager) Close() error { + return nil +} + +func (*testPolicyManager) ForLevel(uint32) policy.Session { + return policy.SessionDefault() +} + +func (*testPolicyManager) ForSystem() policy.System { + return policy.System{} +} + +type testUserConn struct { + user *protocol.MemoryUser + auth string +} + +func (c *testUserConn) User() *protocol.MemoryUser { + return c.user +} + +func (c *testUserConn) Auth() string { + return c.auth +} + +func (*testUserConn) Read([]byte) (int, error) { + return 0, io.EOF +} + +func (*testUserConn) Write(b []byte) (int, error) { + return len(b), nil +} + +func (*testUserConn) Close() error { + return nil +} + +func (*testUserConn) LocalAddr() stdnet.Addr { + return &stdnet.TCPAddr{IP: stdnet.IPv4(127, 0, 0, 1), Port: 443} +} + +func (*testUserConn) RemoteAddr() stdnet.Addr { + return &stdnet.TCPAddr{IP: stdnet.IPv4(127, 0, 0, 1), Port: 12345} +} + +func (*testUserConn) SetDeadline(time.Time) error { + return nil +} + +func (*testUserConn) SetReadDeadline(time.Time) error { + return nil +} + +func (*testUserConn) SetWriteDeadline(time.Time) error { + return nil +} + +func TestServerProcessSetsVlessRouteFromHysteriaAuth(t *testing.T) { + const serverAuth = "00000000-0000-1234-8000-000000000000" + const clientAuth = "00000000-0000-0001-8000-000000000000" + + inbound := &session.Inbound{} + user := &protocol.MemoryUser{ + Account: &account.MemoryAccount{Auth: serverAuth}, + Level: 1, + Email: "user@example.com", + } + server := &Server{ + policyManager: &testPolicyManager{}, + } + + err := server.Process(session.ContextWithInbound(context.Background(), inbound), net.Network_TCP, &testUserConn{user: user, auth: clientAuth}, nil) + if err == nil { + t.Fatal("Process unexpectedly succeeded with an empty test connection") + } + if inbound.User != user { + t.Fatal("server did not use the authenticated Hysteria user from the connection") + } + if inbound.VlessRoute != 1 { + t.Fatalf("inbound.VlessRoute = %d, want %d", inbound.VlessRoute, net.Port(1)) + } +} diff --git a/transport/internet/hysteria/conn.go b/transport/internet/hysteria/conn.go index ce2a4af31cf7..f2df168170b8 100644 --- a/transport/internet/hysteria/conn.go +++ b/transport/internet/hysteria/conn.go @@ -22,12 +22,17 @@ type interConn struct { client bool user *protocol.MemoryUser + auth string } func (c *interConn) User() *protocol.MemoryUser { return c.user } +func (c *interConn) Auth() string { + return c.auth +} + func (c *interConn) Read(b []byte) (int, error) { return c.stream.Read(b) } @@ -82,12 +87,17 @@ type InterConn struct { write func(p []byte) error close func() user *protocol.MemoryUser + auth string } func (i *InterConn) User() *protocol.MemoryUser { return i.user } +func (i *InterConn) Auth() string { + return i.auth +} + func (c *InterConn) Time() time.Time { c.mutex.Lock() v := c.time @@ -161,6 +171,7 @@ type udpSessionManager struct { addConn internet.ConnHandler udpIdleTimeout time.Duration user *protocol.MemoryUser + auth string } func (m *udpSessionManager) close(udpConn *InterConn) { @@ -287,6 +298,7 @@ func (m *udpSessionManager) feed(id uint32, d []byte) { m.Unlock() } udpConn.user = m.user + udpConn.auth = m.auth m.m[id] = udpConn m.addConn(udpConn) } diff --git a/transport/internet/hysteria/hub.go b/transport/internet/hysteria/hub.go index d20313b5d031..38ad2161ac11 100644 --- a/transport/internet/hysteria/hub.go +++ b/transport/internet/hysteria/hub.go @@ -36,8 +36,9 @@ type httpHandler struct { addConn internet.ConnHandler conn *quic.Conn - auth bool - user *protocol.MemoryUser + auth bool + user *protocol.MemoryUser + authValue string } func (h *httpHandler) AuthHTTP(w http.ResponseWriter, r *http.Request) bool { @@ -67,6 +68,7 @@ func (h *httpHandler) AuthHTTP(w http.ResponseWriter, r *http.Request) bool { if user != nil || ok { h.auth = true h.user = user + h.authValue = auth conn := h.conn quicParams := h.quicParams @@ -94,6 +96,7 @@ func (h *httpHandler) AuthHTTP(w http.ResponseWriter, r *http.Request) bool { addConn: h.addConn, udpIdleTimeout: time.Duration(h.config.UdpIdleTimeout) * time.Second, user: h.user, + auth: h.authValue, } go udpSM.clean() go udpSM.run() @@ -133,6 +136,7 @@ func (h *httpHandler) StreamDispatcher(ft http3.FrameType, stream *quic.Stream, remote: h.conn.RemoteAddr(), user: h.user, + auth: h.authValue, }) return true, nil default: From baccbd57af72839743e9b1fb53dbc414e364cb77 Mon Sep 17 00:00:00 2001 From: seally Date: Thu, 25 Jun 2026 05:14:37 +0500 Subject: [PATCH 2/7] Router: add Hysteria route matcher --- app/router/command/command.pb.go | 13 ++++++-- app/router/command/command.proto | 1 + app/router/command/config.go | 4 +++ app/router/condition.go | 3 ++ app/router/condition_test.go | 34 +++++++++++++++++++ app/router/config.go | 4 +++ app/router/config.pb.go | 48 ++++++++++++++++----------- app/router/config.proto | 1 + common/session/session.go | 2 ++ features/routing/context.go | 3 ++ features/routing/session/context.go | 8 +++++ infra/conf/router.go | 39 ++++++++++++---------- infra/conf/router_test.go | 30 +++++++++++++++++ proxy/hysteria/account/config.go | 8 ++--- proxy/hysteria/account/config_test.go | 4 +-- proxy/hysteria/server.go | 2 +- proxy/hysteria/server_test.go | 15 +++++---- 17 files changed, 168 insertions(+), 51 deletions(-) diff --git a/app/router/command/command.pb.go b/app/router/command/command.pb.go index db55b17e9700..9a387268d237 100644 --- a/app/router/command/command.pb.go +++ b/app/router/command/command.pb.go @@ -43,6 +43,7 @@ type RoutingContext struct { 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"` + HysteriaRoute uint32 `protobuf:"varint,16,opt,name=HysteriaRoute,proto3" json:"HysteriaRoute,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -182,6 +183,13 @@ func (x *RoutingContext) GetVlessRoute() uint32 { return 0 } +func (x *RoutingContext) GetHysteriaRoute() uint32 { + if x != nil { + return x.HysteriaRoute + } + return 0 +} + // SubscribeRoutingStatsRequest subscribes to routing statistics channel if // opened by xray-core. // * FieldSelectors selects a subset of fields in routing statistics to return. @@ -967,7 +975,7 @@ var File_app_router_command_command_proto protoreflect.FileDescriptor 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" + + " app/router/command/command.proto\x12\x17xray.app.router.command\x1a\x18common/net/network.proto\x1a!common/serial/typed_message.proto\"\x9c\x05\n" + "\x0eRoutingContext\x12\x1e\n" + "\n" + "InboundTag\x18\x01 \x01(\tR\n" + @@ -994,7 +1002,8 @@ const file_app_router_command_command_proto_rawDesc = "" + "\tLocalPort\x18\x0e \x01(\rR\tLocalPort\x12\x1e\n" + "\n" + "VlessRoute\x18\x0f \x01(\rR\n" + - "VlessRoute\x1a=\n" + + "VlessRoute\x12$\n" + + "\rHysteriaRoute\x18\x10 \x01(\rR\rHysteriaRoute\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" + diff --git a/app/router/command/command.proto b/app/router/command/command.proto index 22ae95b98440..9a57aeb1800e 100644 --- a/app/router/command/command.proto +++ b/app/router/command/command.proto @@ -28,6 +28,7 @@ message RoutingContext { repeated bytes LocalIPs = 13; uint32 LocalPort = 14; uint32 VlessRoute = 15; + uint32 HysteriaRoute = 16; } // SubscribeRoutingStatsRequest subscribes to routing statistics channel if diff --git a/app/router/command/config.go b/app/router/command/config.go index 13a319b15528..b1d76dc8f814 100644 --- a/app/router/command/config.go +++ b/app/router/command/config.go @@ -40,6 +40,10 @@ func (c routingContext) GetVlessRoute() net.Port { return net.Port(c.RoutingContext.GetVlessRoute()) } +func (c routingContext) GetHysteriaRoute() net.Port { + return net.Port(c.RoutingContext.GetHysteriaRoute()) +} + func (c routingContext) GetRuleTag() string { return "" } diff --git a/app/router/condition.go b/app/router/condition.go index b8a482593234..b6071115aab8 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -75,6 +75,7 @@ const ( MatcherAsType_Source MatcherAsType_Target MatcherAsType_VlessRoute // for port + MatcherAsType_HysteriaRoute ) type IPMatcher struct { @@ -132,6 +133,8 @@ func (v *PortMatcher) Apply(ctx routing.Context) bool { return v.port.Contains(ctx.GetTargetPort()) case MatcherAsType_VlessRoute: return v.port.Contains(ctx.GetVlessRoute()) + case MatcherAsType_HysteriaRoute: + return v.port.Contains(ctx.GetHysteriaRoute()) default: panic("unk asType") } diff --git a/app/router/condition_test.go b/app/router/condition_test.go index 1b94bb8efe1a..67233f63f43b 100644 --- a/app/router/condition_test.go +++ b/app/router/condition_test.go @@ -187,6 +187,40 @@ func TestRoutingRule(t *testing.T) { }, }, }, + { + rule: &RoutingRule{ + VlessRouteList: &net.PortList{ + Range: []*net.PortRange{{From: 1, To: 1}}, + }, + }, + test: []ruleTest{ + { + input: withInbound(&session.Inbound{VlessRoute: 1}), + output: true, + }, + { + input: withInbound(&session.Inbound{HysteriaRoute: 1}), + output: false, + }, + }, + }, + { + rule: &RoutingRule{ + HysteriaRouteList: &net.PortList{ + Range: []*net.PortRange{{From: 1, To: 1}}, + }, + }, + test: []ruleTest{ + { + input: withInbound(&session.Inbound{HysteriaRoute: 1}), + output: true, + }, + { + input: withInbound(&session.Inbound{VlessRoute: 1}), + output: false, + }, + }, + }, { rule: &RoutingRule{ InboundTag: []string{"test", "test1"}, diff --git a/app/router/config.go b/app/router/config.go index 0a76905c10d6..5160f5f8d98b 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -61,6 +61,10 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { conds.Add(NewPortMatcher(rr.VlessRouteList, MatcherAsType_VlessRoute)) } + if rr.HysteriaRouteList != nil { + conds.Add(NewPortMatcher(rr.HysteriaRouteList, MatcherAsType_HysteriaRoute)) + } + if len(rr.UserEmail) > 0 { conds.Add(NewUserMatcher(rr.UserEmail)) } diff --git a/app/router/config.pb.go b/app/router/config.pb.go index 6c1e27504573..be949c7815fa 100644 --- a/app/router/config.pb.go +++ b/app/router/config.pb.go @@ -103,12 +103,13 @@ type RoutingRule struct { // List of IPs for local IP address matching. LocalIp []*geodata.IPRule `protobuf:"bytes,17,rep,name=local_ip,json=localIp,proto3" json:"local_ip,omitempty"` // List of ports for local port matching. - 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"` - Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"` - Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + 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"` + Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"` + Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"` + HysteriaRouteList *net.PortList `protobuf:"bytes,23,opt,name=hysteria_route_list,json=hysteriaRouteList,proto3" json:"hysteria_route_list,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RoutingRule) Reset() { @@ -278,6 +279,13 @@ func (x *RoutingRule) GetWebhook() *WebhookConfig { return nil } +func (x *RoutingRule) GetHysteriaRouteList() *net.PortList { + if x != nil { + return x.HysteriaRouteList + } + return nil +} + type isRoutingRule_TargetTag interface { isRoutingRule_TargetTag() } @@ -637,7 +645,7 @@ var File_app_router_config_proto protoreflect.FileDescriptor const file_app_router_config_proto_rawDesc = "" + "\n" + - "\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"\xc1\a\n" + + "\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"\x8c\b\n" + "\vRoutingRule\x12\x12\n" + "\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" + "\rbalancing_tag\x18\f \x01(\tH\x00R\fbalancingTag\x12\x19\n" + @@ -661,7 +669,8 @@ const file_app_router_config_proto_rawDesc = "" + "\x0flocal_port_list\x18\x12 \x01(\v2\x19.xray.common.net.PortListR\rlocalPortList\x12C\n" + "\x10vless_route_list\x18\x14 \x01(\v2\x19.xray.common.net.PortListR\x0evlessRouteList\x12\x18\n" + "\aprocess\x18\x15 \x03(\tR\aprocess\x128\n" + - "\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x1a=\n" + + "\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x12I\n" + + "\x13hysteria_route_list\x18\x17 \x01(\v2\x19.xray.common.net.PortListR\x11hysteriaRouteList\x1a=\n" + "\x0fAttributesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\f\n" + @@ -743,17 +752,18 @@ var file_app_router_config_proto_depIdxs = []int32{ 11, // 8: xray.app.router.RoutingRule.local_port_list:type_name -> xray.common.net.PortList 11, // 9: xray.app.router.RoutingRule.vless_route_list:type_name -> xray.common.net.PortList 2, // 10: xray.app.router.RoutingRule.webhook:type_name -> xray.app.router.WebhookConfig - 8, // 11: xray.app.router.WebhookConfig.headers:type_name -> xray.app.router.WebhookConfig.HeadersEntry - 13, // 12: xray.app.router.BalancingRule.strategy_settings:type_name -> xray.common.serial.TypedMessage - 4, // 13: xray.app.router.StrategyLeastLoadConfig.costs:type_name -> xray.app.router.StrategyWeight - 0, // 14: xray.app.router.Config.domain_strategy:type_name -> xray.app.router.Config.DomainStrategy - 1, // 15: xray.app.router.Config.rule:type_name -> xray.app.router.RoutingRule - 3, // 16: xray.app.router.Config.balancing_rule:type_name -> xray.app.router.BalancingRule - 17, // [17:17] is the sub-list for method output_type - 17, // [17:17] is the sub-list for method input_type - 17, // [17:17] is the sub-list for extension type_name - 17, // [17:17] is the sub-list for extension extendee - 0, // [0:17] is the sub-list for field type_name + 11, // 11: xray.app.router.RoutingRule.hysteria_route_list:type_name -> xray.common.net.PortList + 8, // 12: xray.app.router.WebhookConfig.headers:type_name -> xray.app.router.WebhookConfig.HeadersEntry + 13, // 13: xray.app.router.BalancingRule.strategy_settings:type_name -> xray.common.serial.TypedMessage + 4, // 14: xray.app.router.StrategyLeastLoadConfig.costs:type_name -> xray.app.router.StrategyWeight + 0, // 15: xray.app.router.Config.domain_strategy:type_name -> xray.app.router.Config.DomainStrategy + 1, // 16: xray.app.router.Config.rule:type_name -> xray.app.router.RoutingRule + 3, // 17: xray.app.router.Config.balancing_rule:type_name -> xray.app.router.BalancingRule + 18, // [18:18] is the sub-list for method output_type + 18, // [18:18] is the sub-list for method input_type + 18, // [18:18] is the sub-list for extension type_name + 18, // [18:18] is the sub-list for extension extendee + 0, // [0:18] is the sub-list for field type_name } func init() { file_app_router_config_proto_init() } diff --git a/app/router/config.proto b/app/router/config.proto index 60f565c362aa..4866a3e687a4 100644 --- a/app/router/config.proto +++ b/app/router/config.proto @@ -56,6 +56,7 @@ message RoutingRule { repeated string process = 21; WebhookConfig webhook = 22; + xray.common.net.PortList hysteria_route_list = 23; } message WebhookConfig { diff --git a/common/session/session.go b/common/session/session.go index b5f163ff172e..2902242a0e87 100644 --- a/common/session/session.go +++ b/common/session/session.go @@ -49,6 +49,8 @@ type Inbound struct { User *protocol.MemoryUser // VlessRoute is the user-sent VLESS UUID's 7th<<8 | 8th bytes. VlessRoute net.Port + // HysteriaRoute is the user-sent Hysteria UUID auth's 7th<<8 | 8th bytes. + HysteriaRoute net.Port // Used by splice copy. Conn is actually internet.Connection. May be nil. Conn net.Conn // Used by splice copy. Timer of the inbound buf copier. May be nil. diff --git a/features/routing/context.go b/features/routing/context.go index 6b38d175c2d8..09bc7974c3a9 100644 --- a/features/routing/context.go +++ b/features/routing/context.go @@ -44,6 +44,9 @@ type Context interface { // GetVlessRoute returns the user-sent VLESS UUID's 7th<<8 | 8th bytes, if exists. GetVlessRoute() net.Port + // GetHysteriaRoute returns the user-sent Hysteria UUID auth's 7th<<8 | 8th bytes, if exists. + GetHysteriaRoute() net.Port + // GetAttributes returns extra attributes from the conneciont content. GetAttributes() map[string]string diff --git a/features/routing/session/context.go b/features/routing/session/context.go index e331c4347213..4e4268c2466a 100644 --- a/features/routing/session/context.go +++ b/features/routing/session/context.go @@ -135,6 +135,14 @@ func (ctx *Context) GetVlessRoute() net.Port { return ctx.Inbound.VlessRoute } +// GetHysteriaRoute implements routing.Context. +func (ctx *Context) GetHysteriaRoute() net.Port { + if ctx.Inbound == nil { + return 0 + } + return ctx.Inbound.HysteriaRoute +} + // GetAttributes implements routing.Context. func (ctx *Context) GetAttributes() map[string]string { if ctx.Content == nil { diff --git a/infra/conf/router.go b/infra/conf/router.go index af83c743597c..cb71dbd2dfe5 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -132,23 +132,24 @@ type WebhookRuleConfig struct { 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"` - Process *StringList `json:"process"` - Webhook *WebhookRuleConfig `json:"webhook"` + 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"` + HysteriaRoute *PortList `json:"hysteriaRoute"` + 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"` + Webhook *WebhookRuleConfig `json:"webhook"` } rawFieldRule := new(RawFieldRule) err := json.Unmarshal(msg, rawFieldRule) @@ -241,6 +242,10 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) { rule.VlessRouteList = rawFieldRule.VlessRoute.Build() } + if rawFieldRule.HysteriaRoute != nil { + rule.HysteriaRouteList = rawFieldRule.HysteriaRoute.Build() + } + if rawFieldRule.InboundTag != nil { for _, s := range *rawFieldRule.InboundTag { rule.InboundTag = append(rule.InboundTag, s) diff --git a/infra/conf/router_test.go b/infra/conf/router_test.go index 130cf4f7857a..78688982e341 100644 --- a/infra/conf/router_test.go +++ b/infra/conf/router_test.go @@ -236,3 +236,33 @@ func TestRouterConfig(t *testing.T) { }, }) } + +func TestRouterConfigHysteriaRoute(t *testing.T) { + config := new(RouterConfig) + err := json.Unmarshal([]byte(`{ + "rules": [ + { + "hysteriaRoute": "1-2", + "outboundTag": "hysteria" + }, + { + "vlessRoute": "3", + "outboundTag": "vless" + } + ] + }`), config) + if err != nil { + t.Fatal(err) + } + + built, err := config.Build() + if err != nil { + t.Fatal(err) + } + if got := built.Rule[0].GetHysteriaRouteList().Range[0]; got.From != 1 || got.To != 2 { + t.Fatalf("hysteriaRoute = %d-%d, want 1-2", got.From, got.To) + } + if got := built.Rule[1].GetVlessRouteList().Range[0]; got.From != 3 || got.To != 3 { + t.Fatalf("vlessRoute = %d-%d, want 3-3", got.From, got.To) + } +} diff --git a/proxy/hysteria/account/config.go b/proxy/hysteria/account/config.go index 49d2ac07f56f..a4b85e0e95ba 100644 --- a/proxy/hysteria/account/config.go +++ b/proxy/hysteria/account/config.go @@ -41,7 +41,7 @@ type Validator struct { mutex sync.Mutex } -func vlessRouteAuthKey(auth string) ([16]byte, bool) { +func hysteriaRouteAuthKey(auth string) ([16]byte, bool) { var key [16]byte if len(auth) != 32 && len(auth) != 36 { return key, false @@ -56,7 +56,7 @@ func vlessRouteAuthKey(auth string) ([16]byte, bool) { return key, true } -func VlessRouteFromAuth(auth string) net.Port { +func HysteriaRouteFromAuth(auth string) net.Port { if len(auth) != 32 && len(auth) != 36 { return 0 } @@ -119,12 +119,12 @@ func (v *Validator) Get(auth string) *protocol.MemoryUser { return user } - key, ok := vlessRouteAuthKey(auth) + key, ok := hysteriaRouteAuthKey(auth) if !ok { return nil } for storedAuth, user := range v.users { - storedKey, ok := vlessRouteAuthKey(storedAuth) + storedKey, ok := hysteriaRouteAuthKey(storedAuth) if ok && storedKey == key { return user } diff --git a/proxy/hysteria/account/config_test.go b/proxy/hysteria/account/config_test.go index f547504ed48c..a99c3a09fe7a 100644 --- a/proxy/hysteria/account/config_test.go +++ b/proxy/hysteria/account/config_test.go @@ -6,7 +6,7 @@ import ( "github.com/xtls/xray-core/common/protocol" ) -func TestValidatorGetAcceptsVlessRouteAuthVariant(t *testing.T) { +func TestValidatorGetAcceptsHysteriaRouteAuthVariant(t *testing.T) { user := &protocol.MemoryUser{ Email: "user@example.com", Account: &MemoryAccount{Auth: "00000000-0000-1234-8000-000000000000"}, @@ -17,7 +17,7 @@ func TestValidatorGetAcceptsVlessRouteAuthVariant(t *testing.T) { } if got := validator.Get("00000000-0000-0001-8000-000000000000"); got != user { - t.Fatal("validator did not accept UUID auth variant with a different VLESS route") + t.Fatal("validator did not accept UUID auth variant with a different Hysteria route") } if got := validator.Get("00000000-0000-0001-9000-000000000000"); got != nil { t.Fatalf("validator accepted auth with non-route bytes changed: %v", got) diff --git a/proxy/hysteria/server.go b/proxy/hysteria/server.go index 1b480dc9a50a..f150c3cc28a5 100644 --- a/proxy/hysteria/server.go +++ b/proxy/hysteria/server.go @@ -103,7 +103,7 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con if auth == "" { auth = hysteriaAccount.Auth } - inbound.VlessRoute = account.VlessRouteFromAuth(auth) + inbound.HysteriaRoute = account.HysteriaRouteFromAuth(auth) } } diff --git a/proxy/hysteria/server_test.go b/proxy/hysteria/server_test.go index b95a3b9103b6..810c962176cd 100644 --- a/proxy/hysteria/server_test.go +++ b/proxy/hysteria/server_test.go @@ -14,7 +14,7 @@ import ( "github.com/xtls/xray-core/proxy/hysteria/account" ) -func TestVlessRouteFromAuth(t *testing.T) { +func TestHysteriaRouteFromAuth(t *testing.T) { const routeID = net.Port(0x1234) tests := []struct { @@ -46,8 +46,8 @@ func TestVlessRouteFromAuth(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - if got := account.VlessRouteFromAuth(test.auth); got != test.want { - t.Fatalf("VlessRouteFromAuth(%q) = %d, want %d", test.auth, got, test.want) + if got := account.HysteriaRouteFromAuth(test.auth); got != test.want { + t.Fatalf("HysteriaRouteFromAuth(%q) = %d, want %d", test.auth, got, test.want) } }) } @@ -120,7 +120,7 @@ func (*testUserConn) SetWriteDeadline(time.Time) error { return nil } -func TestServerProcessSetsVlessRouteFromHysteriaAuth(t *testing.T) { +func TestServerProcessSetsHysteriaRouteFromAuth(t *testing.T) { const serverAuth = "00000000-0000-1234-8000-000000000000" const clientAuth = "00000000-0000-0001-8000-000000000000" @@ -141,7 +141,10 @@ func TestServerProcessSetsVlessRouteFromHysteriaAuth(t *testing.T) { if inbound.User != user { t.Fatal("server did not use the authenticated Hysteria user from the connection") } - if inbound.VlessRoute != 1 { - t.Fatalf("inbound.VlessRoute = %d, want %d", inbound.VlessRoute, net.Port(1)) + if inbound.HysteriaRoute != 1 { + t.Fatalf("inbound.HysteriaRoute = %d, want %d", inbound.HysteriaRoute, net.Port(1)) + } + if inbound.VlessRoute != 0 { + t.Fatalf("inbound.VlessRoute = %d, want 0", inbound.VlessRoute) } } From eb25d280d85c96e1947ae5774b9c5ef20e802d3e Mon Sep 17 00:00:00 2001 From: seally Date: Thu, 25 Jun 2026 18:16:31 +0500 Subject: [PATCH 3/7] Hysteria: keep route auth in account --- proxy/hysteria/account/config.go | 8 +++++++- proxy/hysteria/account/config_test.go | 21 +++++++++++++++++++-- proxy/hysteria/server.go | 10 +--------- proxy/hysteria/server_test.go | 10 ++-------- transport/internet/hysteria/conn.go | 12 ------------ transport/internet/hysteria/hub.go | 8 ++------ 6 files changed, 31 insertions(+), 38 deletions(-) diff --git a/proxy/hysteria/account/config.go b/proxy/hysteria/account/config.go index a4b85e0e95ba..f32c157aada2 100644 --- a/proxy/hysteria/account/config.go +++ b/proxy/hysteria/account/config.go @@ -67,6 +67,12 @@ func HysteriaRouteFromAuth(auth string) net.Port { return net.PortFromBytes(id.Bytes()[6:8]) } +func userWithAuth(user *protocol.MemoryUser, auth string) *protocol.MemoryUser { + clone := *user + clone.Account = &MemoryAccount{Auth: auth} + return &clone +} + func NewValidator() *Validator { return &Validator{ emails: make(map[string]struct{}), @@ -126,7 +132,7 @@ func (v *Validator) Get(auth string) *protocol.MemoryUser { for storedAuth, user := range v.users { storedKey, ok := hysteriaRouteAuthKey(storedAuth) if ok && storedKey == key { - return user + return userWithAuth(user, auth) } } return nil diff --git a/proxy/hysteria/account/config_test.go b/proxy/hysteria/account/config_test.go index a99c3a09fe7a..9a05a872ac6f 100644 --- a/proxy/hysteria/account/config_test.go +++ b/proxy/hysteria/account/config_test.go @@ -7,18 +7,35 @@ import ( ) func TestValidatorGetAcceptsHysteriaRouteAuthVariant(t *testing.T) { + const serverAuth = "00000000-0000-1234-8000-000000000000" + const clientAuth = "00000000-0000-0001-8000-000000000000" + user := &protocol.MemoryUser{ Email: "user@example.com", - Account: &MemoryAccount{Auth: "00000000-0000-1234-8000-000000000000"}, + Level: 1, + Account: &MemoryAccount{Auth: serverAuth}, } validator := NewValidator() if err := validator.Add(user); err != nil { t.Fatal(err) } - if got := validator.Get("00000000-0000-0001-8000-000000000000"); got != user { + got := validator.Get(clientAuth) + if got == nil { t.Fatal("validator did not accept UUID auth variant with a different Hysteria route") } + if got == user { + t.Fatal("validator returned stored user instead of a copy with the client auth") + } + if got.Email != user.Email || got.Level != user.Level { + t.Fatalf("validator returned wrong user metadata: %v", got) + } + if got.Account.(*MemoryAccount).Auth != clientAuth { + t.Fatalf("returned auth = %q, want %q", got.Account.(*MemoryAccount).Auth, clientAuth) + } + if user.Account.(*MemoryAccount).Auth != serverAuth { + t.Fatalf("stored auth was mutated to %q", user.Account.(*MemoryAccount).Auth) + } if got := validator.Get("00000000-0000-0001-9000-000000000000"); got != nil { t.Fatalf("validator accepted auth with non-route bytes changed: %v", got) } diff --git a/proxy/hysteria/server.go b/proxy/hysteria/server.go index f150c3cc28a5..a970960388bf 100644 --- a/proxy/hysteria/server.go +++ b/proxy/hysteria/server.go @@ -92,18 +92,10 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con iConn := stat.TryUnwrapStatsConn(conn) type User interface{ User() *protocol.MemoryUser } - type Auth interface{ Auth() string } if v, ok := iConn.(User); ok && v.User() != nil { inbound.User = v.User() - auth := "" - if v, ok := iConn.(Auth); ok { - auth = v.Auth() - } if hysteriaAccount, ok := inbound.User.Account.(*account.MemoryAccount); ok { - if auth == "" { - auth = hysteriaAccount.Auth - } - inbound.HysteriaRoute = account.HysteriaRouteFromAuth(auth) + inbound.HysteriaRoute = account.HysteriaRouteFromAuth(hysteriaAccount.Auth) } } diff --git a/proxy/hysteria/server_test.go b/proxy/hysteria/server_test.go index 810c962176cd..10760f11884f 100644 --- a/proxy/hysteria/server_test.go +++ b/proxy/hysteria/server_test.go @@ -77,17 +77,12 @@ func (*testPolicyManager) ForSystem() policy.System { type testUserConn struct { user *protocol.MemoryUser - auth string } func (c *testUserConn) User() *protocol.MemoryUser { return c.user } -func (c *testUserConn) Auth() string { - return c.auth -} - func (*testUserConn) Read([]byte) (int, error) { return 0, io.EOF } @@ -121,12 +116,11 @@ func (*testUserConn) SetWriteDeadline(time.Time) error { } func TestServerProcessSetsHysteriaRouteFromAuth(t *testing.T) { - const serverAuth = "00000000-0000-1234-8000-000000000000" const clientAuth = "00000000-0000-0001-8000-000000000000" inbound := &session.Inbound{} user := &protocol.MemoryUser{ - Account: &account.MemoryAccount{Auth: serverAuth}, + Account: &account.MemoryAccount{Auth: clientAuth}, Level: 1, Email: "user@example.com", } @@ -134,7 +128,7 @@ func TestServerProcessSetsHysteriaRouteFromAuth(t *testing.T) { policyManager: &testPolicyManager{}, } - err := server.Process(session.ContextWithInbound(context.Background(), inbound), net.Network_TCP, &testUserConn{user: user, auth: clientAuth}, nil) + err := server.Process(session.ContextWithInbound(context.Background(), inbound), net.Network_TCP, &testUserConn{user: user}, nil) if err == nil { t.Fatal("Process unexpectedly succeeded with an empty test connection") } diff --git a/transport/internet/hysteria/conn.go b/transport/internet/hysteria/conn.go index f2df168170b8..ce2a4af31cf7 100644 --- a/transport/internet/hysteria/conn.go +++ b/transport/internet/hysteria/conn.go @@ -22,17 +22,12 @@ type interConn struct { client bool user *protocol.MemoryUser - auth string } func (c *interConn) User() *protocol.MemoryUser { return c.user } -func (c *interConn) Auth() string { - return c.auth -} - func (c *interConn) Read(b []byte) (int, error) { return c.stream.Read(b) } @@ -87,17 +82,12 @@ type InterConn struct { write func(p []byte) error close func() user *protocol.MemoryUser - auth string } func (i *InterConn) User() *protocol.MemoryUser { return i.user } -func (i *InterConn) Auth() string { - return i.auth -} - func (c *InterConn) Time() time.Time { c.mutex.Lock() v := c.time @@ -171,7 +161,6 @@ type udpSessionManager struct { addConn internet.ConnHandler udpIdleTimeout time.Duration user *protocol.MemoryUser - auth string } func (m *udpSessionManager) close(udpConn *InterConn) { @@ -298,7 +287,6 @@ func (m *udpSessionManager) feed(id uint32, d []byte) { m.Unlock() } udpConn.user = m.user - udpConn.auth = m.auth m.m[id] = udpConn m.addConn(udpConn) } diff --git a/transport/internet/hysteria/hub.go b/transport/internet/hysteria/hub.go index 38ad2161ac11..d20313b5d031 100644 --- a/transport/internet/hysteria/hub.go +++ b/transport/internet/hysteria/hub.go @@ -36,9 +36,8 @@ type httpHandler struct { addConn internet.ConnHandler conn *quic.Conn - auth bool - user *protocol.MemoryUser - authValue string + auth bool + user *protocol.MemoryUser } func (h *httpHandler) AuthHTTP(w http.ResponseWriter, r *http.Request) bool { @@ -68,7 +67,6 @@ func (h *httpHandler) AuthHTTP(w http.ResponseWriter, r *http.Request) bool { if user != nil || ok { h.auth = true h.user = user - h.authValue = auth conn := h.conn quicParams := h.quicParams @@ -96,7 +94,6 @@ func (h *httpHandler) AuthHTTP(w http.ResponseWriter, r *http.Request) bool { addConn: h.addConn, udpIdleTimeout: time.Duration(h.config.UdpIdleTimeout) * time.Second, user: h.user, - auth: h.authValue, } go udpSM.clean() go udpSM.run() @@ -136,7 +133,6 @@ func (h *httpHandler) StreamDispatcher(ft http3.FrameType, stream *quic.Stream, remote: h.conn.RemoteAddr(), user: h.user, - auth: h.authValue, }) return true, nil default: From 4d8264acc02c063857ec278926a7b54c18df3a48 Mon Sep 17 00:00:00 2001 From: seally Date: Thu, 25 Jun 2026 18:32:25 +0500 Subject: [PATCH 4/7] Hysteria: use VLESS route for auth IDs --- app/router/command/command.pb.go | 13 ++------ app/router/command/command.proto | 1 - app/router/command/config.go | 4 --- app/router/condition.go | 3 -- app/router/condition_test.go | 34 ------------------- app/router/config.go | 4 --- app/router/config.pb.go | 48 +++++++++++---------------- app/router/config.proto | 1 - common/session/session.go | 2 -- features/routing/context.go | 3 -- features/routing/session/context.go | 8 ----- infra/conf/router.go | 39 ++++++++++------------ infra/conf/router_test.go | 30 ----------------- proxy/hysteria/account/config.go | 8 ++--- proxy/hysteria/account/config_test.go | 4 +-- proxy/hysteria/server.go | 2 +- proxy/hysteria/server_test.go | 15 ++++----- 17 files changed, 51 insertions(+), 168 deletions(-) diff --git a/app/router/command/command.pb.go b/app/router/command/command.pb.go index 9a387268d237..db55b17e9700 100644 --- a/app/router/command/command.pb.go +++ b/app/router/command/command.pb.go @@ -43,7 +43,6 @@ type RoutingContext struct { 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"` - HysteriaRoute uint32 `protobuf:"varint,16,opt,name=HysteriaRoute,proto3" json:"HysteriaRoute,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -183,13 +182,6 @@ func (x *RoutingContext) GetVlessRoute() uint32 { return 0 } -func (x *RoutingContext) GetHysteriaRoute() uint32 { - if x != nil { - return x.HysteriaRoute - } - return 0 -} - // SubscribeRoutingStatsRequest subscribes to routing statistics channel if // opened by xray-core. // * FieldSelectors selects a subset of fields in routing statistics to return. @@ -975,7 +967,7 @@ var File_app_router_command_command_proto protoreflect.FileDescriptor 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\"\x9c\x05\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" + @@ -1002,8 +994,7 @@ const file_app_router_command_command_proto_rawDesc = "" + "\tLocalPort\x18\x0e \x01(\rR\tLocalPort\x12\x1e\n" + "\n" + "VlessRoute\x18\x0f \x01(\rR\n" + - "VlessRoute\x12$\n" + - "\rHysteriaRoute\x18\x10 \x01(\rR\rHysteriaRoute\x1a=\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" + diff --git a/app/router/command/command.proto b/app/router/command/command.proto index 9a57aeb1800e..22ae95b98440 100644 --- a/app/router/command/command.proto +++ b/app/router/command/command.proto @@ -28,7 +28,6 @@ message RoutingContext { repeated bytes LocalIPs = 13; uint32 LocalPort = 14; uint32 VlessRoute = 15; - uint32 HysteriaRoute = 16; } // SubscribeRoutingStatsRequest subscribes to routing statistics channel if diff --git a/app/router/command/config.go b/app/router/command/config.go index b1d76dc8f814..13a319b15528 100644 --- a/app/router/command/config.go +++ b/app/router/command/config.go @@ -40,10 +40,6 @@ func (c routingContext) GetVlessRoute() net.Port { return net.Port(c.RoutingContext.GetVlessRoute()) } -func (c routingContext) GetHysteriaRoute() net.Port { - return net.Port(c.RoutingContext.GetHysteriaRoute()) -} - func (c routingContext) GetRuleTag() string { return "" } diff --git a/app/router/condition.go b/app/router/condition.go index b6071115aab8..b8a482593234 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -75,7 +75,6 @@ const ( MatcherAsType_Source MatcherAsType_Target MatcherAsType_VlessRoute // for port - MatcherAsType_HysteriaRoute ) type IPMatcher struct { @@ -133,8 +132,6 @@ func (v *PortMatcher) Apply(ctx routing.Context) bool { return v.port.Contains(ctx.GetTargetPort()) case MatcherAsType_VlessRoute: return v.port.Contains(ctx.GetVlessRoute()) - case MatcherAsType_HysteriaRoute: - return v.port.Contains(ctx.GetHysteriaRoute()) default: panic("unk asType") } diff --git a/app/router/condition_test.go b/app/router/condition_test.go index 67233f63f43b..1b94bb8efe1a 100644 --- a/app/router/condition_test.go +++ b/app/router/condition_test.go @@ -187,40 +187,6 @@ func TestRoutingRule(t *testing.T) { }, }, }, - { - rule: &RoutingRule{ - VlessRouteList: &net.PortList{ - Range: []*net.PortRange{{From: 1, To: 1}}, - }, - }, - test: []ruleTest{ - { - input: withInbound(&session.Inbound{VlessRoute: 1}), - output: true, - }, - { - input: withInbound(&session.Inbound{HysteriaRoute: 1}), - output: false, - }, - }, - }, - { - rule: &RoutingRule{ - HysteriaRouteList: &net.PortList{ - Range: []*net.PortRange{{From: 1, To: 1}}, - }, - }, - test: []ruleTest{ - { - input: withInbound(&session.Inbound{HysteriaRoute: 1}), - output: true, - }, - { - input: withInbound(&session.Inbound{VlessRoute: 1}), - output: false, - }, - }, - }, { rule: &RoutingRule{ InboundTag: []string{"test", "test1"}, diff --git a/app/router/config.go b/app/router/config.go index 5160f5f8d98b..0a76905c10d6 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -61,10 +61,6 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { conds.Add(NewPortMatcher(rr.VlessRouteList, MatcherAsType_VlessRoute)) } - if rr.HysteriaRouteList != nil { - conds.Add(NewPortMatcher(rr.HysteriaRouteList, MatcherAsType_HysteriaRoute)) - } - if len(rr.UserEmail) > 0 { conds.Add(NewUserMatcher(rr.UserEmail)) } diff --git a/app/router/config.pb.go b/app/router/config.pb.go index be949c7815fa..6c1e27504573 100644 --- a/app/router/config.pb.go +++ b/app/router/config.pb.go @@ -103,13 +103,12 @@ type RoutingRule struct { // List of IPs for local IP address matching. LocalIp []*geodata.IPRule `protobuf:"bytes,17,rep,name=local_ip,json=localIp,proto3" json:"local_ip,omitempty"` // List of ports for local port matching. - 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"` - Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"` - Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"` - HysteriaRouteList *net.PortList `protobuf:"bytes,23,opt,name=hysteria_route_list,json=hysteriaRouteList,proto3" json:"hysteria_route_list,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + 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"` + Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"` + Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RoutingRule) Reset() { @@ -279,13 +278,6 @@ func (x *RoutingRule) GetWebhook() *WebhookConfig { return nil } -func (x *RoutingRule) GetHysteriaRouteList() *net.PortList { - if x != nil { - return x.HysteriaRouteList - } - return nil -} - type isRoutingRule_TargetTag interface { isRoutingRule_TargetTag() } @@ -645,7 +637,7 @@ var File_app_router_config_proto protoreflect.FileDescriptor const file_app_router_config_proto_rawDesc = "" + "\n" + - "\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"\x8c\b\n" + + "\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"\xc1\a\n" + "\vRoutingRule\x12\x12\n" + "\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" + "\rbalancing_tag\x18\f \x01(\tH\x00R\fbalancingTag\x12\x19\n" + @@ -669,8 +661,7 @@ const file_app_router_config_proto_rawDesc = "" + "\x0flocal_port_list\x18\x12 \x01(\v2\x19.xray.common.net.PortListR\rlocalPortList\x12C\n" + "\x10vless_route_list\x18\x14 \x01(\v2\x19.xray.common.net.PortListR\x0evlessRouteList\x12\x18\n" + "\aprocess\x18\x15 \x03(\tR\aprocess\x128\n" + - "\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x12I\n" + - "\x13hysteria_route_list\x18\x17 \x01(\v2\x19.xray.common.net.PortListR\x11hysteriaRouteList\x1a=\n" + + "\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x1a=\n" + "\x0fAttributesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\f\n" + @@ -752,18 +743,17 @@ var file_app_router_config_proto_depIdxs = []int32{ 11, // 8: xray.app.router.RoutingRule.local_port_list:type_name -> xray.common.net.PortList 11, // 9: xray.app.router.RoutingRule.vless_route_list:type_name -> xray.common.net.PortList 2, // 10: xray.app.router.RoutingRule.webhook:type_name -> xray.app.router.WebhookConfig - 11, // 11: xray.app.router.RoutingRule.hysteria_route_list:type_name -> xray.common.net.PortList - 8, // 12: xray.app.router.WebhookConfig.headers:type_name -> xray.app.router.WebhookConfig.HeadersEntry - 13, // 13: xray.app.router.BalancingRule.strategy_settings:type_name -> xray.common.serial.TypedMessage - 4, // 14: xray.app.router.StrategyLeastLoadConfig.costs:type_name -> xray.app.router.StrategyWeight - 0, // 15: xray.app.router.Config.domain_strategy:type_name -> xray.app.router.Config.DomainStrategy - 1, // 16: xray.app.router.Config.rule:type_name -> xray.app.router.RoutingRule - 3, // 17: xray.app.router.Config.balancing_rule:type_name -> xray.app.router.BalancingRule - 18, // [18:18] is the sub-list for method output_type - 18, // [18:18] is the sub-list for method input_type - 18, // [18:18] is the sub-list for extension type_name - 18, // [18:18] is the sub-list for extension extendee - 0, // [0:18] is the sub-list for field type_name + 8, // 11: xray.app.router.WebhookConfig.headers:type_name -> xray.app.router.WebhookConfig.HeadersEntry + 13, // 12: xray.app.router.BalancingRule.strategy_settings:type_name -> xray.common.serial.TypedMessage + 4, // 13: xray.app.router.StrategyLeastLoadConfig.costs:type_name -> xray.app.router.StrategyWeight + 0, // 14: xray.app.router.Config.domain_strategy:type_name -> xray.app.router.Config.DomainStrategy + 1, // 15: xray.app.router.Config.rule:type_name -> xray.app.router.RoutingRule + 3, // 16: xray.app.router.Config.balancing_rule:type_name -> xray.app.router.BalancingRule + 17, // [17:17] is the sub-list for method output_type + 17, // [17:17] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_app_router_config_proto_init() } diff --git a/app/router/config.proto b/app/router/config.proto index 4866a3e687a4..60f565c362aa 100644 --- a/app/router/config.proto +++ b/app/router/config.proto @@ -56,7 +56,6 @@ message RoutingRule { repeated string process = 21; WebhookConfig webhook = 22; - xray.common.net.PortList hysteria_route_list = 23; } message WebhookConfig { diff --git a/common/session/session.go b/common/session/session.go index 2902242a0e87..b5f163ff172e 100644 --- a/common/session/session.go +++ b/common/session/session.go @@ -49,8 +49,6 @@ type Inbound struct { User *protocol.MemoryUser // VlessRoute is the user-sent VLESS UUID's 7th<<8 | 8th bytes. VlessRoute net.Port - // HysteriaRoute is the user-sent Hysteria UUID auth's 7th<<8 | 8th bytes. - HysteriaRoute net.Port // Used by splice copy. Conn is actually internet.Connection. May be nil. Conn net.Conn // Used by splice copy. Timer of the inbound buf copier. May be nil. diff --git a/features/routing/context.go b/features/routing/context.go index 09bc7974c3a9..6b38d175c2d8 100644 --- a/features/routing/context.go +++ b/features/routing/context.go @@ -44,9 +44,6 @@ type Context interface { // GetVlessRoute returns the user-sent VLESS UUID's 7th<<8 | 8th bytes, if exists. GetVlessRoute() net.Port - // GetHysteriaRoute returns the user-sent Hysteria UUID auth's 7th<<8 | 8th bytes, if exists. - GetHysteriaRoute() net.Port - // GetAttributes returns extra attributes from the conneciont content. GetAttributes() map[string]string diff --git a/features/routing/session/context.go b/features/routing/session/context.go index 4e4268c2466a..e331c4347213 100644 --- a/features/routing/session/context.go +++ b/features/routing/session/context.go @@ -135,14 +135,6 @@ func (ctx *Context) GetVlessRoute() net.Port { return ctx.Inbound.VlessRoute } -// GetHysteriaRoute implements routing.Context. -func (ctx *Context) GetHysteriaRoute() net.Port { - if ctx.Inbound == nil { - return 0 - } - return ctx.Inbound.HysteriaRoute -} - // GetAttributes implements routing.Context. func (ctx *Context) GetAttributes() map[string]string { if ctx.Content == nil { diff --git a/infra/conf/router.go b/infra/conf/router.go index cb71dbd2dfe5..af83c743597c 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -132,24 +132,23 @@ type WebhookRuleConfig struct { 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"` - HysteriaRoute *PortList `json:"hysteriaRoute"` - 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"` - Webhook *WebhookRuleConfig `json:"webhook"` + 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"` + Webhook *WebhookRuleConfig `json:"webhook"` } rawFieldRule := new(RawFieldRule) err := json.Unmarshal(msg, rawFieldRule) @@ -242,10 +241,6 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) { rule.VlessRouteList = rawFieldRule.VlessRoute.Build() } - if rawFieldRule.HysteriaRoute != nil { - rule.HysteriaRouteList = rawFieldRule.HysteriaRoute.Build() - } - if rawFieldRule.InboundTag != nil { for _, s := range *rawFieldRule.InboundTag { rule.InboundTag = append(rule.InboundTag, s) diff --git a/infra/conf/router_test.go b/infra/conf/router_test.go index 78688982e341..130cf4f7857a 100644 --- a/infra/conf/router_test.go +++ b/infra/conf/router_test.go @@ -236,33 +236,3 @@ func TestRouterConfig(t *testing.T) { }, }) } - -func TestRouterConfigHysteriaRoute(t *testing.T) { - config := new(RouterConfig) - err := json.Unmarshal([]byte(`{ - "rules": [ - { - "hysteriaRoute": "1-2", - "outboundTag": "hysteria" - }, - { - "vlessRoute": "3", - "outboundTag": "vless" - } - ] - }`), config) - if err != nil { - t.Fatal(err) - } - - built, err := config.Build() - if err != nil { - t.Fatal(err) - } - if got := built.Rule[0].GetHysteriaRouteList().Range[0]; got.From != 1 || got.To != 2 { - t.Fatalf("hysteriaRoute = %d-%d, want 1-2", got.From, got.To) - } - if got := built.Rule[1].GetVlessRouteList().Range[0]; got.From != 3 || got.To != 3 { - t.Fatalf("vlessRoute = %d-%d, want 3-3", got.From, got.To) - } -} diff --git a/proxy/hysteria/account/config.go b/proxy/hysteria/account/config.go index f32c157aada2..dc9c9058b708 100644 --- a/proxy/hysteria/account/config.go +++ b/proxy/hysteria/account/config.go @@ -41,7 +41,7 @@ type Validator struct { mutex sync.Mutex } -func hysteriaRouteAuthKey(auth string) ([16]byte, bool) { +func vlessRouteAuthKey(auth string) ([16]byte, bool) { var key [16]byte if len(auth) != 32 && len(auth) != 36 { return key, false @@ -56,7 +56,7 @@ func hysteriaRouteAuthKey(auth string) ([16]byte, bool) { return key, true } -func HysteriaRouteFromAuth(auth string) net.Port { +func VlessRouteFromAuth(auth string) net.Port { if len(auth) != 32 && len(auth) != 36 { return 0 } @@ -125,12 +125,12 @@ func (v *Validator) Get(auth string) *protocol.MemoryUser { return user } - key, ok := hysteriaRouteAuthKey(auth) + key, ok := vlessRouteAuthKey(auth) if !ok { return nil } for storedAuth, user := range v.users { - storedKey, ok := hysteriaRouteAuthKey(storedAuth) + storedKey, ok := vlessRouteAuthKey(storedAuth) if ok && storedKey == key { return userWithAuth(user, auth) } diff --git a/proxy/hysteria/account/config_test.go b/proxy/hysteria/account/config_test.go index 9a05a872ac6f..031342f22058 100644 --- a/proxy/hysteria/account/config_test.go +++ b/proxy/hysteria/account/config_test.go @@ -6,7 +6,7 @@ import ( "github.com/xtls/xray-core/common/protocol" ) -func TestValidatorGetAcceptsHysteriaRouteAuthVariant(t *testing.T) { +func TestValidatorGetAcceptsVlessRouteAuthVariant(t *testing.T) { const serverAuth = "00000000-0000-1234-8000-000000000000" const clientAuth = "00000000-0000-0001-8000-000000000000" @@ -22,7 +22,7 @@ func TestValidatorGetAcceptsHysteriaRouteAuthVariant(t *testing.T) { got := validator.Get(clientAuth) if got == nil { - t.Fatal("validator did not accept UUID auth variant with a different Hysteria route") + t.Fatal("validator did not accept UUID auth variant with a different VLESS route") } if got == user { t.Fatal("validator returned stored user instead of a copy with the client auth") diff --git a/proxy/hysteria/server.go b/proxy/hysteria/server.go index a970960388bf..e99f0142873d 100644 --- a/proxy/hysteria/server.go +++ b/proxy/hysteria/server.go @@ -95,7 +95,7 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con if v, ok := iConn.(User); ok && v.User() != nil { inbound.User = v.User() if hysteriaAccount, ok := inbound.User.Account.(*account.MemoryAccount); ok { - inbound.HysteriaRoute = account.HysteriaRouteFromAuth(hysteriaAccount.Auth) + inbound.VlessRoute = account.VlessRouteFromAuth(hysteriaAccount.Auth) } } diff --git a/proxy/hysteria/server_test.go b/proxy/hysteria/server_test.go index 10760f11884f..de02fe736a89 100644 --- a/proxy/hysteria/server_test.go +++ b/proxy/hysteria/server_test.go @@ -14,7 +14,7 @@ import ( "github.com/xtls/xray-core/proxy/hysteria/account" ) -func TestHysteriaRouteFromAuth(t *testing.T) { +func TestVlessRouteFromAuth(t *testing.T) { const routeID = net.Port(0x1234) tests := []struct { @@ -46,8 +46,8 @@ func TestHysteriaRouteFromAuth(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - if got := account.HysteriaRouteFromAuth(test.auth); got != test.want { - t.Fatalf("HysteriaRouteFromAuth(%q) = %d, want %d", test.auth, got, test.want) + if got := account.VlessRouteFromAuth(test.auth); got != test.want { + t.Fatalf("VlessRouteFromAuth(%q) = %d, want %d", test.auth, got, test.want) } }) } @@ -115,7 +115,7 @@ func (*testUserConn) SetWriteDeadline(time.Time) error { return nil } -func TestServerProcessSetsHysteriaRouteFromAuth(t *testing.T) { +func TestServerProcessSetsVlessRouteFromAuth(t *testing.T) { const clientAuth = "00000000-0000-0001-8000-000000000000" inbound := &session.Inbound{} @@ -135,10 +135,7 @@ func TestServerProcessSetsHysteriaRouteFromAuth(t *testing.T) { if inbound.User != user { t.Fatal("server did not use the authenticated Hysteria user from the connection") } - if inbound.HysteriaRoute != 1 { - t.Fatalf("inbound.HysteriaRoute = %d, want %d", inbound.HysteriaRoute, net.Port(1)) - } - if inbound.VlessRoute != 0 { - t.Fatalf("inbound.VlessRoute = %d, want 0", inbound.VlessRoute) + if inbound.VlessRoute != 1 { + t.Fatalf("inbound.VlessRoute = %d, want %d", inbound.VlessRoute, net.Port(1)) } } From 6a4390d14a377ef0305c3a107ebedac9c639c798 Mon Sep 17 00:00:00 2001 From: seally Date: Thu, 25 Jun 2026 18:42:16 +0500 Subject: [PATCH 5/7] Hysteria: remove route auth tests --- proxy/hysteria/account/config_test.go | 45 -------- proxy/hysteria/server_test.go | 141 -------------------------- 2 files changed, 186 deletions(-) delete mode 100644 proxy/hysteria/account/config_test.go delete mode 100644 proxy/hysteria/server_test.go diff --git a/proxy/hysteria/account/config_test.go b/proxy/hysteria/account/config_test.go deleted file mode 100644 index 031342f22058..000000000000 --- a/proxy/hysteria/account/config_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package account - -import ( - "testing" - - "github.com/xtls/xray-core/common/protocol" -) - -func TestValidatorGetAcceptsVlessRouteAuthVariant(t *testing.T) { - const serverAuth = "00000000-0000-1234-8000-000000000000" - const clientAuth = "00000000-0000-0001-8000-000000000000" - - user := &protocol.MemoryUser{ - Email: "user@example.com", - Level: 1, - Account: &MemoryAccount{Auth: serverAuth}, - } - validator := NewValidator() - if err := validator.Add(user); err != nil { - t.Fatal(err) - } - - got := validator.Get(clientAuth) - if got == nil { - t.Fatal("validator did not accept UUID auth variant with a different VLESS route") - } - if got == user { - t.Fatal("validator returned stored user instead of a copy with the client auth") - } - if got.Email != user.Email || got.Level != user.Level { - t.Fatalf("validator returned wrong user metadata: %v", got) - } - if got.Account.(*MemoryAccount).Auth != clientAuth { - t.Fatalf("returned auth = %q, want %q", got.Account.(*MemoryAccount).Auth, clientAuth) - } - if user.Account.(*MemoryAccount).Auth != serverAuth { - t.Fatalf("stored auth was mutated to %q", user.Account.(*MemoryAccount).Auth) - } - if got := validator.Get("00000000-0000-0001-9000-000000000000"); got != nil { - t.Fatalf("validator accepted auth with non-route bytes changed: %v", got) - } - if got := validator.Get("password"); got != nil { - t.Fatalf("validator accepted unrelated password auth: %v", got) - } -} diff --git a/proxy/hysteria/server_test.go b/proxy/hysteria/server_test.go deleted file mode 100644 index de02fe736a89..000000000000 --- a/proxy/hysteria/server_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package hysteria - -import ( - "context" - "io" - stdnet "net" - "testing" - "time" - - "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/features/policy" - "github.com/xtls/xray-core/proxy/hysteria/account" -) - -func TestVlessRouteFromAuth(t *testing.T) { - const routeID = net.Port(0x1234) - - tests := []struct { - name string - auth string - want net.Port - }{ - { - name: "hyphenated UUID", - auth: "00000000-0000-1234-8000-000000000000", - want: routeID, - }, - { - name: "plain UUID", - auth: "00000000000012348000000000000000", - want: routeID, - }, - { - name: "password auth", - auth: "password", - want: 0, - }, - { - name: "invalid UUID", - auth: "00000000-0000-123k-8000-000000000000", - want: 0, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if got := account.VlessRouteFromAuth(test.auth); got != test.want { - t.Fatalf("VlessRouteFromAuth(%q) = %d, want %d", test.auth, got, test.want) - } - }) - } -} - -type testPolicyManager struct{} - -func (*testPolicyManager) Type() interface{} { - return policy.ManagerType() -} - -func (*testPolicyManager) Start() error { - return nil -} - -func (*testPolicyManager) Close() error { - return nil -} - -func (*testPolicyManager) ForLevel(uint32) policy.Session { - return policy.SessionDefault() -} - -func (*testPolicyManager) ForSystem() policy.System { - return policy.System{} -} - -type testUserConn struct { - user *protocol.MemoryUser -} - -func (c *testUserConn) User() *protocol.MemoryUser { - return c.user -} - -func (*testUserConn) Read([]byte) (int, error) { - return 0, io.EOF -} - -func (*testUserConn) Write(b []byte) (int, error) { - return len(b), nil -} - -func (*testUserConn) Close() error { - return nil -} - -func (*testUserConn) LocalAddr() stdnet.Addr { - return &stdnet.TCPAddr{IP: stdnet.IPv4(127, 0, 0, 1), Port: 443} -} - -func (*testUserConn) RemoteAddr() stdnet.Addr { - return &stdnet.TCPAddr{IP: stdnet.IPv4(127, 0, 0, 1), Port: 12345} -} - -func (*testUserConn) SetDeadline(time.Time) error { - return nil -} - -func (*testUserConn) SetReadDeadline(time.Time) error { - return nil -} - -func (*testUserConn) SetWriteDeadline(time.Time) error { - return nil -} - -func TestServerProcessSetsVlessRouteFromAuth(t *testing.T) { - const clientAuth = "00000000-0000-0001-8000-000000000000" - - inbound := &session.Inbound{} - user := &protocol.MemoryUser{ - Account: &account.MemoryAccount{Auth: clientAuth}, - Level: 1, - Email: "user@example.com", - } - server := &Server{ - policyManager: &testPolicyManager{}, - } - - err := server.Process(session.ContextWithInbound(context.Background(), inbound), net.Network_TCP, &testUserConn{user: user}, nil) - if err == nil { - t.Fatal("Process unexpectedly succeeded with an empty test connection") - } - if inbound.User != user { - t.Fatal("server did not use the authenticated Hysteria user from the connection") - } - if inbound.VlessRoute != 1 { - t.Fatalf("inbound.VlessRoute = %d, want %d", inbound.VlessRoute, net.Port(1)) - } -} From 843ed060dbfc02c067957cc2624df62a5107b052 Mon Sep 17 00:00:00 2001 From: null Date: Thu, 25 Jun 2026 22:20:23 +0800 Subject: [PATCH 6/7] chore --- proxy/hysteria/account/config.go | 165 ++++++++--------------------- proxy/hysteria/server.go | 18 ++-- transport/internet/hysteria/hub.go | 2 +- 3 files changed, 55 insertions(+), 130 deletions(-) diff --git a/proxy/hysteria/account/config.go b/proxy/hysteria/account/config.go index dc9c9058b708..01ff8c2e7111 100644 --- a/proxy/hysteria/account/config.go +++ b/proxy/hysteria/account/config.go @@ -3,7 +3,6 @@ package account import ( "sync" - "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/uuid" @@ -12,18 +11,24 @@ import ( ) func (a *Account) AsAccount() (protocol.Account, error) { + var VR net.Port + if id, err := uuid.ParseString(a.Auth); err == nil { + VR = net.PortFromBytes(id[6:8]) + } return &MemoryAccount{ Auth: a.Auth, + VR: VR, }, nil } type MemoryAccount struct { Auth string + VR net.Port } -func (a *MemoryAccount) Equals(another protocol.Account) bool { - if account, ok := another.(*MemoryAccount); ok { - return a.Auth == account.Auth +func (a *MemoryAccount) Equals(other protocol.Account) bool { + if b, ok := other.(*MemoryAccount); ok { + return a.Auth == b.Auth } return false } @@ -35,143 +40,63 @@ func (a *MemoryAccount) ToProto() proto.Message { } type Validator struct { - emails map[string]struct{} - users map[string]*protocol.MemoryUser - - mutex sync.Mutex -} - -func vlessRouteAuthKey(auth string) ([16]byte, bool) { - var key [16]byte - if len(auth) != 32 && len(auth) != 36 { - return key, false - } - id, err := uuid.ParseString(auth) - if err != nil { - return key, false - } - copy(key[:], id.Bytes()) - key[6] = 0 - key[7] = 0 - return key, true -} - -func VlessRouteFromAuth(auth string) net.Port { - if len(auth) != 32 && len(auth) != 36 { - return 0 - } - id, err := uuid.ParseString(auth) - if err != nil { - return 0 - } - return net.PortFromBytes(id.Bytes()[6:8]) -} - -func userWithAuth(user *protocol.MemoryUser, auth string) *protocol.MemoryUser { - clone := *user - clone.Account = &MemoryAccount{Auth: auth} - return &clone + users sync.Map } func NewValidator() *Validator { - return &Validator{ - emails: make(map[string]struct{}), - users: make(map[string]*protocol.MemoryUser), - } + return &Validator{} } -func (v *Validator) Add(u *protocol.MemoryUser) error { - v.mutex.Lock() - defer v.mutex.Unlock() - - if u.Email != "" { - if _, ok := v.emails[u.Email]; ok { - return errors.New("User ", u.Email, " already exists.") - } - v.emails[u.Email] = struct{}{} - } - v.users[u.Account.(*MemoryAccount).Auth] = u - +func (v *Validator) Add(user *protocol.MemoryUser) error { + v.users.Store(user.Account.(*MemoryAccount).Auth, user) return nil } -func (v *Validator) Del(email string) error { - if email == "" { - return errors.New("Email must not be empty.") - } - - v.mutex.Lock() - defer v.mutex.Unlock() - - if _, ok := v.emails[email]; !ok { - return errors.New("User ", email, " not found.") - } - delete(v.emails, email) - for key, user := range v.users { - if user.Email == email { - delete(v.users, key) - break - } +func (v *Validator) DelByEmail(email string) error { + if user := v.GetByEmail(email); user != nil { + v.users.Delete(user.Account.(*MemoryAccount).Auth) } - return nil } func (v *Validator) Get(auth string) *protocol.MemoryUser { - v.mutex.Lock() - defer v.mutex.Unlock() - - if user := v.users[auth]; user != nil { - return user - } - - key, ok := vlessRouteAuthKey(auth) - if !ok { - return nil - } - for storedAuth, user := range v.users { - storedKey, ok := vlessRouteAuthKey(storedAuth) - if ok && storedKey == key { - return userWithAuth(user, auth) - } + if user, ok := v.users.Load(auth); ok { + return user.(*protocol.MemoryUser) } return nil } -func (v *Validator) GetByEmail(email string) *protocol.MemoryUser { - if email == "" { - return nil - } - - v.mutex.Lock() - defer v.mutex.Unlock() - - if _, ok := v.emails[email]; ok { - for _, user := range v.users { - if user.Email == email { - return user - } +func (v *Validator) GetByEmail(email string) (user *protocol.MemoryUser) { + v.users.Range(func(key, value any) bool { + if value.(*protocol.MemoryUser).Email == email { + user = value.(*protocol.MemoryUser) + return false } - } - - return nil + return true + }) + return } -func (v *Validator) GetAll() []*protocol.MemoryUser { - v.mutex.Lock() - defer v.mutex.Unlock() - - users := make([]*protocol.MemoryUser, 0, len(v.users)) - for _, user := range v.users { - users = append(users, user) - } - - return users +func (v *Validator) GetAll() (users []*protocol.MemoryUser) { + v.users.Range(func(key, value any) bool { + users = append(users, value.(*protocol.MemoryUser)) + return true + }) + return } -func (v *Validator) GetCount() int64 { - v.mutex.Lock() - defer v.mutex.Unlock() +func (v *Validator) GetCount() (count int64) { + v.users.Range(func(key, value any) bool { + count++ + return true + }) + return +} - return int64(len(v.users)) +func (v *Validator) NotEmpty() (not_empty bool) { + v.users.Range(func(key, value any) bool { + not_empty = true + return false + }) + return } diff --git a/proxy/hysteria/server.go b/proxy/hysteria/server.go index e99f0142873d..1e54b654bfc6 100644 --- a/proxy/hysteria/server.go +++ b/proxy/hysteria/server.go @@ -59,12 +59,12 @@ func (s *Server) HysteriaInboundValidator() *account.Validator { return s.validator } -func (s *Server) AddUser(ctx context.Context, u *protocol.MemoryUser) error { - return s.validator.Add(u) +func (s *Server) AddUser(ctx context.Context, user *protocol.MemoryUser) error { + return s.validator.Add(user) } -func (s *Server) RemoveUser(ctx context.Context, e string) error { - return s.validator.Del(e) +func (s *Server) RemoveUser(ctx context.Context, email string) error { + return s.validator.DelByEmail(email) } func (s *Server) GetUser(ctx context.Context, email string) *protocol.MemoryUser { @@ -91,11 +91,11 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con iConn := stat.TryUnwrapStatsConn(conn) - type User interface{ User() *protocol.MemoryUser } - if v, ok := iConn.(User); ok && v.User() != nil { - inbound.User = v.User() - if hysteriaAccount, ok := inbound.User.Account.(*account.MemoryAccount); ok { - inbound.VlessRoute = account.VlessRouteFromAuth(hysteriaAccount.Auth) + if v, ok := iConn.(interface{ User() *protocol.MemoryUser }); ok { + user := v.User() + if user != nil { + inbound.User = user + inbound.VlessRoute = user.Account.(*account.MemoryAccount).VR } } diff --git a/transport/internet/hysteria/hub.go b/transport/internet/hysteria/hub.go index d20313b5d031..59bcd7d4251f 100644 --- a/transport/internet/hysteria/hub.go +++ b/transport/internet/hysteria/hub.go @@ -58,7 +58,7 @@ func (h *httpHandler) AuthHTTP(w http.ResponseWriter, r *http.Request) bool { var user *protocol.MemoryUser var ok bool - if h.validator != nil && h.validator.GetCount() > 0 { + if h.validator != nil && h.validator.NotEmpty() { user = h.validator.Get(auth) } else if h.config.Auth != "" { ok = auth == h.config.Auth From 8811f5fdd501038714c6b3cb5e12e45260a4fc91 Mon Sep 17 00:00:00 2001 From: null Date: Thu, 25 Jun 2026 22:28:10 +0800 Subject: [PATCH 7/7] chore --- proxy/hysteria/account/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/hysteria/account/config.go b/proxy/hysteria/account/config.go index 01ff8c2e7111..299104585440 100644 --- a/proxy/hysteria/account/config.go +++ b/proxy/hysteria/account/config.go @@ -60,8 +60,8 @@ func (v *Validator) DelByEmail(email string) error { } func (v *Validator) Get(auth string) *protocol.MemoryUser { - if user, ok := v.users.Load(auth); ok { - return user.(*protocol.MemoryUser) + if value, ok := v.users.Load(auth); ok { + return value.(*protocol.MemoryUser) } return nil }