VLESS Reverse Proxy: Add "sniffing" to outbound's "reverse" (which is actually an inbound) (#5837)

Closes https://github.com/XTLS/Xray-core/issues/5662

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
This commit is contained in:
Copilot
2026-03-23 09:49:32 +00:00
committed by GitHub
parent d8a8629a14
commit 755f0a1d12
5 changed files with 84 additions and 27 deletions
-2
View File
@@ -129,8 +129,6 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+39 -7
View File
@@ -78,9 +78,14 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
return nil, errors.New(`VLESS clients: "encryption" should not be in inbound settings`) return nil, errors.New(`VLESS clients: "encryption" should not be in inbound settings`)
} }
if account.Reverse != nil && account.Reverse.Tag == "" { if account.Reverse != nil {
if account.Reverse.Tag == "" {
return nil, errors.New(`VLESS clients: "tag" can't be empty for "reverse"`) return nil, errors.New(`VLESS clients: "tag" can't be empty for "reverse"`)
} }
if account.Reverse.Sniffing != nil { // may not be reached: error json unmarshal
return nil, errors.New(`VLESS clients: inbound's "reverse" can't have "sniffing"`)
}
}
user.Account = serial.ToTypedMessage(account) user.Account = serial.ToTypedMessage(account)
config.Clients[idx] = user config.Clients[idx] = user
@@ -197,6 +202,28 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
return config, nil return config, nil
} }
type VLessReverseConfig struct {
Tag string `json:"tag"`
Sniffing *SniffingConfig `json:"sniffing"`
}
func (c *VLessReverseConfig) Build() (*vless.Reverse, error) {
if c.Tag == "" {
return nil, errors.New(`VLESS reverse: "tag" can't be empty`)
}
r := &vless.Reverse{
Tag: c.Tag,
}
if c.Sniffing != nil {
sc, err := c.Sniffing.Build()
if err != nil {
return nil, errors.New(`VLESS reverse: invalid "sniffing" config`).Base(err)
}
r.Sniffing = sc
}
return r, nil
}
type VLessOutboundVnext struct { type VLessOutboundVnext struct {
Address *Address `json:"address"` Address *Address `json:"address"`
Port uint16 `json:"port"` Port uint16 `json:"port"`
@@ -212,7 +239,7 @@ type VLessOutboundConfig struct {
Flow string `json:"flow"` Flow string `json:"flow"`
Seed string `json:"seed"` Seed string `json:"seed"`
Encryption string `json:"encryption"` Encryption string `json:"encryption"`
Reverse *vless.Reverse `json:"reverse"` Reverse *VLessReverseConfig `json:"reverse"`
Testpre uint32 `json:"testpre"` Testpre uint32 `json:"testpre"`
Testseed []uint32 `json:"testseed"` Testseed []uint32 `json:"testseed"`
Vnext []*VLessOutboundVnext `json:"vnext"` Vnext []*VLessOutboundVnext `json:"vnext"`
@@ -260,13 +287,22 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) {
account.Flow = c.Flow account.Flow = c.Flow
//account.Seed = c.Seed //account.Seed = c.Seed
account.Encryption = c.Encryption account.Encryption = c.Encryption
account.Reverse = c.Reverse if c.Reverse != nil {
rvs, err := c.Reverse.Build()
if err != nil {
return nil, err
}
account.Reverse = rvs
}
account.Testpre = c.Testpre account.Testpre = c.Testpre
account.Testseed = c.Testseed account.Testseed = c.Testseed
} else { } else {
if err := json.Unmarshal(rawUser, account); err != nil { if err := json.Unmarshal(rawUser, account); err != nil {
return nil, errors.New(`VLESS users: invalid user`).Base(err) return nil, errors.New(`VLESS users: invalid user`).Base(err)
} }
if account.Reverse != nil { // may not be reached: error json unmarshal
return nil, errors.New(`VLESS users: please use simplified outbound's config style to use "reverse"`)
}
} }
u, err := uuid.ParseString(account.Id) u, err := uuid.ParseString(account.Id)
@@ -326,10 +362,6 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) {
return nil, errors.New(`VLESS users: unsupported "encryption": ` + account.Encryption) return nil, errors.New(`VLESS users: unsupported "encryption": ` + account.Encryption)
} }
if account.Reverse != nil && account.Reverse.Tag == "" {
return nil, errors.New(`VLESS clients: "tag" can't be empty for "reverse"`)
}
user.Account = serial.ToTypedMessage(account) user.Account = serial.ToTypedMessage(account)
spec.User = user spec.User = user
break break
+20 -8
View File
@@ -7,6 +7,7 @@
package vless package vless
import ( import (
proxyman "github.com/xtls/xray-core/app/proxyman"
protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
@@ -24,6 +25,7 @@ const (
type Reverse struct { type Reverse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
Sniffing *proxyman.SniffingConfig `protobuf:"bytes,2,opt,name=sniffing,proto3" json:"sniffing,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -65,6 +67,13 @@ func (x *Reverse) GetTag() string {
return "" return ""
} }
func (x *Reverse) GetSniffing() *proxyman.SniffingConfig {
if x != nil {
return x.Sniffing
}
return nil
}
type Account struct { type Account struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
// ID of the account, in the form of a UUID, e.g., "66ad4540-b58c-4ad2-9926-ea63445a9b57". // ID of the account, in the form of a UUID, e.g., "66ad4540-b58c-4ad2-9926-ea63445a9b57".
@@ -179,9 +188,10 @@ var File_proxy_vless_account_proto protoreflect.FileDescriptor
const file_proxy_vless_account_proto_rawDesc = "" + const file_proxy_vless_account_proto_rawDesc = "" +
"\n" + "\n" +
"\x19proxy/vless/account.proto\x12\x10xray.proxy.vless\"\x1b\n" + "\x19proxy/vless/account.proto\x12\x10xray.proxy.vless\x1a\x19app/proxyman/config.proto\"Z\n" +
"\aReverse\x12\x10\n" + "\aReverse\x12\x10\n" +
"\x03tag\x18\x01 \x01(\tR\x03tag\"\x86\x02\n" + "\x03tag\x18\x01 \x01(\tR\x03tag\x12=\n" +
"\bsniffing\x18\x02 \x01(\v2!.xray.app.proxyman.SniffingConfigR\bsniffing\"\x86\x02\n" +
"\aAccount\x12\x0e\n" + "\aAccount\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04flow\x18\x02 \x01(\tR\x04flow\x12\x1e\n" + "\x04flow\x18\x02 \x01(\tR\x04flow\x12\x1e\n" +
@@ -212,14 +222,16 @@ var file_proxy_vless_account_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_proxy_vless_account_proto_goTypes = []any{ var file_proxy_vless_account_proto_goTypes = []any{
(*Reverse)(nil), // 0: xray.proxy.vless.Reverse (*Reverse)(nil), // 0: xray.proxy.vless.Reverse
(*Account)(nil), // 1: xray.proxy.vless.Account (*Account)(nil), // 1: xray.proxy.vless.Account
(*proxyman.SniffingConfig)(nil), // 2: xray.app.proxyman.SniffingConfig
} }
var file_proxy_vless_account_proto_depIdxs = []int32{ var file_proxy_vless_account_proto_depIdxs = []int32{
0, // 0: xray.proxy.vless.Account.reverse:type_name -> xray.proxy.vless.Reverse 2, // 0: xray.proxy.vless.Reverse.sniffing:type_name -> xray.app.proxyman.SniffingConfig
1, // [1:1] is the sub-list for method output_type 0, // 1: xray.proxy.vless.Account.reverse:type_name -> xray.proxy.vless.Reverse
1, // [1:1] is the sub-list for method input_type 2, // [2:2] is the sub-list for method output_type
1, // [1:1] is the sub-list for extension type_name 2, // [2:2] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension extendee 2, // [2:2] is the sub-list for extension type_name
0, // [0:1] is the sub-list for field type_name 2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
} }
func init() { file_proxy_vless_account_proto_init() } func init() { file_proxy_vless_account_proto_init() }
+3
View File
@@ -6,8 +6,11 @@ option go_package = "github.com/xtls/xray-core/proxy/vless";
option java_package = "com.xray.proxy.vless"; option java_package = "com.xray.proxy.vless";
option java_multiple_files = true; option java_multiple_files = true;
import "app/proxyman/config.proto";
message Reverse { message Reverse {
string tag = 1; string tag = 1;
xray.app.proxyman.SniffingConfig sniffing = 2;
} }
message Account { message Account {
+16 -4
View File
@@ -97,13 +97,25 @@ func New(ctx context.Context, config *Config) (*Handler, error) {
} }
if a.Reverse != nil { if a.Reverse != nil {
rvsCtx := session.ContextWithInbound(ctx, &session.Inbound{
Tag: a.Reverse.Tag,
User: handler.server.User, // TODO: email
})
if sc := a.Reverse.Sniffing; sc != nil && sc.Enabled {
rvsCtx = session.ContextWithContent(rvsCtx, &session.Content{
SniffingRequest: session.SniffingRequest{
Enabled: sc.Enabled,
OverrideDestinationForProtocol: sc.DestinationOverride,
ExcludeForDomain: sc.DomainsExcluded,
MetadataOnly: sc.MetadataOnly,
RouteOnly: sc.RouteOnly,
},
})
}
handler.reverse = &Reverse{ handler.reverse = &Reverse{
tag: a.Reverse.Tag, tag: a.Reverse.Tag,
dispatcher: v.GetFeature(routing.DispatcherType()).(routing.Dispatcher), dispatcher: v.GetFeature(routing.DispatcherType()).(routing.Dispatcher),
ctx: session.ContextWithInbound(ctx, &session.Inbound{ ctx: rvsCtx,
Tag: a.Reverse.Tag,
User: handler.server.User, // TODO: email
}),
handler: handler, handler: handler,
} }
handler.reverse.monitorTask = &task.Periodic{ handler.reverse.monitorTask = &task.Periodic{