IP伪装解密:后端服务究竟如何找到你?

前言

最近在做业务时,接到了一个小需求: 后端服务记录下请求客户端的Ip地址,方便后续的问题定位和追踪。虽然请求的客户端有后台web服务,移动app和设备,这里想追踪的其实是设备。

在这种图中,工程网关和子设备-电表,子设备-开关属于同一个Zigbee自组网中 ,而工程网关与云端服务以MQTT协议进行交互。客户通过后台网页服务和移动App对电表和开关进行控制指令的下发,但是偶尔会发生子设备没发生响应的情况。项目经理认为是我没有将指令投递到网关上。

此时的我汗流浃背,满头大汗。问题的关键是找到关键的问题

事情到了这里,我作为被告,我需要证明控制指令已经下发到了网关上。立马我问嵌入式同事是否有方法可以看到网关的运行日志。嵌入式同学说,你拿到设备的真实Ip地址,直接通过ssh命令即可登录到网关中看到网关的运行日志了。

此刻,我觉得离我沉冤昭雪的日子马上到来了。

愚昧之山

作为多年的CRUD工程师,我熟练掌握了Restful接口开发网络Socket编程。我写下了验证我清白的2行代码:

HTTP接口:

java 复制代码
@RequestMapping("/")
@ResponseBody
public void requestIn(@PathVariable String entry,
                      HttpServletRequest httpServletRequest,
                      HttpServletResponse httpServletResponse) {

        // 获取客户端 IP 地址
        String clientIp = request.getRemoteAddr();

}

TCP网络编程逻辑:

java 复制代码
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 获取客户端 IP 地址
        log.info("address: {}", ctx.channel.remoteAddress();
        ......
    }

当我急切的将其部署上去时,发现上述2个接口中获取的IP地址都是: 192.168.1.107

难道那里出现问题了么。我恍然想起,我的后端服务都是经过代理过的,这个IP是envoy那台机器的地址。(备注: Envoy是一个类似Nginx的服务代理,但是可动态配置,性能很高且灵活性,在云原生时代的指定CNCF代理服务)

绝望之谷

此时的我拿出服务的部署架构图,我仔细端详:

在常见的代理配置中,TCP和HTTP的代理配置也是有所不同的,这里需要普及下知识。

OSI七层模型

要聊几层代理,需要先看一下网络分层,标准的七层网络分层,也就是OSI七层模型。TCP/IP五层模型和TCP/IP四层模型是从OSI七层优化而来。

从下往上看,第四层为传输层、第七层为应用层。再来看看每层对应的常见协议:

四层对应的是TCP/UDP协议,也就常说的IP+端口。七层已经是非常具体的应用层协议了。因此,所谓四层就是基于IP+端口的负载均衡;七层就是基于URL等应用层信息的负载均衡。

四层代理

四层代理主要工作于OSI模型中的传输层,传输层主要处理消息的传递,而不管消息的内容。TCP就是常见的四层协议。

四层负载均衡只针对由上游服务发送和接收的网络包,而并不检查包内的具体内容是什么。四层负载均衡可以通过检查TCP流中的前几个包,从而决定是否限制路由。

因此,四层负载均衡的核心就是IP+端口层面的负载均衡,不涉及具体的报文内容。

七层代理

七层代理主要工作于OSI模型的应用层,应用层主要用来处理消息内容的。比如,HTTP便是常见的七层协议。

七层负载均衡服务器起到了反向代理的作用,Client端要先与七层负载均衡设备三次握手建立TCP连接,把要访问的报文信息发送给七层负载均衡。

七层负载均衡器基于消息中内容( 比如URL或者cookie中的信息 )来做出负载均衡的决定。之后,七层负载均衡器建立一个新的TCP连接来选择上游服务并向这个服务发出请求。

此时我的心凉半截,如此复杂啊!

开悟之坡

经过 3 分钟的自我内耗后,并喝了一杯热水♨️后,拾起书📚来补充下知识。

Web服务七层代理处理

在http/https的协议中,我们可以通过 X-Forwarded-For 从 Header 信息中获取到离服务端最近的 client 端的 IP 地址, 如果请求经过了多级代理且每级代理都开启此特性, 就可以获得真实有效的用户 IP。

Envoy配置:

yaml 复制代码
static_resources:
  listeners:
    # \u4e0d\u5e26psk\u8ba4\u8bc1\u7684\u8bbe\u5907\u7684atop\u5165\u53e3
    - name: listener_http_8888
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 8888
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http_8888
                access_log:
                  ......
                http_filters:
                  - name: envoy.filters.http.router      
                use_remote_address: true
                xff_num_trusted_hops: 1
                route_config:
                  ......

这里关键的配置主要有 2 行:

  • use_remote_address: x-forwarded-for (XFF) 是一个标准的代理标头,它表示请求在从客户端到服务器的路上经过的 IP 地址。 在代理请求之前,兼容代理会将最近客户端的 IP 地址附加到 XFF 列表中。

    仅在 use_remote_address HTTP 连接管理选项设置为 true 时,Envoy 才会追加到 XFF 。 这意味着如果 use_remote_address 为 false(这是默认值),则连接管理器将以不修改 XFF 的透明模式运行。

  • xff_num_trusted_hops: 如果 use_remote_address 为 true 并且 xff_num_trusted_hops 设置为大于零的值 N,则可信客户端地址是 XFF 右端的第 N 个地址。 (如果 XFF 包含的地址少于 N 个,Envoy 就会使用直接下行连接的源地址作为可信客户端地址。)

后端服务获取IP地址:

java 复制代码
@RequestMapping("/")
@ResponseBody
public void requestIn(@PathVariable String entry,
                      HttpServletRequest httpServletRequest,
                      HttpServletResponse httpServletResponse) {

        // 获取 X-Forwarded-For 头部值
        String xForwardedForHeader = httpServletRequest.getHeader("X-Forwarded-For");
        // 提取真实客户端 IP 地址
        String clientIpAddress = extractClientIpAddress(xForwardedForHeader);
        log.info("Client IP Address:{}", clientIpAddress);

}

TCP层代理处理

使用Proxy Protocol: proxy protocol 最早由 HAproxy 发明实现. 是一种类似 X-Forwarded-For 的基于应用层实现的方式. 由于是基于应用层, 所以需要客户端和服务器端同时支持此协议。

其如何工作的呢?proxy protocol 支持 v1v2 两个版本. v1 版本以明文的字符串发送数据, v2 版本以二进制格式发送. 简单而言, proxy protocol 实现主要是在建立 TCP 连接后, 在发送应用数据之前先将用户的 IP 信息发送到服务端. 我们可以简单理解为 TCP 三次握手完成后由 proxy protocol 的客户端立即将用户的 IP 信息发送过来。

Envoy配置:

yaml 复制代码
    - name: mqtt_service
      connect_timeout: 30s
      per_connection_buffer_limit_bytes: 32768
      health_checks:
        ......
      type: STATIC
      lb_policy: ROUND_ROBIN
      transport_socket:
        name: envoy.transport_sockets.upstream_proxy_protocol
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.proxy_protocol.v3.ProxyProtocolUpstreamTransport
          config:
            version: V2  
          transport_socket:
            name: envoy.transport_sockets.raw_buffer
      load_assignment:
        cluster_name: mqtt_service
        endpoints:
          ......

这里我使用的是V2版本的Proxy Protocol协议。

后端服务处理:

java 复制代码
@Override
    protected void initChannel(SocketChannel ch) {
        ChannelPipeline pipeline = ch.pipeline();

        if (context.isProxyEnabled()) {
            pipeline.addLast("proxy", new HAProxyMessageDecoder());
            pipeline.addLast("ipFilter", new ProxyIpFilter(context));
        } else {
            pipeline.addLast("ipFilter", new IpFilter(context));
        }
        ......
    }

在这段代码中,可以发现我是否经过代理做成配置项来分别处理了。为什么要这么做呢?

因为在MQTT协议中,客户端发起的第一个请求会是CONNECT报文。而经过Envoy代理后的服务,Envoy会在建立连接的时候发送且只发送一次Proxy Protocol报文。如果 2 者进行混用。会发现不代理的情况下,MQTT客户端连不上的情况。

此时,有的同学奇怪HAProxyMessageDecoder这个解码器放在最上面,对于正常请求有影响没?那让我们看下其逻辑:

bash 复制代码
    @Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // determine the specification version
        if (version == -1) {
            if ((version = findVersion(in)) == -1) {
                return;
            }
        }

        ByteBuf decoded;

        if (version == 1) {
            decoded = decodeLine(ctx, in);
        } else {
            decoded = decodeStruct(ctx, in);
        }

        if (decoded != null) {
            finished = true;
            try {
                if (version == 1) {
                    out.add(HAProxyMessage.decodeHeader(decoded.toString(CharsetUtil.US_ASCII)));
                } else {
                    out.add(HAProxyMessage.decodeHeader(decoded));
                }
            } catch (HAProxyProtocolException e) {
                fail(ctx, null, e);
            }
        }
    }

留意注释里强调的:finished变量,接着往下看:

java 复制代码
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        super.channelRead(ctx, msg);
        if (finished) {
            ctx.pipeline().remove(this);
        }
    }

解码成功后,后面就将它移除了,所以不用担心后面读取正常请求数据的时候会被这个decoder影响到。

效果演示

bash 复制代码
 Received msg: HAProxyMessage(protocolVersion: V2, command: PROXY, proxiedProtocol: TCP4, sourceAddress: 192.168.1.106, destinationAddress: 172.18.0.4, sourcePort: 60408, destinationPort: 5883, tlvs: [])
Received msg: HAProxyMessage(protocolVersion: V2, command: PROXY, proxiedProtocol: TCP4, sourceAddress: 192.168.1.109, destinationAddress: 172.18.0.4, sourcePort: 6106, destinationPort: 8883, tlvs: [])

拿到这个网关的IP地址后,我进到这个网关里查看其运行日志。和嵌入式进行疯狂的battle。最后证明我的程序是没问题的,是子设备信号指令查导致的。

开心一笑!深藏功与名。希望大家后续可以通过这个实战案例,学习如何获取这狡猾的IP地址。

相关推荐
点点滴滴的记录10 小时前
RPC核心实现原理
网络·网络协议·rpc
程思扬11 小时前
为什么Uptime+Kuma本地部署与远程使用是网站监控新选择?
linux·服务器·网络·经验分享·后端·网络协议·1024程序员节
海绵波波10712 小时前
Webserver(4.8)UDP、广播、组播
单片机·网络协议·udp
很透彻15 小时前
【网络】传输层协议TCP(下)
网络·c++·网络协议·tcp/ip
蝌蚪代理ip16 小时前
辩论赛——动态IP与静态IP的巅峰对决
网络·网络协议·tcp/ip·ip
dulu~dulu18 小时前
查缺补漏----用户上网过程(HTTP,DNS与ARP)
网络·网络协议·http
丶213619 小时前
【网络】HTTP(超文本传输协议)详解
网络·网络协议·http
坚持拒绝熬夜19 小时前
IP协议知识点总结
网络·笔记·网络协议·tcp/ip
C++忠实粉丝1 天前
计算机网络socket编程(1)_UDP网络编程实现echo server
linux·服务器·网络·c++·网络协议·计算机网络·udp
MetaverseMan1 天前
http防抖和ws防抖
网络·网络协议·http