Skip to content

Commit bfc8ca2

Browse files
manucorporatsorenisanerdappleboythinkeroujavierprovecho
authored
feat(engine): add trustedproxies and remoteIP (#2632)
Co-authored-by: Søren L. Hansen <[email protected]> Co-authored-by: Bo-Yi Wu <[email protected]> Co-authored-by: thinkerou <[email protected]> Co-authored-by: Javier Provecho Fernandez <[email protected]>
1 parent f3de813 commit bfc8ca2

File tree

6 files changed

+391
-22
lines changed

6 files changed

+391
-22
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2124,6 +2124,39 @@ func main() {
21242124
}
21252125
```
21262126

2127+
## Don't trust all proxies
2128+
2129+
Gin lets you specify which headers to hold the real client IP (if any),
2130+
as well as specifying which proxies (or direct clients) you trust to
2131+
specify one of these headers.
2132+
2133+
The `TrustedProxies` slice on your `gin.Engine` specifes network addresses or
2134+
network CIDRs from where clients which their request headers related to client
2135+
IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
2136+
IPv6 CIDRs.
2137+
2138+
```go
2139+
import (
2140+
"fmt"
2141+
2142+
"github.com/gin-gonic/gin"
2143+
)
2144+
2145+
func main() {
2146+
2147+
router := gin.Default()
2148+
router.TrustedProxies = []string{"192.168.1.2"}
2149+
2150+
router.GET("/", func(c *gin.Context) {
2151+
// If the client is 192.168.1.2, use the X-Forwarded-For
2152+
// header to deduce the original client IP from the trust-
2153+
// worthy parts of that header.
2154+
// Otherwise, simply return the direct client IP
2155+
fmt.Printf("ClientIP: %s\n", c.ClientIP())
2156+
})
2157+
router.Run()
2158+
}
2159+
```
21272160

21282161
## Testing
21292162

context.go

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -725,32 +725,82 @@ func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (e
725725
return bb.BindBody(body, obj)
726726
}
727727

728-
// ClientIP implements a best effort algorithm to return the real client IP, it parses
729-
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
730-
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
728+
// ClientIP implements a best effort algorithm to return the real client IP.
729+
// It called c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
730+
// If it's it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
731+
// If the headers are nots syntactically valid OR the remote IP does not correspong to a trusted proxy,
732+
// the remote IP (coming form Request.RemoteAddr) is returned.
731733
func (c *Context) ClientIP() string {
732-
if c.engine.ForwardedByClientIP {
733-
clientIP := c.requestHeader("X-Forwarded-For")
734-
clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
735-
if clientIP == "" {
736-
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
734+
if c.engine.AppEngine {
735+
if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
736+
return addr
737737
}
738-
if clientIP != "" {
739-
return clientIP
738+
}
739+
740+
remoteIP, trusted := c.RemoteIP()
741+
if remoteIP == nil {
742+
return ""
743+
}
744+
745+
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
746+
for _, headerName := range c.engine.RemoteIPHeaders {
747+
ip, valid := validateHeader(c.requestHeader(headerName))
748+
if valid {
749+
return ip
750+
}
740751
}
741752
}
753+
return remoteIP.String()
754+
}
742755

743-
if c.engine.AppEngine {
744-
if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
745-
return addr
756+
// RemoteIP parses the IP from Request.RemoteAddr, normalizes and returns the IP (without the port).
757+
// It also checks if the remoteIP is a trusted proxy or not.
758+
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
759+
// defined in Engine.TrustedProxies
760+
func (c *Context) RemoteIP() (net.IP, bool) {
761+
ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
762+
if err != nil {
763+
return nil, false
764+
}
765+
remoteIP := net.ParseIP(ip)
766+
if remoteIP == nil {
767+
return nil, false
768+
}
769+
770+
trustedCIDRs, _ := c.engine.prepareTrustedCIDRs()
771+
c.engine.trustedCIDRs = trustedCIDRs
772+
if c.engine.trustedCIDRs != nil {
773+
for _, cidr := range c.engine.trustedCIDRs {
774+
if cidr.Contains(remoteIP) {
775+
return remoteIP, true
776+
}
746777
}
747778
}
748779

749-
if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
750-
return ip
780+
return remoteIP, false
781+
}
782+
783+
func validateHeader(header string) (clientIP string, valid bool) {
784+
if header == "" {
785+
return "", false
751786
}
787+
items := strings.Split(header, ",")
788+
for i, ipStr := range items {
789+
ipStr = strings.TrimSpace(ipStr)
790+
ip := net.ParseIP(ipStr)
791+
if ip == nil {
792+
return "", false
793+
}
752794

753-
return ""
795+
// We need to return the first IP in the list, but,
796+
// we should not early return since we need to validate that
797+
// the rest of the header is syntactically valid
798+
if i == 0 {
799+
clientIP = ipStr
800+
valid = true
801+
}
802+
}
803+
return
754804
}
755805

756806
// ContentType returns the Content-Type header of the request.

context_test.go

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,11 +1392,10 @@ func TestContextClientIP(t *testing.T) {
13921392
c, _ := CreateTestContext(httptest.NewRecorder())
13931393
c.Request, _ = http.NewRequest("POST", "/", nil)
13941394

1395-
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
1396-
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
1397-
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
1398-
c.Request.RemoteAddr = " 40.40.40.40:42123 "
1395+
resetContextForClientIPTests(c)
13991396

1397+
// Legacy tests (validating that the defaults don't break the
1398+
// (insecure!) old behaviour)
14001399
assert.Equal(t, "20.20.20.20", c.ClientIP())
14011400

14021401
c.Request.Header.Del("X-Forwarded-For")
@@ -1416,6 +1415,74 @@ func TestContextClientIP(t *testing.T) {
14161415
// no port
14171416
c.Request.RemoteAddr = "50.50.50.50"
14181417
assert.Empty(t, c.ClientIP())
1418+
1419+
// Tests exercising the TrustedProxies functionality
1420+
resetContextForClientIPTests(c)
1421+
1422+
// No trusted proxies
1423+
c.engine.TrustedProxies = []string{}
1424+
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
1425+
assert.Equal(t, "40.40.40.40", c.ClientIP())
1426+
1427+
// Last proxy is trusted, but the RemoteAddr is not
1428+
c.engine.TrustedProxies = []string{"30.30.30.30"}
1429+
assert.Equal(t, "40.40.40.40", c.ClientIP())
1430+
1431+
// Only trust RemoteAddr
1432+
c.engine.TrustedProxies = []string{"40.40.40.40"}
1433+
assert.Equal(t, "20.20.20.20", c.ClientIP())
1434+
1435+
// All steps are trusted
1436+
c.engine.TrustedProxies = []string{"40.40.40.40", "30.30.30.30", "20.20.20.20"}
1437+
assert.Equal(t, "20.20.20.20", c.ClientIP())
1438+
1439+
// Use CIDR
1440+
c.engine.TrustedProxies = []string{"40.40.25.25/16", "30.30.30.30"}
1441+
assert.Equal(t, "20.20.20.20", c.ClientIP())
1442+
1443+
// Use hostname that resolves to all the proxies
1444+
c.engine.TrustedProxies = []string{"foo"}
1445+
assert.Equal(t, "40.40.40.40", c.ClientIP())
1446+
1447+
// Use hostname that returns an error
1448+
c.engine.TrustedProxies = []string{"bar"}
1449+
assert.Equal(t, "40.40.40.40", c.ClientIP())
1450+
1451+
// X-Forwarded-For has a non-IP element
1452+
c.engine.TrustedProxies = []string{"40.40.40.40"}
1453+
c.Request.Header.Set("X-Forwarded-For", " blah ")
1454+
assert.Equal(t, "40.40.40.40", c.ClientIP())
1455+
1456+
// Result from LookupHost has non-IP element. This should never
1457+
// happen, but we should test it to make sure we handle it
1458+
// gracefully.
1459+
c.engine.TrustedProxies = []string{"baz"}
1460+
c.Request.Header.Set("X-Forwarded-For", " 30.30.30.30 ")
1461+
assert.Equal(t, "40.40.40.40", c.ClientIP())
1462+
1463+
c.engine.TrustedProxies = []string{"40.40.40.40"}
1464+
c.Request.Header.Del("X-Forwarded-For")
1465+
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For", "X-Real-IP"}
1466+
assert.Equal(t, "10.10.10.10", c.ClientIP())
1467+
1468+
c.engine.RemoteIPHeaders = []string{}
1469+
c.engine.AppEngine = true
1470+
assert.Equal(t, "50.50.50.50", c.ClientIP())
1471+
1472+
c.Request.Header.Del("X-Appengine-Remote-Addr")
1473+
assert.Equal(t, "40.40.40.40", c.ClientIP())
1474+
1475+
// no port
1476+
c.Request.RemoteAddr = "50.50.50.50"
1477+
assert.Empty(t, c.ClientIP())
1478+
}
1479+
1480+
func resetContextForClientIPTests(c *Context) {
1481+
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
1482+
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
1483+
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
1484+
c.Request.RemoteAddr = " 40.40.40.40:42123 "
1485+
c.engine.AppEngine = false
14191486
}
14201487

14211488
func TestContextContentType(t *testing.T) {
@@ -1960,3 +2027,12 @@ func TestContextWithKeysMutex(t *testing.T) {
19602027
assert.Nil(t, value)
19612028
assert.False(t, err)
19622029
}
2030+
2031+
func TestRemoteIPFail(t *testing.T) {
2032+
c, _ := CreateTestContext(httptest.NewRecorder())
2033+
c.Request, _ = http.NewRequest("POST", "/", nil)
2034+
c.Request.RemoteAddr = "[:::]:80"
2035+
ip, trust := c.RemoteIP()
2036+
assert.Nil(t, ip)
2037+
assert.False(t, trust)
2038+
}

gin.go

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net/http"
1212
"os"
1313
"path"
14+
"strings"
1415
"sync"
1516

1617
"github.com/gin-gonic/gin/internal/bytesconv"
@@ -81,9 +82,26 @@ type Engine struct {
8182
// If no other Method is allowed, the request is delegated to the NotFound
8283
// handler.
8384
HandleMethodNotAllowed bool
84-
ForwardedByClientIP bool
8585

86-
// #726 #755 If enabled, it will thrust some headers starting with
86+
// If enabled, client IP will be parsed from the request's headers that
87+
// match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was
88+
// fetched, it falls back to the IP obtained from
89+
// `(*gin.Context).Request.RemoteAddr`.
90+
ForwardedByClientIP bool
91+
92+
// List of headers used to obtain the client IP when
93+
// `(*gin.Engine).ForwardedByClientIP` is `true` and
94+
// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
95+
// network origins of `(*gin.Engine).TrustedProxies`.
96+
RemoteIPHeaders []string
97+
98+
// List of network origins (IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
99+
// IPv6 CIDRs) from which to trust request's headers that contain
100+
// alternative client IP when `(*gin.Engine).ForwardedByClientIP` is
101+
// `true`.
102+
TrustedProxies []string
103+
104+
// #726 #755 If enabled, it will trust some headers starting with
87105
// 'X-AppEngine...' for better integration with that PaaS.
88106
AppEngine bool
89107

@@ -114,6 +132,7 @@ type Engine struct {
114132
pool sync.Pool
115133
trees methodTrees
116134
maxParams uint16
135+
trustedCIDRs []*net.IPNet
117136
}
118137

119138
var _ IRouter = &Engine{}
@@ -139,6 +158,8 @@ func New() *Engine {
139158
RedirectFixedPath: false,
140159
HandleMethodNotAllowed: false,
141160
ForwardedByClientIP: true,
161+
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
162+
TrustedProxies: []string{"0.0.0.0/0"},
142163
AppEngine: defaultAppEngine,
143164
UseRawPath: false,
144165
RemoveExtraSlash: false,
@@ -305,12 +326,60 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
305326
func (engine *Engine) Run(addr ...string) (err error) {
306327
defer func() { debugPrintError(err) }()
307328

329+
trustedCIDRs, err := engine.prepareTrustedCIDRs()
330+
if err != nil {
331+
return err
332+
}
333+
engine.trustedCIDRs = trustedCIDRs
308334
address := resolveAddress(addr)
309335
debugPrint("Listening and serving HTTP on %s\n", address)
310336
err = http.ListenAndServe(address, engine)
311337
return
312338
}
313339

340+
func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
341+
if engine.TrustedProxies == nil {
342+
return nil, nil
343+
}
344+
345+
cidr := make([]*net.IPNet, 0, len(engine.TrustedProxies))
346+
for _, trustedProxy := range engine.TrustedProxies {
347+
if !strings.Contains(trustedProxy, "/") {
348+
ip := parseIP(trustedProxy)
349+
if ip == nil {
350+
return cidr, &net.ParseError{Type: "IP address", Text: trustedProxy}
351+
}
352+
353+
switch len(ip) {
354+
case net.IPv4len:
355+
trustedProxy += "/32"
356+
case net.IPv6len:
357+
trustedProxy += "/128"
358+
}
359+
}
360+
_, cidrNet, err := net.ParseCIDR(trustedProxy)
361+
if err != nil {
362+
return cidr, err
363+
}
364+
cidr = append(cidr, cidrNet)
365+
}
366+
return cidr, nil
367+
}
368+
369+
// parseIP parse a string representation of an IP and returns a net.IP with the
370+
// minimum byte representation or nil if input is invalid.
371+
func parseIP(ip string) net.IP {
372+
parsedIP := net.ParseIP(ip)
373+
374+
if ipv4 := parsedIP.To4(); ipv4 != nil {
375+
// return ip in a 4-byte representation
376+
return ipv4
377+
}
378+
379+
// return ip in a 16-byte representation or nil
380+
return parsedIP
381+
}
382+
314383
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
315384
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
316385
// Note: this method will block the calling goroutine indefinitely unless an error happens.

gin_integration_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ func TestRunEmpty(t *testing.T) {
5555
testRequest(t, "http://localhost:8080/example")
5656
}
5757

58+
func TestTrustedCIDRsForRun(t *testing.T) {
59+
os.Setenv("PORT", "")
60+
router := New()
61+
router.TrustedProxies = []string{"hello/world"}
62+
assert.Error(t, router.Run(":8080"))
63+
}
64+
5865
func TestRunTLS(t *testing.T) {
5966
router := New()
6067
go func() {

0 commit comments

Comments
 (0)