使用boost::asio转发TCP流量时如何保留源IP地址?
这个场景我之前处理过,你的核心问题在于当前的代理是常规正向代理——代理作为中间层主动和本地目标应用建立连接,所以目标应用通过getpeername拿到的自然是代理的本地回环IP(127.0.0.1)。不用RAW套接字或者内核模块也能搞定,咱们用透明代理+Linux系统参数+iptables的组合就能实现需求,下面一步步说:
为什么当前目标应用看到的是127.0.0.1?
简单来说,你的代理逻辑是:
- 接受互联网客户端的连接(源IP是客户端公网IP)
- 代理自己新建一个socket,以本地IP(127.0.0.1)为源,连接到目标应用的端口
- 双向转发流量
目标应用的getpeername获取的是第二步中代理socket的源IP,也就是127.0.0.1,这是完全正常的正向代理行为。要让目标应用拿到原始客户端IP,需要让代理在和目标应用建立连接时,把源IP伪装成原始客户端的公网IP。
可行解决方案:绑定原始客户端IP+iptables流量重定向
1. 开启非本地绑定权限
Linux默认不允许进程绑定到非本机的IP,我们需要开启ip_nonlocal_bind参数,让代理可以绑定到原始客户端的公网IP:
# 临时生效 sysctl -w net.ipv4.ip_nonlocal_bind=1 # 永久生效,写入/etc/sysctl.conf echo "net.ipv4.ip_nonlocal_bind=1" >> /etc/sysctl.conf sysctl -p
2. 修改Boost.Asio的连接逻辑
在代理接受互联网客户端的连接后,先获取客户端的原始端点(IP+端口),然后在和目标应用建立连接时,先绑定到这个原始端点的IP,再发起连接。这样目标应用的getpeername就会拿到原始客户端的IP了。
示例代码片段:
// 假设已经接受了客户端连接,client_socket是已连接的客户端socket boost::asio::ip::tcp::endpoint client_endpoint = client_socket.remote_endpoint(); // 创建到目标应用的socket boost::asio::ip::tcp::socket target_socket(io_context); boost::asio::ip::tcp::endpoint target_endpoint( boost::asio::ip::address::from_string("127.0.0.1"), 8080 // 目标应用的端口 ); // 允许地址复用,避免端口占用问题 target_socket.set_option(boost::asio::socket_base::reuse_address(true)); // 绑定到原始客户端的IP(关键步骤) target_socket.bind(client_endpoint); // 连接到本地目标应用 target_socket.connect(target_endpoint); // 之后的双向流量转发和修改逻辑和你之前的实现一致
3. 处理目标应用的出站流量(避免绕过代理)
如果不做额外处理,目标应用的出站流量会直接发回原始客户端,完全绕过你的代理。你需要用iptables把目标应用的出站流量重定向到代理,同时在代理中维护连接映射,匹配对应的客户端连接。
比如目标应用的端口是8080,代理监听一个端口(比如8081)来接收目标应用的出站流量:
# 把目标应用的出站TCP流量重定向到代理的8081端口 iptables -t nat -A OUTPUT -p tcp -s 127.0.0.1 --sport 8080 -j REDIRECT --to-ports 8081
在代理中,你需要维护一个映射表(比如用目标应用的连接端点或者原始客户端IP/端口作为键),当接收到来自目标应用的重定向连接时,找到对应的互联网客户端socket,继续双向转发和流量修改。
关于你提到的其他方案的疑问
1. 是否需要用RAW套接字?
完全没必要。RAW套接字需要你手动实现整个TCP协议栈(握手、序列号、重传、拥塞控制等等),复杂度极高,容易引入各种bug,而且上面的方案已经能完美解决问题,何必自找麻烦?
2. 自行实现TCP握手再传递FD给Boost.Asio?
不需要。Boost.Asio已经支持绑定源端点(就是上面代码里的target_socket.bind(client_endpoint)),直接用这个方式就能实现源IP伪造,不需要自己手动处理TCP握手流程,既省事又稳定。
3. 目标进程静态编译无法用LD_PRELOAD?
没关系,咱们的方案完全不需要修改目标进程,也不需要注入,只需要通过系统参数和iptables配置,加上代理的逻辑修改就能实现,完美适配静态编译的目标进程。
内容的提问来源于stack exchange,提问作者Kracken

