Skip to content

Commit 757544f

Browse files
authored
Extend realip interceptors with ip selection based on proxy count and list (#695)
* Extend realip interceptors with ip selection based on proxy count and list The rightmost IP is not always the client IP. One example is Google: https://cloud.google.com/load-balancing/docs/https#x-forwarded-for_header The PR extends the IP selection for `X-Forwarded-For` based on [MDN Selecting an IP address](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#selecting_an_ip_address). Just so you know, it is possible to configure both at the same time. The user needs to be cautious when configuring these for IP selection and preferably pick `TrustedProxies` or `TrustedProxiesCount`. * Use functional options to configure the interceptor * Fix linter
1 parent 3782759 commit 757544f

File tree

5 files changed

+252
-28
lines changed

5 files changed

+252
-28
lines changed

interceptors/realip/doc.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,33 @@ the real IP from the request headers. If the peer address is not found to be
3636
within one of the trusted networks, the peer address will be returned as the
3737
real IP.
3838
39-
"trusted" in this context means that the peer is configured to overwrite the
39+
"trusted peer" in this context means that the peer is configured to overwrite the
4040
header value with the real IP. This is typically done by a proxy or load
4141
balancer that is configured to forward the real IP of the client in a header
4242
value. Alternatively, the peer may be configured to append the real IP to the
4343
header value. In this case, the middleware will use the last, rightmost, IP
4444
address in the header as the real IP. Most load balancers, such as NGINX, AWS
45-
ELB, and Google Cloud Load Balancer, are configured to append the real IP to
46-
the header value as their default action.
45+
ELB, are configured to append the real IP to the header value as their default action.
46+
However, Google Cloud Load Balancer for `X-Forwarded-For` follows the pattern:
47+
`<client-ip>,<load-balancer-ip>`. Hence we need to have an ability to exact the
48+
real ip from the header ignoring the LB/proxy IPs.
4749
48-
To mitigate the risk of a denial of service by proxy of a malicious header,
49-
the middleware validates that the header value contains a valid IP address. Only
50-
if a valid IP address is found will the middleware use that value as the real
51-
IP.
50+
### Supported Methods for Extracting Real IP:
51+
52+
This is based on
53+
[Selecting an IP address](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#selecting_an_ip_address).
54+
55+
1. Trusted Proxy Count
56+
57+
With this method, the count of reverse proxies between the internet and the server is configured.
58+
The middleware searches the `X-Forwarded-For` IP list from the rightmost by that count.
59+
60+
2. Trusted Proxy List
61+
62+
Alternatively, you can configure a list of trusted reverse proxies by specifying their
63+
IPs or IP ranges. The middleware will then search the `X-Forwarded-For` IP list from
64+
the rightmost, skipping all addresses that are on the trusted proxy list.
65+
The first non-matching address is considered the target address.
5266
5367
# Individual IP addresses as trusted peers
5468

interceptors/realip/examples_test.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,49 @@ import (
1111
)
1212

1313
// Simple example of a unary server initialization code.
14-
func ExampleUnaryServerInterceptor() {
14+
func ExampleUnaryServerInterceptorOpts() {
1515
// Define list of trusted peers from which we accept forwarded-for and
1616
// real-ip headers.
1717
trustedPeers := []netip.Prefix{
1818
netip.MustParsePrefix("127.0.0.1/32"),
1919
}
2020
// Define headers to look for in the incoming request.
2121
headers := []string{realip.XForwardedFor, realip.XRealIp}
22+
// Consider that there is one proxy in front,
23+
// so the real client ip will be rightmost - 1 in the csv list of X-Forwarded-For
24+
// Optionally you can specify TrustedProxies
25+
opts := []realip.Option{
26+
realip.WithTrustedPeers(trustedPeers),
27+
realip.WithHeaders(headers),
28+
realip.WithTrustedProxiesCount(1),
29+
}
2230
_ = grpc.NewServer(
2331
grpc.ChainUnaryInterceptor(
24-
realip.UnaryServerInterceptor(trustedPeers, headers),
32+
realip.UnaryServerInterceptorOpts(opts...),
2533
),
2634
)
2735
}
2836

2937
// Simple example of a streaming server initialization code.
30-
func ExampleStreamServerInterceptor() {
38+
func ExampleStreamServerInterceptorOpts() {
3139
// Define list of trusted peers from which we accept forwarded-for and
3240
// real-ip headers.
3341
trustedPeers := []netip.Prefix{
3442
netip.MustParsePrefix("127.0.0.1/32"),
3543
}
3644
// Define headers to look for in the incoming request.
3745
headers := []string{realip.XForwardedFor, realip.XRealIp}
46+
// Consider that there is one proxy in front,
47+
// so the real client ip will be rightmost - 1 in the csv list of X-Forwarded-For
48+
// Optionally you can specify TrustedProxies
49+
opts := []realip.Option{
50+
realip.WithTrustedPeers(trustedPeers),
51+
realip.WithHeaders(headers),
52+
realip.WithTrustedProxiesCount(1),
53+
}
3854
_ = grpc.NewServer(
3955
grpc.ChainStreamInterceptor(
40-
realip.StreamServerInterceptor(trustedPeers, headers),
56+
realip.StreamServerInterceptorOpts(opts...),
4157
),
4258
)
4359
}

interceptors/realip/options.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) The go-grpc-middleware Authors.
2+
// Licensed under the Apache License 2.0.
3+
4+
package realip
5+
6+
import "net/netip"
7+
8+
// options represents the configuration options for the realip middleware.
9+
type options struct {
10+
// trustedPeers is a list of trusted peers network prefixes.
11+
trustedPeers []netip.Prefix
12+
// trustedProxies is a list of trusted proxies network prefixes.
13+
// The first rightmost non-matching IP when going through X-Forwarded-For is considered the client IP.
14+
trustedProxies []netip.Prefix
15+
// trustedProxiesCount specifies the number of proxies in front that may append X-Forwarded-For.
16+
// It defaults to 0.
17+
trustedProxiesCount uint
18+
// headers specifies the headers to use in real IP extraction when the request is from a trusted peer.
19+
headers []string
20+
}
21+
22+
// An Option lets you add options to realip interceptors using With* functions.
23+
type Option func(*options)
24+
25+
func evaluateOpts(opts []Option) *options {
26+
optCopy := &options{}
27+
for _, o := range opts {
28+
o(optCopy)
29+
}
30+
return optCopy
31+
}
32+
33+
// WithTrustedPeers sets the trusted peers network prefixes.
34+
func WithTrustedPeers(peers []netip.Prefix) Option {
35+
return func(o *options) {
36+
o.trustedPeers = peers
37+
}
38+
}
39+
40+
// WithTrustedProxies sets the trusted proxies network prefixes.
41+
func WithTrustedProxies(proxies []netip.Prefix) Option {
42+
return func(o *options) {
43+
o.trustedProxies = proxies
44+
}
45+
}
46+
47+
// WithTrustedProxiesCount sets the number of trusted proxies that may append X-Forwarded-For.
48+
func WithTrustedProxiesCount(count uint) Option {
49+
return func(o *options) {
50+
o.trustedProxiesCount = count
51+
}
52+
}
53+
54+
// WithHeaders sets the headers to use in real IP extraction for requests from trusted peers.
55+
func WithHeaders(headers []string) Option {
56+
return func(o *options) {
57+
o.headers = headers
58+
}
59+
}

interceptors/realip/realip.go

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,32 @@ func getHeader(ctx context.Context, key string) string {
6565
return md[strings.ToLower(key)][0]
6666
}
6767

68-
func ipFromHeaders(ctx context.Context, headers []string) netip.Addr {
68+
func ipFromXForwardedFoR(trustedProxies []netip.Prefix, ips []string, idx int) netip.Addr {
69+
for i := idx; i >= 0; i-- {
70+
h := strings.TrimSpace(ips[i])
71+
ip, err := netip.ParseAddr(h)
72+
if err != nil {
73+
return noIP
74+
}
75+
if !ipInNets(ip, trustedProxies) {
76+
return ip
77+
}
78+
}
79+
return noIP
80+
}
81+
82+
func ipFromHeaders(ctx context.Context, headers []string, trustedProxies []netip.Prefix, trustedProxyCnt uint) netip.Addr {
6983
for _, header := range headers {
7084
a := strings.Split(getHeader(ctx, header), ",")
71-
h := strings.TrimSpace(a[len(a)-1])
85+
idx := len(a) - 1
86+
if header == XForwardedFor {
87+
idx = idx - int(trustedProxyCnt)
88+
if idx < 0 {
89+
continue
90+
}
91+
return ipFromXForwardedFoR(trustedProxies, a, idx)
92+
}
93+
h := strings.TrimSpace(a[idx])
7294
ip, err := netip.ParseAddr(h)
7395
if err == nil {
7496
return ip
@@ -77,7 +99,7 @@ func ipFromHeaders(ctx context.Context, headers []string) netip.Addr {
7799
return noIP
78100
}
79101

80-
func getRemoteIP(ctx context.Context, trustedPeers []netip.Prefix, headers []string) netip.Addr {
102+
func getRemoteIP(ctx context.Context, trustedPeers, trustedProxies []netip.Prefix, headers []string, proxyCnt uint) netip.Addr {
81103
pr := remotePeer(ctx)
82104
if pr == nil {
83105
return noIP
@@ -92,7 +114,7 @@ func getRemoteIP(ctx context.Context, trustedPeers []netip.Prefix, headers []str
92114
if len(trustedPeers) == 0 || !ipInNets(ip, trustedPeers) {
93115
return ip
94116
}
95-
if ip := ipFromHeaders(ctx, headers); ip != noIP {
117+
if ip := ipFromHeaders(ctx, headers, trustedProxies, proxyCnt); ip != noIP {
96118
return ip
97119
}
98120
// No ip from the headers, return the peer ip.
@@ -111,22 +133,42 @@ func (s *serverStream) Context() context.Context {
111133
// UnaryServerInterceptor returns a new unary server interceptor that extracts the real client IP from request headers.
112134
// It checks if the request comes from a trusted peer, and if so, extracts the IP from the configured headers.
113135
// The real IP is added to the request context.
136+
// See UnaryServerInterceptorOpts as it allows to configure trusted proxy ips list and count that should work better with Google LB
114137
func UnaryServerInterceptor(trustedPeers []netip.Prefix, headers []string) grpc.UnaryServerInterceptor {
138+
return UnaryServerInterceptorOpts(WithTrustedPeers(trustedPeers), WithHeaders(headers))
139+
}
140+
141+
// StreamServerInterceptor returns a new stream server interceptor that extracts the real client IP from request headers.
142+
// It checks if the request comes from a trusted peer, and if so, extracts the IP from the configured headers.
143+
// The real IP is added to the request context.
144+
// See UnaryServerInterceptorOpts as it allows to configure trusted proxy ips list and count that should work better with Google LB
145+
func StreamServerInterceptor(trustedPeers []netip.Prefix, headers []string) grpc.StreamServerInterceptor {
146+
return StreamServerInterceptorOpts(WithTrustedPeers(trustedPeers), WithHeaders(headers))
147+
}
148+
149+
// UnaryServerInterceptorOpts returns a new unary server interceptor that extracts the real client IP from request headers.
150+
// It checks if the request comes from a trusted peer, validates headers against trusted proxies list and trusted proxies count
151+
// then it extracts the IP from the configured headers.
152+
// The real IP is added to the request context.
153+
func UnaryServerInterceptorOpts(opts ...Option) grpc.UnaryServerInterceptor {
154+
o := evaluateOpts(opts)
115155
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
116-
ip := getRemoteIP(ctx, trustedPeers, headers)
156+
ip := getRemoteIP(ctx, o.trustedPeers, o.trustedProxies, o.headers, o.trustedProxiesCount)
117157
if ip != noIP {
118158
ctx = context.WithValue(ctx, realipKey{}, ip)
119159
}
120160
return handler(ctx, req)
121161
}
122162
}
123163

124-
// StreamServerInterceptor returns a new stream server interceptor that extracts the real client IP from request headers.
125-
// It checks if the request comes from a trusted peer, and if so, extracts the IP from the configured headers.
164+
// StreamServerInterceptorOpts returns a new stream server interceptor that extracts the real client IP from request headers.
165+
// It checks if the request comes from a trusted peer, validates headers against trusted proxies list and trusted proxies count
166+
// then it extracts the IP from the configured headers.
126167
// The real IP is added to the request context.
127-
func StreamServerInterceptor(trustedPeers []netip.Prefix, headers []string) grpc.StreamServerInterceptor {
168+
func StreamServerInterceptorOpts(opts ...Option) grpc.StreamServerInterceptor {
169+
o := evaluateOpts(opts)
128170
return func(srv any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
129-
ip := getRemoteIP(stream.Context(), trustedPeers, headers)
171+
ip := getRemoteIP(stream.Context(), o.trustedPeers, o.trustedProxies, o.headers, o.trustedProxiesCount)
130172
if ip != noIP {
131173
return handler(srv, &serverStream{
132174
ServerStream: stream,

interceptors/realip/realip_test.go

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,26 @@ func private6Peer() *peer.Peer {
7878
}
7979

8080
type testCase struct {
81-
trustedPeers []netip.Prefix
82-
headerKeys []string
83-
inputHeaders map[string]string
84-
peer *peer.Peer
85-
expectedIP netip.Addr
81+
trustedPeers []netip.Prefix
82+
trustedProxies []netip.Prefix
83+
proxiesCount uint
84+
headerKeys []string
85+
inputHeaders map[string]string
86+
peer *peer.Peer
87+
expectedIP netip.Addr
88+
}
89+
90+
func (c testCase) optsFromTesCase() []Option {
91+
return []Option{
92+
WithTrustedPeers(c.trustedPeers),
93+
WithTrustedProxies(c.trustedProxies),
94+
WithTrustedProxiesCount(c.proxiesCount),
95+
WithHeaders(c.headerKeys),
96+
}
8697
}
8798

8899
func testUnaryServerInterceptor(t *testing.T, c testCase) {
89-
interceptor := UnaryServerInterceptor(c.trustedPeers, c.headerKeys)
100+
interceptor := UnaryServerInterceptorOpts(c.optsFromTesCase()...)
90101
handler := func(ctx context.Context, req any) (any, error) {
91102
ip, _ := FromContext(ctx)
92103

@@ -111,7 +122,7 @@ func testUnaryServerInterceptor(t *testing.T, c testCase) {
111122
}
112123

113124
func testStreamServerInterceptor(t *testing.T, c testCase) {
114-
interceptor := StreamServerInterceptor(c.trustedPeers, c.headerKeys)
125+
interceptor := StreamServerInterceptorOpts(c.optsFromTesCase()...)
115126
handler := func(srv any, stream grpc.ServerStream) error {
116127
ip, _ := FromContext(stream.Context())
117128

@@ -153,7 +164,6 @@ func TestInterceptor(t *testing.T) {
153164
testStreamServerInterceptor(t, tc)
154165
})
155166
})
156-
157167
t.Run("trusted peer header csv", func(t *testing.T) {
158168
tc := testCase{
159169
// Test that if the remote peer is trusted and the header contains
@@ -173,6 +183,89 @@ func TestInterceptor(t *testing.T) {
173183
testStreamServerInterceptor(t, tc)
174184
})
175185
})
186+
t.Run("trusted proxy list with XForwardedFor", func(t *testing.T) {
187+
tc := testCase{
188+
// Test that if the remote peer is trusted and the header contains
189+
// a comma separated list of valid IPs,
190+
// we get the first going from right to left that is not in local net
191+
trustedPeers: localnet,
192+
trustedProxies: localnet,
193+
headerKeys: []string{XForwardedFor},
194+
inputHeaders: map[string]string{
195+
XForwardedFor: fmt.Sprintf("%s,%s", publicIP.String(), localhost.String()),
196+
},
197+
peer: localhostPeer(),
198+
expectedIP: publicIP,
199+
}
200+
t.Run("unary", func(t *testing.T) {
201+
testUnaryServerInterceptor(t, tc)
202+
})
203+
t.Run("stream", func(t *testing.T) {
204+
testStreamServerInterceptor(t, tc)
205+
})
206+
})
207+
t.Run("trusted proxy list private net with XForwardedFor", func(t *testing.T) {
208+
tc := testCase{
209+
// Test that if the remote peer is trusted and the header contains
210+
// a comma separated list of valid IPs,
211+
// we get the first going from right to left that is not in private net
212+
trustedPeers: localnet,
213+
trustedProxies: privatenet,
214+
headerKeys: []string{XForwardedFor},
215+
inputHeaders: map[string]string{
216+
XForwardedFor: fmt.Sprintf("%s,%s", publicIP.String(), localhost.String()),
217+
},
218+
peer: localhostPeer(),
219+
expectedIP: localhost,
220+
}
221+
t.Run("unary", func(t *testing.T) {
222+
testUnaryServerInterceptor(t, tc)
223+
})
224+
t.Run("stream", func(t *testing.T) {
225+
testStreamServerInterceptor(t, tc)
226+
})
227+
})
228+
t.Run("trusted proxy count with XForwardedFor", func(t *testing.T) {
229+
tc := testCase{
230+
// Test that if the remote peer is trusted and the header contains
231+
// a comma separated list of valid IPs, we get right most one -1 proxiesCount.
232+
trustedPeers: localnet,
233+
proxiesCount: 1,
234+
headerKeys: []string{XForwardedFor},
235+
inputHeaders: map[string]string{
236+
XForwardedFor: fmt.Sprintf("%s,%s", publicIP.String(), localhost.String()),
237+
},
238+
peer: localhostPeer(),
239+
expectedIP: publicIP,
240+
}
241+
t.Run("unary", func(t *testing.T) {
242+
testUnaryServerInterceptor(t, tc)
243+
})
244+
t.Run("stream", func(t *testing.T) {
245+
testStreamServerInterceptor(t, tc)
246+
})
247+
})
248+
t.Run("wrong trusted proxy count with XForwardedFor", func(t *testing.T) {
249+
tc := testCase{
250+
// Test that if the remote peer is trusted and the header contains
251+
// a comma separated list of valid IPs,
252+
// we get peer ip as the proxiesCount is wrongly configured
253+
trustedPeers: localnet,
254+
proxiesCount: 10,
255+
headerKeys: []string{XForwardedFor},
256+
inputHeaders: map[string]string{
257+
XForwardedFor: fmt.Sprintf("%s,%s", publicIP.String(), localhost.String()),
258+
},
259+
peer: localhostPeer(),
260+
expectedIP: localhost,
261+
}
262+
t.Run("unary", func(t *testing.T) {
263+
testUnaryServerInterceptor(t, tc)
264+
})
265+
t.Run("stream", func(t *testing.T) {
266+
testStreamServerInterceptor(t, tc)
267+
})
268+
})
176269
t.Run("trusted peer single", func(t *testing.T) {
177270
tc := testCase{
178271
// Test that if the remote peer is trusted and the header contains

0 commit comments

Comments
 (0)