@@ -6,6 +6,130 @@ import (
66 "strings"
77)
88
9+ /**
10+ By: https://github.com/tmshn (See: https://github.com/labstack/echo/pull/1478 , https://github.com/labstack/echox/pull/134 )
11+ Source: https://echo.labstack.com/guide/ip-address/
12+
13+ IP address plays fundamental role in HTTP; it's used for access control, auditing, geo-based access analysis and more.
14+ Echo provides handy method [`Context#RealIP()`](https://godoc.org/github.com/labstack/echo#Context) for that.
15+
16+ However, it is not trivial to retrieve the _real_ IP address from requests especially when you put L7 proxies before the application.
17+ In such situation, _real_ IP needs to be relayed on HTTP layer from proxies to your app, but you must not trust HTTP headers unconditionally.
18+ Otherwise, you might give someone a chance of deceiving you. **A security risk!**
19+
20+ To retrieve IP address reliably/securely, you must let your application be aware of the entire architecture of your infrastructure.
21+ In Echo, this can be done by configuring `Echo#IPExtractor` appropriately.
22+ This guides show you why and how.
23+
24+ > Note: if you dont' set `Echo#IPExtractor` explicitly, Echo fallback to legacy behavior, which is not a good choice.
25+
26+ Let's start from two questions to know the right direction:
27+
28+ 1. Do you put any HTTP (L7) proxy in front of the application?
29+ - It includes both cloud solutions (such as AWS ALB or GCP HTTP LB) and OSS ones (such as Nginx, Envoy or Istio ingress gateway).
30+ 2. If yes, what HTTP header do your proxies use to pass client IP to the application?
31+
32+ ## Case 1. With no proxy
33+
34+ If you put no proxy (e.g.: directory facing to the internet), all you need to (and have to) see is IP address from network layer.
35+ Any HTTP header is untrustable because the clients have full control what headers to be set.
36+
37+ In this case, use `echo.ExtractIPDirect()`.
38+
39+ ```go
40+ e.IPExtractor = echo.ExtractIPDirect()
41+ ```
42+
43+ ## Case 2. With proxies using `X-Forwarded-For` header
44+
45+ [`X-Forwared-For` (XFF)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) is the popular header
46+ to relay clients' IP addresses.
47+ At each hop on the proxies, they append the request IP address at the end of the header.
48+
49+ Following example diagram illustrates this behavior.
50+
51+ ```text
52+ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
53+ │ "Origin" │───────────>│ Proxy 1 │───────────>│ Proxy 2 │───────────>│ Your app │
54+ │ (IP: a) │ │ (IP: b) │ │ (IP: c) │ │ │
55+ └──────────┘ └──────────┘ └──────────┘ └──────────┘
56+
57+ Case 1.
58+ XFF: "" "a" "a, b"
59+ ~~~~~~
60+ Case 2.
61+ XFF: "x" "x, a" "x, a, b"
62+ ~~~~~~~~~
63+ ↑ What your app will see
64+ ```
65+
66+ In this case, use **first _untrustable_ IP reading from right**. Never use first one reading from left, as it is
67+ configurable by client. Here "trustable" means "you are sure the IP address belongs to your infrastructre".
68+ In above example, if `b` and `c` are trustable, the IP address of the client is `a` for both cases, never be `x`.
69+
70+ In Echo, use `ExtractIPFromXFFHeader(...TrustOption)`.
71+
72+ ```go
73+ e.IPExtractor = echo.ExtractIPFromXFFHeader()
74+ ```
75+
76+ By default, it trusts internal IP addresses (loopback, link-local unicast, private-use and unique local address
77+ from [RFC6890](https://tools.ietf.org/html/rfc6890), [RFC4291](https://tools.ietf.org/html/rfc4291) and
78+ [RFC4193](https://tools.ietf.org/html/rfc4193)).
79+ To control this behavior, use [`TrustOption`](https://godoc.org/github.com/labstack/echo#TrustOption)s.
80+
81+ E.g.:
82+
83+ ```go
84+ e.IPExtractor = echo.ExtractIPFromXFFHeader(
85+ TrustLinkLocal(false),
86+ TrustIPRanges(lbIPRange),
87+ )
88+ ```
89+
90+ - Ref: https://godoc.org/github.com/labstack/echo#TrustOption
91+
92+ ## Case 3. With proxies using `X-Real-IP` header
93+
94+ `X-Real-IP` is another HTTP header to relay clients' IP addresses, but it carries only one address unlike XFF.
95+
96+ If your proxies set this header, use `ExtractIPFromRealIPHeader(...TrustOption)`.
97+
98+ ```go
99+ e.IPExtractor = echo.ExtractIPFromRealIPHeader()
100+ ```
101+
102+ Again, it trusts internal IP addresses by default (loopback, link-local unicast, private-use and unique local address
103+ from [RFC6890](https://tools.ietf.org/html/rfc6890), [RFC4291](https://tools.ietf.org/html/rfc4291) and
104+ [RFC4193](https://tools.ietf.org/html/rfc4193)).
105+ To control this behavior, use [`TrustOption`](https://godoc.org/github.com/labstack/echo#TrustOption)s.
106+
107+ - Ref: https://godoc.org/github.com/labstack/echo#TrustOption
108+
109+ > **Never forget** to configure the outermost proxy (i.e.; at the edge of your infrastructure) **not to pass through incoming headers**.
110+ > Otherwise there is a chance of fraud, as it is what clients can control.
111+
112+ ## About default behavior
113+
114+ In default behavior, Echo sees all of first XFF header, X-Real-IP header and IP from network layer.
115+
116+ As you might already notice, after reading this article, this is not good.
117+ Sole reason this is default is just backward compatibility.
118+
119+ ## Private IP ranges
120+
121+ See: https://en.wikipedia.org/wiki/Private_network
122+
123+ Private IPv4 address ranges (RFC 1918):
124+ * 10.0.0.0 – 10.255.255.255 (24-bit block)
125+ * 172.16.0.0 – 172.31.255.255 (20-bit block)
126+ * 192.168.0.0 – 192.168.255.255 (16-bit block)
127+
128+ Private IPv6 address ranges:
129+ * fc00::/7 address block = RFC 4193 Unique Local Addresses (ULA)
130+
131+ */
132+
9133type ipChecker struct {
10134 trustLoopback bool
11135 trustLinkLocal bool
@@ -52,6 +176,7 @@ func newIPChecker(configs []TrustOption) *ipChecker {
52176 return checker
53177}
54178
179+ // Go1.16+ added `ip.IsPrivate()` but until that use this implementation
55180func isPrivateIPRange (ip net.IP ) bool {
56181 if ip4 := ip .To4 (); ip4 != nil {
57182 return ip4 [0 ] == 10 ||
@@ -87,25 +212,26 @@ type IPExtractor func(*http.Request) string
87212// ExtractIPDirect extracts IP address using actual IP address.
88213// Use this if your server faces to internet directory (i.e.: uses no proxy).
89214func ExtractIPDirect () IPExtractor {
90- return func (req * http.Request ) string {
91- ra , _ , _ := net .SplitHostPort (req .RemoteAddr )
92- return ra
93- }
215+ return extractIP
216+ }
217+
218+ func extractIP (req * http.Request ) string {
219+ ra , _ , _ := net .SplitHostPort (req .RemoteAddr )
220+ return ra
94221}
95222
96223// ExtractIPFromRealIPHeader extracts IP address using x-real-ip header.
97224// Use this if you put proxy which uses this header.
98225func ExtractIPFromRealIPHeader (options ... TrustOption ) IPExtractor {
99226 checker := newIPChecker (options )
100227 return func (req * http.Request ) string {
101- directIP := ExtractIPDirect ()(req )
102228 realIP := req .Header .Get (HeaderXRealIP )
103229 if realIP != "" {
104- if ip := net .ParseIP (directIP ); ip != nil && checker .trust (ip ) {
230+ if ip := net .ParseIP (realIP ); ip != nil && checker .trust (ip ) {
105231 return realIP
106232 }
107233 }
108- return directIP
234+ return extractIP ( req )
109235 }
110236}
111237
@@ -115,7 +241,7 @@ func ExtractIPFromRealIPHeader(options ...TrustOption) IPExtractor {
115241func ExtractIPFromXFFHeader (options ... TrustOption ) IPExtractor {
116242 checker := newIPChecker (options )
117243 return func (req * http.Request ) string {
118- directIP := ExtractIPDirect () (req )
244+ directIP := extractIP (req )
119245 xffs := req .Header [HeaderXForwardedFor ]
120246 if len (xffs ) == 0 {
121247 return directIP
0 commit comments