likes
comments
collection
share

【雨夜】request 获取的就是真实的ip么?

作者站长头像
站长
· 阅读数 11

最近有一个项目(调用第三方接口)需要添加验证白名单功能,周五和老大在探讨的时候,我们说到了获取ip ,我当时是这么说的

我:

我让header 传递ip过来

老大:

直接request 直接获取ip不就行了么

老大意思应该是直接调用如下这类代码搞定

package com.example.winterholity.util;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class IpUtil {
    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
                if (ipAddress.equals("127.0.0.1")) {
                    // 根据网卡取本机配置的IP
                    InetAddress inet = null;
                    try {
                        inet = InetAddress.getLocalHost();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                    ipAddress = inet.getHostAddress();
                }
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress="";
        }
        // ipAddress = this.getRequest().getRemoteAddr();
        return ipAddress;
    }
}


但是我感觉不一定

问题

这个方法就一定获取的是真实的IP么?

先搭建一个环境再说

【雨夜】request 获取的就是真实的ip么?

简单测试 ip获取成功

但是在互联网 请求 nginx 的接口,业务系统获取的是 nginx的ip,这就不对了,应该是返回互联网的IP 才对

如果request.getRemoteAddr() 就可以获取真实ip,那就不会有x-forwarded-for 等属性的事了

request.getRemoteAddr() 和 request.getRemoteHost() 区别

System.out.println("request.getRemoteAddr(): " + request.getRemoteAddr());   
System.out.println("request.getRemoteHost(): " + request.getRemoteHost());

前一个是获得客户端的ip地址 

后一个是获得客户端的主机名

request.getRemoteAddr() 里面的ip 是怎么获取的

public String getRemoteAddr() {  
    if (this.remoteAddr == null) {  
        this.coyoteRequest.action(ActionCode.REQ_HOST_ADDR_ATTRIBUTE, this.coyoteRequest);  
        this.remoteAddr = this.coyoteRequest.remoteAddr().toString();  
    }  
  
return this.remoteAddr;  
}

不一定获取的是真实的IP,可能获取的是 nginx等中间件的ip,甚至伪造的也是可能的

因为负载均衡 可能丢弃真实ip,有什么办法进行

  1. 应用层方法
  2. 传输层方法
  3. 网络层方法

应用层

在这一层,Web 协议的制定者们想到了一个巧妙的办法:既然 HTTP 协议比较灵活,那就可以设计一个新的 header,用来传递真实源 IP,它就是 X-Forwarded-For。这个标准最初是 Squid 的开发工程师提出的,很快受到了业界的支持,各种 web 服务器都早已支持了这个 header。

他是通过X-Forwarded-For 直接传递进去

不过,X-Forwarded-For 这个标准,虽然用一种相对低的成本解决了“服务器不能获取真实源 IP”的问题,但它本身还是有一些不足的

源 IP 信息的伪造问题

这也是它最大的问题,因为这个头部本身没有任何安全保障机制,攻击者完全可以任意构造 X-Forwarded-For 信息来欺骗服务端。比如,如果攻击者知道服务端对某个 IP 段来的请求进行特殊处理(比如会提供更大力度的优惠券),那么攻击者就可以在发送请求时候,构造一个 X-Forwarded-For 头部,它的值就是这个段内的某个 IP。当服务端收到请求时,认为 X-Forwarded-For 里排在最左边的 IP 是真实 IP,而事实上这个是伪造出来的,所以可想而知,这个请求就可以获取它原本不应该得到的特权了。

重复的 X-Forwarded-For 头部

HTTP 协议本身并不严格要求 header 是唯一的,所以有些情况下,HTTP 请求可能会携带两个或者更多的 X-Forwarded-For 头部。

造成这个现象的原因是,某些代理或者 LB 并不是严格按照协议规定的,把 IP 附加到已有的 X-Forwarded-For 头部,而是自己另起一个 X-Forwarded-For 头部,那么这样就导致了重复的 X-Forwarded-For。

对于服务端来说,在收到这种请求的时候,可能会导致信息识别上的错乱。比如某些服务端的逻辑是读取第一个 X-Forwarded-For,而另外一些服务端程序可能是读取最后一个,并无定法

不能解决 HTTP 和邮件协议以外的真实源 IP 获取的需求

X-Forwarded-For 解决了 HTTP 的透传真实源 IP 的需求,但是事实上,很多应用并不是基于 HTTP 协议工作的,比如数据库、FTP、syslog 等等,这些场景也需要“获取真实源 IP”这个功能。但是前面说的 X-Forwarded-For,只能为 HTTP/ 邮件协议所用,那其他这么多协议和应用难道就成了没妈的孩子,永远不能获取到真实源 IP 了吗?

这时候,传输层的方法就上场了。

传输层方法

TOA 和 TCP Options

TOA 全称是 TCP Option Address,它是利用 TCP Options 的字段来承载真实源 IP 信息,这个是目前比较常见的第四层方案。不过,这并非是 TCP 标准所支持的,所以需要通信双方都进行改造。也就是:

对于发送方来说,需要有能力把真实源 IP 插入到 TCP Options 里面。

对于接收方来说,需要有能力把 TCP Options 里面的 IP 地址读取出来。

可见,TCP Options 是可变长的,最长为 40 字节(第一列的偏移量 20 到 60 字节之差)。每个 Option 项由三部分组成:

【雨夜】request 获取的就是真实的ip么?

【雨夜】request 获取的就是真实的ip么?

扩展 SYN 报文的 TCP Options,让它携带真实源 IP 信息。这个需要对中间的 LB 和后端服务器都进行小幅的配置改造。

Proxy Protocol

这个方案是 HAProxy(另外一个广泛应用的反向代理软件)工程师提出的。它的实现原理是这样的:

  1. 客户端在 TCP 握手完成之后,在应用层数据发送之前,插入一个包,这个包的 payload 就是真实源 IP。也就是说,在三次握手后,第四个包不是应用层请求,而是一个包含了真实源 IP 信息的 TCP 包,这样应用层请求会延后一个包,从第五个包开始。
  2. 服务端也需要支持 Proxy Protocol,以此来识别三次握手后的这个额外的数据包,提取出真实源 IP。

这是一个逐步被各种反向代理和 HTTP Server 软件接纳的方案,可以在不改动代码或者内核配置的情况下,只修改反向代理和 HTTP Server 软件的配置就能做到。

为什么会有 x-forwarded-for

我们第三方调用不是http 就是webservice,其他的很少(其中http是大多数),针对这种情况,我们都会在nginx配置如下

如果发送请求设置X-Forwarded-For=xxx,然后nginx 设置

location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header Host $http_host;
    proxy_set_header X-NginX-Proxy true;
 
    proxy_pass http://127.0.0.1:9009/;
    proxy_redirect off;
}

后台 获取的ip为 xxx的IP,实际获取的ip为xxx

上面这个配置有问题么?

proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;

这样配置之后,安全性确实提高了,但是也导致请求到达 Nginx 之前的所有代理信息都被抹掉,无法为真正使用代理的用户提供更好的服务。还是应该弄明白这中间的原理,具体场景具体分析。

应该用

location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-NginX-Proxy true;
 
    proxy_pass http://127.0.0.1:9009/;
    proxy_redirect off;
}

思考题

我有一个场景也是用了本文章的内容,您看看怎么实现

背景

我们对接了很多的第三方平台接口,需要部署我们的agent,但是第三方接口经常出问题,但是用户看到有问题了,肯定找我们,或者对我们的影响不好

第三方接口都是http请求,但是其中 怎么转发的不清楚

【雨夜】request 获取的就是真实的ip么?

然后我们就要找原因,找到是2,3 中间的问题,但是这两方都不认为是自己的问题,让我们找证据,他们的网络环境,对我们来说是黑盒,这种情况,你怎么定位问题出现在哪

需求

  1. agent要验证白名单ip
  2. 需要验证是哪个节点 出现了问题,方便找对应的人
  3. 需要满足以后第三方接口(未来是未知的) 各种情况

分析

其实如果LB 都是我们自己维护 ,当然怎么搞都可以实现,但是往往调用第三方接口,LB 不一定怎么实现的,有的 直接header 给你过滤掉都是有可能的,这就给我们增加了难度