diff --git a/infra/conf/freedom.go b/infra/conf/freedom.go index 8e6abe8d9..8c93152bc 100644 --- a/infra/conf/freedom.go +++ b/infra/conf/freedom.go @@ -185,7 +185,7 @@ func (c *FreedomConfig) Build() (proto.Message, error) { } if c.ProxyProtocol > 0 && c.ProxyProtocol <= 2 { - config.ProxyProtocol = c.ProxyProtocol + errors.PrintDeprecatedFeatureWarning(`"proxyProtocol" in Freedom`, `"proxy-protocol" in "streamSettings.finalmask.tcp"`) } firewall := c.Firewall diff --git a/infra/conf/transport_finalmask.go b/infra/conf/transport_finalmask.go index 6f3292313..bf669854f 100644 --- a/infra/conf/transport_finalmask.go +++ b/infra/conf/transport_finalmask.go @@ -18,6 +18,7 @@ import ( "github.com/xtls/xray-core/common/geodata" xlua "github.com/xtls/xray-core/common/lua" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/finalmask/filter" "github.com/xtls/xray-core/transport/internet/finalmask/fragment" @@ -26,6 +27,7 @@ import ( "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/header" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original" "github.com/xtls/xray-core/transport/internet/finalmask/noise" + "github.com/xtls/xray-core/transport/internet/finalmask/proxyprotocol" "github.com/xtls/xray-core/transport/internet/finalmask/realm" "github.com/xtls/xray-core/transport/internet/finalmask/salamander" "github.com/xtls/xray-core/transport/internet/finalmask/sudoku" @@ -76,11 +78,12 @@ var ( customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ - "filter": func() interface{} { return new(Filter) }, - "header-custom": func() interface{} { return new(HeaderCustomTCP) }, - "fragment": func() interface{} { return new(FragmentMask) }, - "sudoku": func() interface{} { return new(Sudoku) }, - "xmc": func() interface{} { return new(XMC) }, + "filter": func() interface{} { return new(Filter) }, + "header-custom": func() interface{} { return new(HeaderCustomTCP) }, + "proxy-protocol": func() interface{} { return new(ProxyProtocol) }, + "fragment": func() interface{} { return new(FragmentMask) }, + "sudoku": func() interface{} { return new(Sudoku) }, + "xmc": func() interface{} { return new(XMC) }, }, "type", "settings") udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ @@ -97,6 +100,32 @@ var ( }, "type", "settings") ) +type ProxyProtocol struct { + Version uint32 `json:"version"` +} + +func (c *ProxyProtocol) Build() (proto.Message, error) { + version := c.Version + if version == 0 { + version = 2 + } + if version != 1 && version != 2 { + return nil, errors.New("PROXY protocol version must be 1 or 2") + } + return &proxyprotocol.Config{Version: version}, nil +} + +func addProxyProtocolMask(config *internet.StreamConfig, version uint32) { + mask := serial.ToTypedMessage(&proxyprotocol.Config{Version: version}) + for _, existing := range config.Tcpmasks { + if existing.Type == mask.Type { + return + } + } + // Put the header directly on the socket, before the other masks. + config.Tcpmasks = append(config.Tcpmasks, mask) +} + type Filter struct { BlockedSourceIPs []string `json:"blockedSourceIPs"` AllowedSourceIPs []string `json:"allowedSourceIPs"` diff --git a/infra/conf/xray.go b/infra/conf/xray.go index 2c94ce55c..cabb94957 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -408,6 +408,12 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) { } if fc, ok := ts.(*freedom.Config); ok { + if legacy := rawConfig.(*FreedomConfig); legacy.ProxyProtocol > 0 && legacy.ProxyProtocol <= 2 { + if senderSettings.StreamSettings == nil { + senderSettings.StreamSettings = &internet.StreamConfig{} + } + addProxyProtocolMask(senderSettings.StreamSettings, legacy.ProxyProtocol) + } if senderSettings.StreamSettings != nil && senderSettings.StreamSettings.SocketSettings != nil && senderSettings.StreamSettings.SocketSettings.AddressPortStrategy != internet.AddressPortStrategy_None { diff --git a/proxy/freedom/config.pb.go b/proxy/freedom/config.pb.go index 8fb882b50..8e555770d 100644 --- a/proxy/freedom/config.pb.go +++ b/proxy/freedom/config.pb.go @@ -442,7 +442,6 @@ type Config struct { DestinationOverride *DestinationOverride `protobuf:"bytes,3,opt,name=destination_override,json=destinationOverride,proto3" json:"destination_override,omitempty"` UserLevel uint32 `protobuf:"varint,4,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` Fragment *Fragment `protobuf:"bytes,5,opt,name=fragment,proto3" json:"fragment,omitempty"` - ProxyProtocol uint32 `protobuf:"varint,6,opt,name=proxy_protocol,json=proxyProtocol,proto3" json:"proxy_protocol,omitempty"` Noises []*Noise `protobuf:"bytes,7,rep,name=noises,proto3" json:"noises,omitempty"` Firewall []*FirewallConfig `protobuf:"bytes,8,rep,name=firewall,proto3" json:"firewall,omitempty"` unknownFields protoimpl.UnknownFields @@ -507,13 +506,6 @@ func (x *Config) GetFragment() *Fragment { return nil } -func (x *Config) GetProxyProtocol() uint32 { - if x != nil { - return x.ProxyProtocol - } - return 0 -} - func (x *Config) GetNoises() []*Noise { if x != nil { return x.Noises @@ -567,16 +559,15 @@ const file_proxy_freedom_config_proto_rawDesc = "" + "\x05delay\x18\x05 \x01(\v2\x19.xray.proxy.freedom.RangeR\x05delay\x12;\n" + "\tlog_level\x18\x06 \x01(\x0e2\x19.xray.common.log.SeverityH\x00R\blogLevel\x88\x01\x01B\f\n" + "\n" + - "_log_level\"\xa9\x03\n" + + "_log_level\"\x88\x03\n" + "\x06Config\x12P\n" + "\x0fdomain_strategy\x18\x01 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0edomainStrategy\x12Z\n" + "\x14destination_override\x18\x03 \x01(\v2'.xray.proxy.freedom.DestinationOverrideR\x13destinationOverride\x12\x1d\n" + "\n" + "user_level\x18\x04 \x01(\rR\tuserLevel\x128\n" + - "\bfragment\x18\x05 \x01(\v2\x1c.xray.proxy.freedom.FragmentR\bfragment\x12%\n" + - "\x0eproxy_protocol\x18\x06 \x01(\rR\rproxyProtocol\x121\n" + + "\bfragment\x18\x05 \x01(\v2\x1c.xray.proxy.freedom.FragmentR\bfragment\x121\n" + "\x06noises\x18\a \x03(\v2\x19.xray.proxy.freedom.NoiseR\x06noises\x12>\n" + - "\bfirewall\x18\b \x03(\v2\".xray.proxy.freedom.FirewallConfigR\bfirewall*\"\n" + + "\bfirewall\x18\b \x03(\v2\".xray.proxy.freedom.FirewallConfigR\bfirewallJ\x04\b\x06\x10\a*\"\n" + "\n" + "RuleAction\x12\t\n" + "\x05Allow\x10\x00\x12\t\n" + diff --git a/proxy/freedom/config.proto b/proxy/freedom/config.proto index 7770b77f2..3b71e06df 100644 --- a/proxy/freedom/config.proto +++ b/proxy/freedom/config.proto @@ -57,11 +57,11 @@ message FirewallConfig { } message Config { + reserved 6; xray.transport.internet.DomainStrategy domain_strategy = 1; DestinationOverride destination_override = 3; uint32 user_level = 4; Fragment fragment = 5; - uint32 proxy_protocol = 6; repeated Noise noises = 7; repeated FirewallConfig firewall = 8; } diff --git a/proxy/freedom/freedom.go b/proxy/freedom/freedom.go index bdd4670f9..88151b9c9 100644 --- a/proxy/freedom/freedom.go +++ b/proxy/freedom/freedom.go @@ -8,7 +8,6 @@ import ( "sync/atomic" "time" - "github.com/pires/go-proxyproto" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/crypto" @@ -386,16 +385,6 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte } } - if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 { - version := byte(h.config.ProxyProtocol) - srcAddr := inbound.Source.RawNetAddr() - dstAddr := conn.RemoteAddr() - header := proxyproto.HeaderProxyFromAddrs(version, srcAddr, dstAddr) - if _, err = header.WriteTo(conn); err != nil { - conn.Close() - return errors.New("failed to set PROXY protocol v", version).Base(err) - } - } defer conn.Close() errors.LogInfo(ctx, "connection opened to ", destination, ", local endpoint ", conn.LocalAddr(), ", remote endpoint ", conn.RemoteAddr()) diff --git a/transport/internet/finalmask/finalmask.go b/transport/internet/finalmask/finalmask.go index a64d4183a..617d7fa2e 100644 --- a/transport/internet/finalmask/finalmask.go +++ b/transport/internet/finalmask/finalmask.go @@ -9,9 +9,11 @@ import ( "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/session" ) type Dialer struct { + Source net.Addr DialTCP func(net.Destination) (net.Conn, error) DialUDP func(net.Destination) (net.Conn, error) } @@ -85,6 +87,9 @@ func (fm *FinalMask) DialTCP(ctx context.Context, dest net.Destination) (net.Con return &net.PacketConnWrapper{PacketConn: conn, Dest: addr}, err }, } + if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.IsValid() { + dialer.Source = inbound.Source.RawNetAddr() + } for i := range fm.tcpMasks { var newConn net.Conn newConn, err = fm.tcpMasks[i].WrapConnClient(conn, &dest, dialer) diff --git a/transport/internet/finalmask/proxyprotocol/config.go b/transport/internet/finalmask/proxyprotocol/config.go new file mode 100644 index 000000000..d8c179eec --- /dev/null +++ b/transport/internet/finalmask/proxyprotocol/config.go @@ -0,0 +1,31 @@ +package proxyprotocol + +import ( + "github.com/pires/go-proxyproto" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet/finalmask" +) + +func (c *Config) WrapConnClient(conn net.Conn, dest *net.Destination, dialer *finalmask.Dialer) (net.Conn, error) { + version := c.Version + if version == 0 { + version = 2 + } + if version != 1 && version != 2 { + return nil, errors.New("PROXY protocol version must be 1 or 2") + } + source := conn.LocalAddr() + if dialer != nil && dialer.Source != nil { + source = dialer.Source + } + header := proxyproto.HeaderProxyFromAddrs(byte(version), source, conn.RemoteAddr()) + if _, err := header.WriteTo(conn); err != nil { + return nil, errors.New("failed to write PROXY protocol header").Base(err) + } + return conn, nil +} + +func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) { + return conn, nil +} diff --git a/transport/internet/finalmask/proxyprotocol/config.pb.go b/transport/internet/finalmask/proxyprotocol/config.pb.go new file mode 100644 index 000000000..c765d3b9f --- /dev/null +++ b/transport/internet/finalmask/proxyprotocol/config.pb.go @@ -0,0 +1,123 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc (unknown) +// source: transport/internet/finalmask/proxyprotocol/config.proto + +package proxyprotocol + +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 uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_proxyprotocol_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_finalmask_proxyprotocol_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_finalmask_proxyprotocol_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +var File_transport_internet_finalmask_proxyprotocol_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_proxyprotocol_config_proto_rawDesc = "" + + "\n" + + "7transport/internet/finalmask/proxyprotocol/config.proto\x12/xray.transport.internet.finalmask.proxyprotocol\"\"\n" + + "\x06Config\x12\x18\n" + + "\aversion\x18\x01 \x01(\rR\aversionB\xaf\x01\n" + + "3com.xray.transport.internet.finalmask.proxyprotocolP\x01ZDgithub.com/xtls/xray-core/transport/internet/finalmask/proxyprotocol\xaa\x02/Xray.Transport.Internet.Finalmask.ProxyProtocolb\x06proto3" + +var ( + file_transport_internet_finalmask_proxyprotocol_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_proxyprotocol_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_proxyprotocol_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_proxyprotocol_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_proxyprotocol_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_proxyprotocol_config_proto_rawDesc), len(file_transport_internet_finalmask_proxyprotocol_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_proxyprotocol_config_proto_rawDescData +} + +var file_transport_internet_finalmask_proxyprotocol_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_proxyprotocol_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.proxyprotocol.Config +} +var file_transport_internet_finalmask_proxyprotocol_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_finalmask_proxyprotocol_config_proto_init() } +func file_transport_internet_finalmask_proxyprotocol_config_proto_init() { + if File_transport_internet_finalmask_proxyprotocol_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_finalmask_proxyprotocol_config_proto_rawDesc), len(file_transport_internet_finalmask_proxyprotocol_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_proxyprotocol_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_proxyprotocol_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_proxyprotocol_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_proxyprotocol_config_proto = out.File + file_transport_internet_finalmask_proxyprotocol_config_proto_goTypes = nil + file_transport_internet_finalmask_proxyprotocol_config_proto_depIdxs = nil +} diff --git a/transport/internet/finalmask/proxyprotocol/config.proto b/transport/internet/finalmask/proxyprotocol/config.proto new file mode 100644 index 000000000..1b1030179 --- /dev/null +++ b/transport/internet/finalmask/proxyprotocol/config.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.proxyprotocol; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.ProxyProtocol"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/proxyprotocol"; +option java_package = "com.xray.transport.internet.finalmask.proxyprotocol"; +option java_multiple_files = true; + +message Config { + uint32 version = 1; +} diff --git a/transport/internet/finalmask/proxyprotocol/config_test.go b/transport/internet/finalmask/proxyprotocol/config_test.go new file mode 100644 index 000000000..837c19a9b --- /dev/null +++ b/transport/internet/finalmask/proxyprotocol/config_test.go @@ -0,0 +1,84 @@ +package proxyprotocol + +import ( + "context" + "fmt" + "io" + "testing" + "time" + + "github.com/pires/go-proxyproto" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/transport/internet/finalmask" +) + +func TestClientHeader(t *testing.T) { + for _, version := range []uint32{0, 1, 2} { + for _, sourceIP := range []string{"", "198.51.100.7", "2001:db8::7"} { + t.Run(fmt.Sprintf("version=%d/source=%s", version, sourceIP), func(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer listener.Close() + ctx := context.Background() + if sourceIP != "" { + ctx = session.ContextWithInbound(ctx, &session.Inbound{ + Source: net.TCPDestination(net.ParseAddress(sourceIP), 12345), + }) + } + fm := finalmask.NewFinalMask([]finalmask.TCPMask{&Config{Version: version}}, nil, + func(ctx context.Context, dest net.Destination) (net.Conn, error) { + return net.Dial("tcp", dest.NetAddr()) + }, nil, nil, nil) + client, err := fm.DialTCP(ctx, net.DestinationFromAddr(listener.Addr())) + if err != nil { + t.Fatal(err) + } + defer client.Close() + raw, err := listener.Accept() + if err != nil { + t.Fatal(err) + } + server := proxyproto.NewConn(raw, proxyproto.WithPolicy(proxyproto.REQUIRE)) + defer server.Close() + deadline := time.Now().Add(5 * time.Second) + if err := client.SetDeadline(deadline); err != nil { + t.Fatal(err) + } + if err := server.SetDeadline(deadline); err != nil { + t.Fatal(err) + } + header := server.ProxyHeader() + wantVersion := byte(version) + if wantVersion == 0 { + wantVersion = 2 + } + if header == nil || header.Version != wantVersion || header.Command != proxyproto.PROXY { + t.Fatalf("unexpected PROXY header: %v", header) + } + wantSource := client.LocalAddr() + if sourceIP != "" { + wantSource = &net.TCPAddr{IP: net.ParseIP(sourceIP), Port: 12345} + } + if server.RemoteAddr().String() != wantSource.String() || server.LocalAddr().String() != listener.Addr().String() { + t.Fatalf("addresses = %v -> %v, want %v -> %v", server.RemoteAddr(), server.LocalAddr(), wantSource, listener.Addr()) + } + for _, pair := range [][2]net.Conn{{client, server}, {client, server}, {server, client}} { + const payload = "application payload" + if _, err := io.WriteString(pair[0], payload); err != nil { + t.Fatal(err) + } + data := make([]byte, len(payload)) + if _, err := io.ReadFull(pair[1], data); err != nil { + t.Fatal(err) + } + if string(data) != payload { + t.Fatalf("payload = %q", data) + } + } + }) + } + } +}