Skip to content

Commit f35f047

Browse files
authored
Middleware for determining the real ip of the client (#682)
* feat: added realip middleware * refactor for netip * set proper case * update header values * force lowercase when checking header
1 parent 9e92fd5 commit f35f047

File tree

4 files changed

+642
-0
lines changed

4 files changed

+642
-0
lines changed

interceptors/realip/doc.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) The go-grpc-middleware Authors.
2+
// Licensed under the Apache License 2.0.
3+
4+
/*
5+
Package realip is a middleware that extracts the real IP of requests based on
6+
header values.
7+
8+
The real IP is subsequently placed inside the context of each request and can
9+
be retrieved using the [FromContext] function.
10+
11+
The middleware is designed to work with gRPC servers serving clients over
12+
TCP/IP connections. If no headers are found, the middleware will return the
13+
remote peer address as the real IP. If remote peer address is not a TCP/IP
14+
address, the middleware will return nil as the real IP.
15+
16+
Headers provided by clients in the request will be searched for in the order
17+
of the list provided to the middleware. The first header that contains a valid
18+
IP address will be used as the real IP.
19+
20+
Comma separated headers are supported. The last, rightmost, IP address in the
21+
header will be used as the real IP.
22+
23+
# Security
24+
25+
There are 2 main security concerns when deriving the real IP from request
26+
headers:
27+
28+
1. Risk of spoofing the real IP by setting a header value.
29+
2. Risk of injecting a header value that causes a denial of service.
30+
31+
To mitigate the risk of spoofing, the middleware introduces the concept of
32+
"trusted peers". Trusted peers are defined as a list of IP networks that are
33+
verified by the gRPC server operator to be trusted. If the peer address is found
34+
to be within one of the trusted networks, the middleware will attempt to extract
35+
the real IP from the request headers. If the peer address is not found to be
36+
within one of the trusted networks, the peer address will be returned as the
37+
real IP.
38+
39+
"trusted" in this context means that the peer is configured to overwrite the
40+
header value with the real IP. This is typically done by a proxy or load
41+
balancer that is configured to forward the real IP of the client in a header
42+
value. Alternatively, the peer may be configured to append the real IP to the
43+
header value. In this case, the middleware will use the last, rightmost, IP
44+
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.
47+
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.
52+
53+
# Individual IP addresses as trusted peers
54+
55+
When creating the list of trusted peers, it is possible to specify individual IP
56+
addresses. This is useful when your proxy or load balancer has a set of
57+
well-known addresses.
58+
59+
The following example shows how to specify individual IP addresses as trusted
60+
peers:
61+
62+
trusted := []net.IPNet{
63+
{IP: net.IPv4(192, 168, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 255)},
64+
{IP: net.IPv4(192, 168, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},
65+
}
66+
67+
In the above example, the middleware will only attempt to extract the real IP
68+
from the request headers if the peer address is either 192.168.0.1 or
69+
192.168.0.2.
70+
71+
# Headers
72+
73+
Headers to search for are specified as a list of strings when creating the
74+
middleware. The middleware will search for the headers in the order specified
75+
and use the first header that contains a valid IP address as the real IP.
76+
77+
The following are examples of headers that may contain the real IP:
78+
79+
- X-Forwarded-For: This header is set by proxies and contains a comma
80+
separated list of IP addresses. Each proxy that forwards the request will
81+
append the real IP to the header value.
82+
- X-Real-IP: This header is set by NGINX and contains the real IP as a string
83+
containing a single IP address.
84+
- Forwarded-For: Header defined by RFC7239. This header is set by proxies and
85+
contains the real IP as a string containing a single IP address. Please note
86+
that the obfuscated identifier from section 6.3 of RFC7239, and that the
87+
unknown identifier from section 6.2 of RFC7239 are not supported.
88+
- True-Client-IP: This header is set by Cloudflare and contains the real IP
89+
as a string containing a single IP address.
90+
91+
# Usage
92+
93+
Please see examples for simple examples of use.
94+
*/
95+
package realip

interceptors/realip/examples_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) The go-grpc-middleware Authors.
2+
// Licensed under the Apache License 2.0.
3+
4+
package realip_test
5+
6+
import (
7+
"net/netip"
8+
9+
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
10+
"google.golang.org/grpc"
11+
)
12+
13+
// Simple example of a unary server initialization code.
14+
func ExampleUnaryServerInterceptor() {
15+
// Define list of trusted peers from which we accept forwarded-for and
16+
// real-ip headers.
17+
trustedPeers := []netip.Prefix{
18+
netip.MustParsePrefix("127.0.0.1/32"),
19+
}
20+
// Define headers to look for in the incoming request.
21+
headers := []string{realip.XForwardedFor, realip.XRealIp}
22+
_ = grpc.NewServer(
23+
grpc.ChainUnaryInterceptor(
24+
realip.UnaryServerInterceptor(trustedPeers, headers),
25+
),
26+
)
27+
}
28+
29+
// Simple example of a streaming server initialization code.
30+
func ExampleStreamServerInterceptor() {
31+
// Define list of trusted peers from which we accept forwarded-for and
32+
// real-ip headers.
33+
trustedPeers := []netip.Prefix{
34+
netip.MustParsePrefix("127.0.0.1/32"),
35+
}
36+
// Define headers to look for in the incoming request.
37+
headers := []string{realip.XForwardedFor, realip.XRealIp}
38+
_ = grpc.NewServer(
39+
grpc.ChainStreamInterceptor(
40+
realip.StreamServerInterceptor(trustedPeers, headers),
41+
),
42+
)
43+
}

interceptors/realip/realip.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright (c) The go-grpc-middleware Authors.
2+
// Licensed under the Apache License 2.0.
3+
4+
package realip
5+
6+
import (
7+
"context"
8+
"net"
9+
"net/netip"
10+
"strings"
11+
12+
"google.golang.org/grpc"
13+
"google.golang.org/grpc/metadata"
14+
"google.golang.org/grpc/peer"
15+
)
16+
17+
// XRealIp, XForwardedFor and TrueClientIp are header keys
18+
// used to extract the real client IP from the request. They represent common
19+
// conventions for identifying the originating IP address of a client connecting
20+
// through proxies or load balancers.
21+
const (
22+
XRealIp = "X-Real-IP"
23+
XForwardedFor = "X-Forwarded-For"
24+
TrueClientIp = "True-Client-IP"
25+
)
26+
27+
var noIP = netip.Addr{}
28+
29+
type realipKey struct{}
30+
31+
// FromContext extracts the real client IP from the context.
32+
// It returns the IP and a boolean indicating if it was present.
33+
func FromContext(ctx context.Context) (netip.Addr, bool) {
34+
ip, ok := ctx.Value(realipKey{}).(netip.Addr)
35+
return ip, ok
36+
}
37+
38+
func remotePeer(ctx context.Context) net.Addr {
39+
pr, ok := peer.FromContext(ctx)
40+
if !ok {
41+
return nil
42+
}
43+
return pr.Addr
44+
}
45+
46+
func ipInNets(ip netip.Addr, nets []netip.Prefix) bool {
47+
for _, n := range nets {
48+
if n.Contains(ip) {
49+
return true
50+
}
51+
}
52+
return false
53+
}
54+
55+
func getHeader(ctx context.Context, key string) string {
56+
md, ok := metadata.FromIncomingContext(ctx)
57+
if !ok {
58+
return ""
59+
}
60+
61+
if md[strings.ToLower(key)] == nil {
62+
return ""
63+
}
64+
65+
return md[strings.ToLower(key)][0]
66+
}
67+
68+
func ipFromHeaders(ctx context.Context, headers []string) netip.Addr {
69+
for _, header := range headers {
70+
a := strings.Split(getHeader(ctx, header), ",")
71+
h := strings.TrimSpace(a[len(a)-1])
72+
ip, err := netip.ParseAddr(h)
73+
if err == nil {
74+
return ip
75+
}
76+
}
77+
return noIP
78+
}
79+
80+
func getRemoteIP(ctx context.Context, trustedPeers []netip.Prefix, headers []string) netip.Addr {
81+
pr := remotePeer(ctx)
82+
if pr == nil {
83+
return noIP
84+
}
85+
86+
ip, err := netip.ParseAddr(strings.Split(pr.String(), ":")[0])
87+
if err != nil {
88+
return noIP
89+
}
90+
if len(trustedPeers) == 0 || !ipInNets(ip, trustedPeers) {
91+
return ip
92+
}
93+
if ip := ipFromHeaders(ctx, headers); ip != noIP {
94+
return ip
95+
}
96+
// No ip from the headers, return the peer ip.
97+
return ip
98+
}
99+
100+
type serverStream struct {
101+
grpc.ServerStream
102+
ctx context.Context
103+
}
104+
105+
func (s *serverStream) Context() context.Context {
106+
return s.ctx
107+
}
108+
109+
// UnaryServerInterceptor returns a new unary server interceptor that extracts the real client IP from request headers.
110+
// It checks if the request comes from a trusted peer, and if so, extracts the IP from the configured headers.
111+
// The real IP is added to the request context.
112+
func UnaryServerInterceptor(trustedPeers []netip.Prefix, headers []string) grpc.UnaryServerInterceptor {
113+
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
114+
ip := getRemoteIP(ctx, trustedPeers, headers)
115+
if ip != noIP {
116+
ctx = context.WithValue(ctx, realipKey{}, ip)
117+
}
118+
return handler(ctx, req)
119+
}
120+
}
121+
122+
// StreamServerInterceptor returns a new stream server interceptor that extracts the real client IP from request headers.
123+
// It checks if the request comes from a trusted peer, and if so, extracts the IP from the configured headers.
124+
// The real IP is added to the request context.
125+
func StreamServerInterceptor(trustedPeers []netip.Prefix, headers []string) grpc.StreamServerInterceptor {
126+
return func(srv any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
127+
ip := getRemoteIP(stream.Context(), trustedPeers, headers)
128+
if ip != noIP {
129+
return handler(srv, &serverStream{
130+
ServerStream: stream,
131+
ctx: context.WithValue(stream.Context(), realipKey{}, ip),
132+
})
133+
}
134+
return handler(srv, stream)
135+
}
136+
}

0 commit comments

Comments
 (0)