Golang笔记-10-X-Forwarded-For

Overview

1. 前言

我们的业务分布在国内的各个边缘机房,这些机房都需要访问中心云的接口。最初内部服务直接对外暴露,并使用防火墙和白名单,根据来源IP决定访问权限。后来为了满足监控、探测需求和高可用,改造成使用多接入点的方式。

笔者最初的目的只是同步防火墙规则,由于业务都是采用HTTP和Websocket,就顺手加了反向代理功能,并增加了校验和按配置转发请求的功能,最后输出一个小型的ingress,如下:

graph LR; A[边缘机房A] B[边缘机房B] A--->|公网|J1 A--->|公网|J2 B--->|公网|J1 B--->|公网|J2 J1--->|内网|S1 J1--->|内网|S2 J2--->|内网|S1 J2--->|内网|S2 subgraph 中心云大内网 J1[北京接入点J1] J2[广州接入点J2] S1[内部服务S1] S2[内部服务S2] end

笔者开发的转发服务部署在多个接入点上。因为需要转发请求且保留来源IP,于是使用了 X-Forwarded-ForX-Real-IP 这两个头部来传递客户端源IP,后端服务(使用gin框架)读取头部获得真实IP。

但开发过程中发现后端服务能读取 X-Real-IP 获取源IP,而gin.Context的ClientIP函数只能获取转发服务所在机器的IP,今天正巧研究了下源码,这里稍作记录。

2. 如何使用头部

X-Real-IP 的用法比较简单,假设有多个负载均衡或者反向代理,就依次传递,直达后端,如下:

graph LR; A[客户端
公网IP:1.2.3.4] A--->|公网|J1 J1--->|内网
X-Real-IP=1.2.3.4|J2 J2--->|内网
X-Real-IP=1.2.3.4|S subgraph 云服务内网 J1[接入点J1
内网IP:10.0.0.2] J2[接入点J2
内网IP:10.0.0.3] S[内部服务S
内网IP:10.0.0.4] end

我们可以看到从J1获取到客户端IP后,使用固定头部向后逐级传递,内部服务直接读取 X-Real-IP 即可获取来源IP。

X-Forwarded-For 的用法则比较讲究,每一级转发服务都需要添加前一级客户端IP,如下:

graph LR; A[客户端
公网IP:1.2.3.4] A--->|公网|J1 J1--->|内网
X-Forwarded-For=1.2.3.4|J2 J2--->|内网
X-Forwarded-For=1.2.3.4,10.0.0.2|S subgraph 云服务内网 J1[接入点J1
内网IP:10.0.0.2] J2[接入点J2
内网IP:10.0.0.3] S[内部服务S
内网IP:10.0.0.4] end

J2将请求转发给S前,取HTTP请求的来源IP和 X-Forwarded-For 的值组成新的头部,即 X-Forwarded-For 的数值格式为:

1X-Forwarded-For: clientIP
2X-Forwarded-For: clientIP,proxy-1
3X-Forwarded-For: clientIP,proxy-1,proxy-2
4X-Forwarded-For: clientIP,proxy-1,proxy-2,proxy-3

内部服务需要取来源IP时,取 X-Forwarded-For 使用逗号分割后取第一个值。与 X-Forwarded-For 关联的头部还有:

  • X-Forwarded-Host:用来确定客户端发起的请求中使用Host指定的初始域名
  • X-Forwarded-Proto:用来确定客户端与代理服务器或者负载均衡服务器之间的连接所采用的传输协议

有兴趣的读者可以研究下Mozilla的文档。

3. gin.Context的ClientIP函数实现

直接读取解析 X-Forwarded-For 头部是有风险的,Mozilla的文档对此有介绍:

X-Forwarded-For#security_and_privacy_concerns

因此大多数框架都配置了一个trustCIDR参数设置受信赖的IP地址范围,例如gin.New函数就使用了 0.0.0.0/0,如下:

 1// New returns a new blank Engine instance without any middleware attached.
 2// By default the configuration is:
 3// - RedirectTrailingSlash:  true
 4// - RedirectFixedPath:      false
 5// - HandleMethodNotAllowed: false
 6// - ForwardedByClientIP:    true
 7// - UseRawPath:             false
 8// - UnescapePathValues:     true
 9func New() *Engine {
10	debugPrintWARNINGNew()
11	engine := &Engine{
12		RouterGroup: RouterGroup{
13			Handlers: nil,
14			basePath: "/",
15			root:     true,
16		},
17		FuncMap:                template.FuncMap{},
18		RedirectTrailingSlash:  true,
19		RedirectFixedPath:      false,
20		HandleMethodNotAllowed: false,
21		ForwardedByClientIP:    true,
22		RemoteIPHeaders:        []string{"X-Forwarded-For", "X-Real-IP"},
23		TrustedPlatform:        defaultPlatform,
24		UseRawPath:             false,
25		RemoveExtraSlash:       false,
26		UnescapePathValues:     true,
27		MaxMultipartMemory:     defaultMultipartMemory,
28		trees:                  make(methodTrees, 0, 9),
29		delims:                 render.Delims{Left: "{{", Right: "}}"},
30		secureJSONPrefix:       "while(1);",
31		trustedProxies:         []string{"0.0.0.0/0"},
32		trustedCIDRs:           defaultTrustedCIDRs,
33	}
34	engine.RouterGroup.engine = engine
35	engine.pool.New = func() interface{} {
36		return engine.allocateContext()
37	}
38	return engine
39}

并且在 RemoteIPHeaders 中可以看到我们上述提到的两个头部,当我们拿到一个请求时,通过gin.Context的ClientIP函数可以读取到来源IP,如下:

 1// ClientIP implements one best effort algorithm to return the real client IP.
 2// It called c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
 3// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
 4// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
 5// the remote IP (coming form Request.RemoteAddr) is returned.
 6func (c *Context) ClientIP() string {
 7	// Check if we're running on a trusted platform, continue running backwards if error
 8	if c.engine.TrustedPlatform != "" {
 9		// Developers can define their own header of Trusted Platform or use predefined constants
10		if addr := c.requestHeader(c.engine.TrustedPlatform); addr != "" {
11			return addr
12		}
13	}
14
15	// Legacy "AppEngine" flag
16	if c.engine.AppEngine {
17		log.Println(`The AppEngine flag is going to be deprecated. Please check issues #2723 and #2739 and use 'TrustedPlatform: gin.PlatformGoogleAppEngine' instead.`)
18		if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
19			return addr
20		}
21	}
22
23	remoteIP, trusted := c.RemoteIP()
24	if remoteIP == nil {
25		return ""
26	}
27
28	if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
29		for _, headerName := range c.engine.RemoteIPHeaders {
30			ip, valid := c.engine.validateHeader(c.requestHeader(headerName))
31			if valid {
32				return ip
33			}
34		}
35	}
36	return remoteIP.String()
37}
38
39// RemoteIP parses the IP from Request.RemoteAddr, normalizes and returns the IP (without the port).
40// It also checks if the remoteIP is a trusted proxy or not.
41// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
42// defined by Engine.SetTrustedProxies()
43func (c *Context) RemoteIP() (net.IP, bool) {
44	ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
45	if err != nil {
46		return nil, false
47	}
48	remoteIP := net.ParseIP(ip)
49	if remoteIP == nil {
50		return nil, false
51	}
52
53	return remoteIP, c.engine.isTrustedProxy(remoteIP)
54}
55
56func (e *Engine) isTrustedProxy(ip net.IP) bool {
57	if e.trustedCIDRs != nil {
58		for _, cidr := range e.trustedCIDRs {
59			if cidr.Contains(ip) {
60				return true
61			}
62		}
63	}
64	return false
65}
66
67func (e *Engine) validateHeader(header string) (clientIP string, valid bool) {
68	if header == "" {
69		return "", false
70	}
71	items := strings.Split(header, ",")
72	for i := len(items) - 1; i >= 0; i-- {
73		ipStr := strings.TrimSpace(items[i])
74		ip := net.ParseIP(ipStr)
75		if ip == nil {
76			return "", false
77		}
78
79		// X-Forwarded-For is appended by proxy
80		// Check IPs in reverse order and stop when find untrusted proxy
81		if (i == 0) || (!e.isTrustedProxy(ip)) {
82			return ipStr, true
83		}
84	}
85	return
86}
  1. 读取 TrustedPlatform 预定义的受信任头部,若存在Value则返回
  2. 处理废弃头部 X-Appengine-Remote-Addr
  3. 读取 RemoteIP,这里取HTTP请求的RemoteAddr,并判断它是否属于受信赖的IP地址范围
  4. RemoteIP 属于受信赖的IP地址范围,且允许从头部读取 X-Forwarded-ForX-Real-IP,则解析头部获取来源IP,IP不为空时返回
  5. 未读取到头部中的来源IP,返回 RemoteIP

可以看到,ClientIP能否获取到反向代理转发的来源IP,需要满足两个条件:

  1. 反向代理设置了X-Forwarded-For或X-Real-IP头部保存客户端来源IP
  2. 反向代理的IP在gin.Engine的trustedCIDRs地址范围内

gin默认设置trustedCIDRs为 0.0.0.0/0,理论上能处理所有情况...除了IPv6。

由于笔者部署服务的内部网络使用IPv6地址,导致反向代理的地址始终无法落在默认的IPv4地址范围内,因此ClientIP函数总是返回反向代理服务的宿主机IPv6地址,处理办法很简单,初始化gin.Engine后,设置一下 trustedCIDRs

1// 初始化
2g := gin.New()
3// 信赖所有IPv4及IPv6地址段
4g.SetTrustedProxies([]string{"0.0.0.0/0", "::/0"})

目前在gin项目的PR中也有看到IPv6环境无法获取客户端IP的问题,只是修改还未合并到主分支。

今天笔者的同事调试ssh登录时,也遇到因为IPv6使用冒号分割地址导致的连接复用出现问题,看来迁移到IPv6的过程中还会有很多的坑。