Skip to content

Commit 711aea4

Browse files
authored
XHTTP & WS & HU & gRPC servers: Require sockopt.trustedXForwardedFor (#6309)
#6258 (comment) Behavior: #6258 (comment) Replaces #6159
1 parent 6412738 commit 711aea4

15 files changed

Lines changed: 208 additions & 151 deletions

File tree

common/protocol/http/headers.go

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,41 @@
11
package http
22

33
import (
4+
"context"
45
"net/http"
56
"strconv"
67
"strings"
78

9+
"github.com/xtls/xray-core/common/errors"
810
"github.com/xtls/xray-core/common/net"
911
)
1012

11-
// ParseXForwardedFor parses X-Forwarded-For header in http headers, and return the IP list in it.
12-
func ParseXForwardedFor(header http.Header) []net.Address {
13-
xff := header.Get("X-Forwarded-For")
14-
if xff == "" {
15-
return nil
13+
// ApplyTrustedXForwardedFor returns remoteAddr overridden by X-Forwarded-For only when a configured trusted header is present.
14+
func ApplyTrustedXForwardedFor(header http.Header, trusted []string, remoteAddr net.Addr) net.Addr {
15+
value := header.Get("X-Forwarded-For")
16+
if value == "" {
17+
return remoteAddr
1618
}
17-
list := strings.Split(xff, ",")
18-
addrs := make([]net.Address, 0, len(list))
19-
for _, proxy := range list {
20-
addrs = append(addrs, net.ParseAddress(proxy))
19+
for _, t := range trusted {
20+
if len(header.Values(t)) > 0 {
21+
if idx := strings.IndexByte(value, ','); idx >= 0 {
22+
value = value[:idx]
23+
}
24+
if addr := net.ParseAddress(value); addr.Family().IsIP() {
25+
return &net.TCPAddr{
26+
IP: addr.IP(),
27+
Port: 0,
28+
}
29+
}
30+
return remoteAddr
31+
}
32+
}
33+
if len(trusted) == 0 {
34+
errors.LogWarning(context.Background(), `received "X-Forwarded-For" from `, remoteAddr, ` but "sockopt.trustedXForwardedFor" is not configured; ignoring it and using the real remote address`)
35+
} else {
36+
errors.LogError(context.Background(), `ignored potentially forged "X-Forwarded-For" from `, remoteAddr, `: `, value)
2137
}
22-
return addrs
38+
return remoteAddr
2339
}
2440

2541
// RemoveHopByHopHeaders removes hop by hop headers in http header list.

common/protocol/http/headers_test.go

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,48 @@ package http_test
22

33
import (
44
"bufio"
5+
gonet "net"
56
"net/http"
67
"strings"
78
"testing"
89

9-
"github.com/google/go-cmp/cmp"
1010
"github.com/xtls/xray-core/common"
1111
"github.com/xtls/xray-core/common/net"
1212
. "github.com/xtls/xray-core/common/protocol/http"
1313
)
1414

15-
func TestParseXForwardedFor(t *testing.T) {
16-
header := http.Header{}
17-
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
18-
addrs := ParseXForwardedFor(header)
19-
if r := cmp.Diff(addrs, []net.Address{net.ParseAddress("129.78.138.66"), net.ParseAddress("129.78.64.103")}); r != "" {
20-
t.Error(r)
21-
}
15+
func TestApplyTrustedXForwardedFor(t *testing.T) {
16+
remoteAddr := &gonet.TCPAddr{IP: gonet.ParseIP("127.0.0.1"), Port: 12345}
17+
18+
t.Run("ignore X-Forwarded-For without trusted header", func(t *testing.T) {
19+
header := http.Header{}
20+
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
21+
22+
if addr := ApplyTrustedXForwardedFor(header, nil, remoteAddr); addr != remoteAddr {
23+
t.Fatalf("unexpected remote address: %v", addr)
24+
}
25+
})
26+
27+
t.Run("trust X-Forwarded-For", func(t *testing.T) {
28+
header := http.Header{}
29+
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
30+
header.Add("X-Trusted-CDN", "")
31+
32+
addr := ApplyTrustedXForwardedFor(header, []string{"X-Trusted-CDN"}, remoteAddr)
33+
if addr.String() != "129.78.138.66:0" {
34+
t.Fatalf("unexpected remote address: %v", addr)
35+
}
36+
})
37+
38+
t.Run("ignore non-IP X-Forwarded-For", func(t *testing.T) {
39+
header := http.Header{}
40+
header.Add("X-Forwarded-For", "example.com")
41+
header.Add("X-Trusted-CDN", "")
42+
43+
if addr := ApplyTrustedXForwardedFor(header, []string{"X-Trusted-CDN"}, remoteAddr); addr != remoteAddr {
44+
t.Fatalf("unexpected remote address: %v", addr)
45+
}
46+
})
2247
}
2348

2449
func TestHopByHopHeadersRemoving(t *testing.T) {

infra/conf/xray.go

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -173,27 +173,6 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
173173
return nil, err
174174
}
175175
receiverSettings.StreamSettings = ss
176-
// TODO: Actually implement this breaking change
177-
protocol := ss.GetEffectiveProtocol()
178-
if (protocol == "websocket" || protocol == "httpupgrade" || protocol == "splithttp") &&
179-
(c.StreamSetting.SocketSettings == nil || len(c.StreamSetting.SocketSettings.TrustedXForwardedFor) == 0) {
180-
errors.LogWarning(
181-
context.Background(),
182-
`====== SECURITY WARNING ======`,
183-
"\n",
184-
`inbound "`, c.Tag, `" using `, protocol, ` has not configured "sockopt.trustedXForwardedFor".`,
185-
"\n",
186-
`THIS IS VERY INSECURE!!!`,
187-
"\n",
188-
`For compatibility, Xray still allows this for now and still trusts X-Forwarded-For implicitly.`,
189-
"\n",
190-
`Please configure "sockopt.trustedXForwardedFor" immediately.`,
191-
"\n",
192-
`In future versions, this option must be explicitly set.`,
193-
"\n",
194-
`====== SECURITY WARNING ======`,
195-
)
196-
}
197176
if strings.Contains(ss.SecurityType, "reality") && (receiverSettings.PortList == nil ||
198177
len(receiverSettings.PortList.Ports()) != 1 || receiverSettings.PortList.Ports()[0] != 443) {
199178
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports may get your IP blocked by the GFW`)

transport/internet/grpc/dial.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func dialgRPC(ctx context.Context, dest net.Destination, streamSettings *interne
6262
if err != nil {
6363
return nil, errors.New("Cannot dial gRPC").Base(err)
6464
}
65-
return encoding.NewMultiHunkConn(grpcService, nil), nil
65+
return encoding.NewMultiHunkConn(grpcService, nil, nil), nil
6666
}
6767

6868
errors.LogDebug(ctx, "using gRPC tun mode service name: `"+grpcSettings.getServiceName()+"` stream name: `"+grpcSettings.getTunStreamName()+"`")
@@ -71,7 +71,7 @@ func dialgRPC(ctx context.Context, dest net.Destination, streamSettings *interne
7171
return nil, errors.New("Cannot dial gRPC").Base(err)
7272
}
7373

74-
return encoding.NewHunkConn(grpcService, nil), nil
74+
return encoding.NewHunkConn(grpcService, nil, nil), nil
7575
}
7676

7777
func getGrpcClient(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (*grpc.ClientConn, error) {

transport/internet/grpc/encoding/hunkconn.go

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import (
99
"github.com/xtls/xray-core/common/net"
1010
"github.com/xtls/xray-core/common/net/cnc"
1111
"github.com/xtls/xray-core/common/signal/done"
12-
"google.golang.org/grpc/metadata"
13-
"google.golang.org/grpc/peer"
1412
)
1513

1614
type HunkConn interface {
@@ -38,31 +36,8 @@ func NewHunkReadWriter(hc HunkConn, cancel context.CancelFunc) *HunkReaderWriter
3836
return &HunkReaderWriter{hc, cancel, done.New(), nil, 0}
3937
}
4038

41-
func NewHunkConn(hc HunkConn, cancel context.CancelFunc) net.Conn {
42-
var rAddr net.Addr
43-
pr, ok := peer.FromContext(hc.Context())
44-
if ok {
45-
rAddr = pr.Addr
46-
} else {
47-
rAddr = &net.TCPAddr{
48-
IP: []byte{0, 0, 0, 0},
49-
Port: 0,
50-
}
51-
}
52-
53-
md, ok := metadata.FromIncomingContext(hc.Context())
54-
if ok {
55-
header := md.Get("x-real-ip")
56-
if len(header) > 0 {
57-
realip := net.ParseAddress(header[0])
58-
if realip.Family().IsIP() {
59-
rAddr = &net.TCPAddr{
60-
IP: realip.IP(),
61-
Port: 0,
62-
}
63-
}
64-
}
65-
}
39+
func NewHunkConn(hc HunkConn, cancel context.CancelFunc, trustedXForwardedFor []string) net.Conn {
40+
rAddr := remoteAddrFromContext(hc.Context(), trustedXForwardedFor)
6641
wrc := NewHunkReadWriter(hc, cancel)
6742
return cnc.NewConnection(
6843
cnc.ConnectionInput(wrc),

transport/internet/grpc/encoding/multiconn.go

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@ package encoding
33
import (
44
"context"
55
"io"
6-
"net"
76

87
"github.com/xtls/xray-core/common/buf"
98
"github.com/xtls/xray-core/common/errors"
10-
xnet "github.com/xtls/xray-core/common/net"
9+
"github.com/xtls/xray-core/common/net"
1110
"github.com/xtls/xray-core/common/net/cnc"
1211
"github.com/xtls/xray-core/common/signal/done"
13-
"google.golang.org/grpc/metadata"
14-
"google.golang.org/grpc/peer"
1512
)
1613

1714
type MultiHunkConn interface {
@@ -34,31 +31,8 @@ func NewMultiHunkReadWriter(hc MultiHunkConn, cancel context.CancelFunc) *MultiH
3431
return &MultiHunkReaderWriter{hc, cancel, done.New(), nil}
3532
}
3633

37-
func NewMultiHunkConn(hc MultiHunkConn, cancel context.CancelFunc) net.Conn {
38-
var rAddr net.Addr
39-
pr, ok := peer.FromContext(hc.Context())
40-
if ok {
41-
rAddr = pr.Addr
42-
} else {
43-
rAddr = &net.TCPAddr{
44-
IP: []byte{0, 0, 0, 0},
45-
Port: 0,
46-
}
47-
}
48-
49-
md, ok := metadata.FromIncomingContext(hc.Context())
50-
if ok {
51-
header := md.Get("x-real-ip")
52-
if len(header) > 0 {
53-
realip := xnet.ParseAddress(header[0])
54-
if realip.Family().IsIP() {
55-
rAddr = &net.TCPAddr{
56-
IP: realip.IP(),
57-
Port: 0,
58-
}
59-
}
60-
}
61-
}
34+
func NewMultiHunkConn(hc MultiHunkConn, cancel context.CancelFunc, trustedXForwardedFor []string) net.Conn {
35+
rAddr := remoteAddrFromContext(hc.Context(), trustedXForwardedFor)
6236
wrc := NewMultiHunkReadWriter(hc, cancel)
6337
return cnc.NewConnection(
6438
cnc.ConnectionInputMulti(wrc),
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package encoding
2+
3+
import (
4+
"context"
5+
"strings"
6+
7+
"github.com/xtls/xray-core/common/errors"
8+
"github.com/xtls/xray-core/common/net"
9+
"google.golang.org/grpc/metadata"
10+
"google.golang.org/grpc/peer"
11+
)
12+
13+
func remoteAddrFromContext(ctx context.Context, trusted []string) net.Addr {
14+
var remoteAddr net.Addr
15+
if pr, ok := peer.FromContext(ctx); ok {
16+
remoteAddr = pr.Addr
17+
} else {
18+
remoteAddr = &net.TCPAddr{
19+
IP: []byte{0, 0, 0, 0},
20+
Port: 0,
21+
}
22+
}
23+
24+
md, ok := metadata.FromIncomingContext(ctx)
25+
if !ok {
26+
return remoteAddr
27+
}
28+
29+
if forwardedAddr := parseTrustedXForwardedFor(md, trusted, remoteAddr); forwardedAddr != nil && forwardedAddr.Family().IsIP() {
30+
remoteAddr = &net.TCPAddr{
31+
IP: forwardedAddr.IP(),
32+
Port: 0,
33+
}
34+
}
35+
return remoteAddr
36+
}
37+
38+
func parseTrustedXForwardedFor(md metadata.MD, trusted []string, remoteAddr net.Addr) net.Address {
39+
values := md.Get("X-Forwarded-For")
40+
if len(values) == 0 || values[0] == "" {
41+
return nil
42+
}
43+
value := values[0]
44+
for _, t := range trusted {
45+
if len(md.Get(t)) > 0 {
46+
if idx := strings.IndexByte(value, ','); idx >= 0 {
47+
value = value[:idx]
48+
}
49+
return net.ParseAddress(value)
50+
}
51+
}
52+
if len(trusted) == 0 {
53+
errors.LogWarning(context.Background(), `received "X-Forwarded-For" from `, remoteAddr, ` but "sockopt.trustedXForwardedFor" is not configured; ignoring it and using the real remote address`)
54+
} else {
55+
errors.LogError(context.Background(), `ignored potentially forged "X-Forwarded-For" from `, remoteAddr, `: `, value)
56+
}
57+
return nil
58+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package encoding
2+
3+
import (
4+
"context"
5+
"net"
6+
"testing"
7+
8+
"google.golang.org/grpc/metadata"
9+
"google.golang.org/grpc/peer"
10+
)
11+
12+
func TestRemoteAddrFromContext(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
metadata metadata.MD
16+
trustedXForwardedFor []string
17+
expectedRemoteAddress string
18+
}{
19+
{
20+
name: "trust X-Forwarded-For when configured",
21+
metadata: metadata.Pairs("X-Forwarded-For", "2.2.2.2, 3.3.3.3"),
22+
trustedXForwardedFor: []string{"X-Forwarded-For"},
23+
expectedRemoteAddress: "2.2.2.2:0",
24+
},
25+
{
26+
name: "trust X-Forwarded-For with trusted marker",
27+
metadata: metadata.Pairs("X-Forwarded-For", "4.4.4.4", "X-Trusted-CDN", "1"),
28+
trustedXForwardedFor: []string{"X-Trusted-CDN"},
29+
expectedRemoteAddress: "4.4.4.4:0",
30+
},
31+
{
32+
name: "ignore X-Forwarded-For without trusted marker",
33+
metadata: metadata.Pairs("X-Forwarded-For", "5.5.5.5"),
34+
trustedXForwardedFor: []string{"X-Trusted-CDN"},
35+
expectedRemoteAddress: "127.0.0.1:12345",
36+
},
37+
}
38+
39+
for _, test := range tests {
40+
t.Run(test.name, func(t *testing.T) {
41+
ctx := peer.NewContext(metadata.NewIncomingContext(context.Background(), test.metadata), &peer.Peer{
42+
Addr: &net.TCPAddr{
43+
IP: net.ParseIP("127.0.0.1"),
44+
Port: 12345,
45+
},
46+
})
47+
remoteAddr := remoteAddrFromContext(ctx, test.trustedXForwardedFor)
48+
if remoteAddr.String() != test.expectedRemoteAddress {
49+
t.Fatalf("unexpected remote address: %s", remoteAddr.String())
50+
}
51+
})
52+
}
53+
}

0 commit comments

Comments
 (0)