Gin升级导致的获取客户端真实IP失败问题

此篇以gin升级引发的”bug”为契机,探讨Go程序获取对方真实ip的方案。

如果我们的程序前面有代理,用remoteaddr这种方式拿到的不是对方真实的ip

如何获取客户端真实 IP?从 Gin 的一个 “Bug” 说起

github上就此的讨论: https://github.com/gin-gonic/gin/issues/2697


【Go】获取用户真实的ip地址

(甚至可以不用gin或者框架的方法,直接通过realIP := r.Header.Get("X-Real-IP")获取)

https://www.xiewo.net/blog/show/580/


帅总找我新部署可一个payment服务,接入的是cryptomus。 支付后,对方要求提供一个地址,作为回调来通知支付是否成功(和微信支付/支付宝的套路一样)。

对方的调用ip是固定的,帅总希望他提供的callback回调接口,只允许这个ip访问,他的业务程序和我在ingress层都做了白名单控制:

ingress层的限制:

1
nginx.ingress.kubernetes.io/whitelist-source-range: 91.227.144.54/32

他程序里的限制:

1
2
3
4
5
6
7
8
9
10
11
12
13

u := strings.Split(r.RemoteAddr, ":") // r为 *http.Request

if u == nil || len(u) < 1 || !config.GlobalConfig.IsInWhitelist(u[0]) {
fmt.Println("不在白名单里")

response.Result = nil
log.Error("UnAuthedIp", "content", fmt.Sprintf("service:%s method:%s not found", request.Service, request.Method), "ip", u[0])
response.Error = taskonerror.New(UnAuthedIp, fmt.Sprintf("service:%s method:%s not found", request.Service, request.Method))
return
}

log.Info("request.RequestIp", "RequestIp", r.RemoteAddr, "host", u[0], "IsInWhitelist", config.GlobalConfig.IsInWhitelist(u[0]))

但这样获取到的,一直都是一个10开头的内网ip。

但他反应,在测试环境是可以的。

测试环境和生产的不同之处,是测试环境是二进制部署,直接是ip:port, 中间也没有nginx等

而线上用的是容器化部署,域名形式—其实本质是有了nginx-ingress,导致帅总程序里拿到的ip,不是真实的对方的ip了,而其实是nginx-ingress命名空间下,其中一个pod的ip

解决办法是使用http header中的X-Real-IP字段,其对应的value,就是对方真实的ip

要搞清楚一个问题,获取或获取不到对方真实ip,问题不在对方,而在我们。对方只要给到出口ip,那就没他们啥问题了。 是对方的真实ip,经过了我方业务程序前面的网关等,导致了我发程序使用RemoteAddr拿到的,就不再是对方真实的ip了。。

解决办法是使用r.Header.Get("X-Real-IP")获取realIP,满足其一即可。(或者如果都不满足,则报错&返回)

1
2
3
4
5
6
7
8
9
10
11
// used in production
realIP := r.Header.Get("X-Real-IP")
if len(u) < 1 || (!config.GlobalConfig.IsInWhitelist(u[0]) && !config.GlobalConfig.IsInWhitelist(realIP)) {
response.Result = nil
log.Error("UnAuthedIp", "content", fmt.Sprintf("service:%s method:%s not found", request.Service, request.Method), "ip", u[0])
response.Error = taskonerror.New(UnAuthedIp, fmt.Sprintf("service:%s method:%s not found", request.Service, request.Method))
return
}

log.Info("request.RequestIp", "RequestIp", r.RemoteAddr, "host", u[0], "IsInWhitelist", config.GlobalConfig.IsInWhitelist(u[0]))
log.Info("request.RequestIp", "RequestIp", r.RemoteAddr, "host", realIP, "IsInWhitelist", config.GlobalConfig.IsInWhitelist(realIP))

可以在业务pod里面,使用

1
2
3
apt-get install tcpdump

tcpdump -i eth0 -nn tcp port 8080 -c 30 -A

tcpdump命令是一个网络数据包捕获工具,常用于网络调试和分析。

你给出的命令行选项及其作用:

  1. -i eth0 指定捕获数据包的网络接口为eth0

  2. -nn 表示不解析主机名和协议名,直接显示IP地址和端口号。

  3. tcp port 8080 表示只捕获TCP协议并且端口号为8080的数据包。

  4. -c 30 限制捕获30个数据包就停止捕获。

  5. -A 表示以ASCII形式打印出数据包的全部内容,包括链路层和IP层头部。

因此,这条命令的作用是:在eth0接口上捕获30个与端口8080有关的TCP数据包,并以ASCII形式打印出完整的数据包内容,包括数据包头部和负载数据。

这对于调试服务器应用监听在8080端口的网络通信十分有用,可以看到来自客户端的请求内容,以及服务器响应的内容。通常在排查Web应用程序等问题时会使用这个命令。

看一下请求的X-Real-IP会不会变。 (如果我们程序前面开启了cf的代理,则就不准,所以还不能开启cf的代理,但这样就起不到隐藏真实后端ip的效果了)




x-forward-for 和remoteaddr 这两个ip都是什么?有什么区别?



x-forward-for和remoteaddr这两个ip有如下区别:

  • x-forward-for:

x-forward-for是HTTP请求头字段,它记录客户端IP地址和代理服务器IP地址。如果请求经过了 proxy 或 load balancers,那么 x-forward-for 字段就会包含来源 IP 地址的链路信息。

  • remoteaddr:

remoteaddr是Nginx根据连接上下文解析出的客户端的真实IP地址。它记录的是最后一次代理或balancer与客户端直接通信的客户端IP地址。

区别在于:

  • x-forward-for可以记录完整的客户端IP地址经过的中继节点列表,而remoteaddr只记录最后的客户端IP。

  • 如果没有经过代理或balancer,x-forward-for和remoteaddr值都将是客户端真实IP地址。

  • 如果经过代理或balancer,remoteaddr将显示最后代理或balancer的IP地址,而x-forward-for将显示完整IP地址链路信息。

所以总的来说:

  • remoteaddr记录最后一个中继的客户端IP
  • x-forward-for记录完整IP地址传递过程中所有中继节点的客户端IP

一般情况下,x-forward-for提供的信息更全面。两者结合可以得知更准确的客户端IP地址来源。



X-Forwarded-For 是一个 HTTP 头部字段,用来记录一个请求从客户端到达原始服务器的所有中间代理服务器的IP地址。

在大多数情况下,当你的应用程序运行在一个代理服务器之后(例如 Nginx、Apache、或者其他反向代理软件),客户端的直接请求会被转发到你的应用程序。在这种情况下,应用程序会收到来自代理服务器的请求,而不是直接的客户端请求。X-Forwarded-For 可以帮助你找到原始的客户端IP地址。

以下是一个如何在其他编程语言中获取真实IP地址的例子:

1
2
3
4
5
6
7
8
9
10
11
Python (Flask):

from flask import request

def get_real_ip():
x_forwarded_for = request.headers.get('X-Forwarded-For')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.remote_addr
return ip
1
2
3
4
5
6
7
8
9
10
Node.js (Express):

const getRealIp = (req) => {
const xForwardedFor = req.headers['x-forwarded-for'];
if (xForwardedFor) {
return xForwardedFor.split(',')[0];
} else {
return req.connection.remoteAddress;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
PHP:

function getRealIp() {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}
return $ip;
}

请注意,X-Forwarded-For 可能被恶意用户篡改,因此不应该被用于安全相关的决策中。如果你需要用于安全关键的应用,请确保使用其他的安全措施,例如可信的反向代理设置或者其他身份验证方法。

4-Web安全——通过x-forward-for获取真实ip(入侵溯源)

X-Forwarded-For(XFF)是用来识别通过HTTP代理或负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段

关于x-forward-for