Service Mesh透明代理TPROXY的技术原理

发布时间:2026/7/23 2:43:19
Service Mesh透明代理TPROXY的技术原理 在现代微服务架构中, Service Mesh 作为基础设施层为服务间通信提供了强大支持其中透明代理是一项关键技术这篇文章做了一个比较细致的分析彻底弄懂 TPROXY 透明代理/REDIRECT 的技术细节涉及到下面这些内容service-mesh 中 TPROXY 和 REDIRECT 模式内核提供的透明代理 TPROXY 功能自定义 iptables 规则链conntrack 状态跟踪IP_TRANSPARENT、SO_MARK 选项自定义路由表、策略路由istio 等透明代理的实现原理SO_ORIGINAL_DST、NAT 与 conntracksystemtap 内核观测实验环境介绍为了更好地理解透明代理的工作机制, 我们搭建了以下实验环境:两台主机, 每台运行一个容器容器间通过 Flannel vxlan 进行通信容器 A (IP: 172.100.1.2) 运行一个监听 8080 端口的 HTTP 服务容器 B (IP: 172.100.36.2) 作为 HTTP 请求的发起端此时的基于 vxlan 的 flannel 容器通信如下形式。在没有配置任何 iptables 规则时, 容器 B 可以正常访问容器 A 的 8080 端口服务。» sudo ip netns exec aaa curl http://172.100.1.2:8080/hello method: GET url: /hello peer addr: 172.100.36.0:60434这个 8080 端口的 http-server 服务会返回客户端的 IP 地址和端口。透明代理的需求在 Service Mesh 方案中我们需要引入一个 proxy 来做流量的代理它的角色有点类似于一个 nginx用于做正向和反向代理。普通的代理方式存在一些限制我们有两个朴素且原始的需求proxy 可以接管所有端口的入流量让后端对前面有一层代理无感后端服务可以获取到客户端的真实源 IP很明显如果通过普通的代理技术首先不能很好的监听所有的流量其次经过代理以后后端服务的请求源 ip 会变为本机 ip。为了解决这些问题, 我们需要利用 Linux 内核提供的 TPROXY 功能来实现真正的透明代理。TPROXY(Transparent Proxy)TPROXY 需要 Linux 内核 2.2 及以上版本的支持。它允许在用户空间程序中透明地代理流量使得应用程序无需知道是否存在代理服务器, 流量可以被透明地重定向到代理服务。此时我们来在 A 容器中新增一条 iptables 规则将非本地目的地址的 TCP 数据包通过 TPROXY 转发到本机监听的 15006 端口同时对数据包打上 0x539 的标记这个标记值你可以自己随意指定这里保持跟 istio 一致iptables -t mangle -A PREROUTING ! -d 127.0.0.1/32 -p tcp -j TPROXY --on-port 15006 --on-ip 0.0.0.0 --tproxy-mark 0x539此时 B 再次 curl A 容器的服务现象变为了 B 发送给 A 的 SYN 包没有回复 SYNACKB 一直重传 SYN。是不是因为 A 容器内并没有一个服务监听 15006 端口导致没有回复 SYNACK 呢启动一个服务监听 15006 端口试试。use std::net::SocketAddr; use nix::sys::socket; use nix::sys::socket::sockopt; use tokio::net::{TcpSocket, TcpStream}; const PORT: u16 15006; const LISTENER_BACKLOG: u32 65535; #[tokio::main] async fn main() - anyhow::Result() { let listen_addr format!(0.0.0.0:{}, PORT).parse().unwrap(); println!(Listening on: {}, listen_addr); let socket TcpSocket::new_v4()?; // #[cfg(any(target_os linux))] // socket::setsockopt(socket, sockopt::IpTransparent, true)?; socket.bind(listen_addr)?; let listener socket.listen(LISTENER_BACKLOG)?; while let Ok((mut downstream_conn, _)) listener.accept().await { println!(accept new connection, peer[{:?}]-local[{:?}], downstream_conn.peer_addr()?, downstream_conn.local_addr()?); tokio::spawn(async move { // 处理连接这里调用 sleep let result handle_connection(downstream_conn).await; match result { Ok(_) { println!(connection closed); } Err(err) { println!(connection closed with error: {:?}, err); } } }); } Ok(()) } async fn handle_connection(mut downstream_conn: TcpStream) - anyhow::Result() { tokio::time::sleep(tokio::time::Duration::from_secs(u64::MAX)).await; Ok::(), anyhow::Error(()) }B 再次 curl A 容器的服务现象依旧是一直重传 SYN。为了解决这个问题需要介绍另外一个重要的知识点IP_TRANSPARENT。IP_TRANSPARENT介绍IP_TRANSPARENT是一个 Linux 中的 socket 选项, 主要用于实现透明代理功能它具有以下两个关键作用接收 TPROXY 重定向的连接允许应用程序接收通过 iptables TPROXY 规则重定向的连接流量。这使得代理服务器可以无缝拦截和处理原本不是发往它的网络流量绑定到非本地 IP通常情况下socket 只能绑定到主机自身的 IP 地址。但启用 IP_TRANSPARENT 选项后应用程序可以绑定到任意 IP 地址即使该地址不属于本机网络接口以下是修改后的 Rust 代码监听 15006 端口并设置 IP_TRANSPARENT 选项在 B 容器再次 curl 以后此时可以看到三次握手可以成功了。通过日志我们可以看到目前 tproxy 拿到的客户端 ip 也是正确的是 B 容器所在节点的flannel.1的 ip172.100.36.0.$ ./target/debug/tproxy-rs Listening on: 0.0.0.0:15006 accept new connection, peer[172.100.36.0:45966]-local[172.100.1.2:8080]大家可能会注意到尽管我们的 tproxy-rs 程序实际上监听的是 15006 端口但连接的本地地址却显示为 8080 端口。这种 欺骗 效果正是 IP_TRANSPARENT 选项和 iptables 规则共同作用的结果。不过因为我们没有真正把流量代理到目标服务所以 curl 请求的 http 响应是不会返回的。IP_TRANSPARENT 的内核代码介绍为什么仅仅给套接字添加 IP_TRANSPARENT 选项就能使握手成功呢这需要从 linux 内核源码角度去理解文件位于net/netfilter/xt_TPROXY.c。tproxy是一个 netfilter 框架下一个内核模块target 处理函数是tproxy_tg4_v1static struct xt_target tproxy_tg_reg[] __read_mostly { { .name TPROXY, .family NFPROTO_IPV4, .table mangle, .target tproxy_tg4_v1, .revision 1, .targetsize sizeof(struct xt_tproxy_target_info_v1), .checkentry tproxy_tg4_check, .hooks 1 NF_INET_PRE_ROUTING, .me THIS_MODULE, }, } module_init(tproxy_tg_init); module_exit(tproxy_tg_exit); MODULE_DESCRIPTION(Netfilter transparent proxy (TPROXY) target module.);tproxy_tg4_v1真正调用tproxy_tg4函数static unsigned int tproxy_tg4(struct net *net, struct sk_buff *skb, __be32 laddr, __be16 lport, u_int32_t mark_mask, u_int32_t mark_value) { const struct iphdr *iph ip_hdr(skb); struct udphdr _hdr, *hp; struct sock *sk; hp skb_header_pointer(skb, ip_hdrlen(skb), sizeof(_hdr), _hdr); if (hp NULL) return NF_DROP; // 查找是否存在已经建连的 socket sk nf_tproxy_get_sock_v4(net, skb, iph-protocol, iph-saddr, iph-daddr, hp-source, hp-dest, skb-dev, NF_TPROXY_LOOKUP_ESTABLISHED); laddr nf_tproxy_laddr4(skb, laddr, iph-daddr); if (!lport) lport hp-dest; if (sk sk-sk_state TCP_TIME_WAIT) sk nf_tproxy_handle_time_wait4(net, skb, laddr, lport, sk); else if (!sk) // 没有找到已经建连的 socket查找 tproxy 重定向地址/端口的 listener /* no, theres no established connection, check if * theres a listener on the redirected addr/port */ sk nf_tproxy_get_sock_v4(net, skb, iph-protocol, iph-saddr, laddr, hp-source, lport, skb-dev, NF_TPROXY_LOOKUP_LISTENER); // 如果 tproxy 目标 socket 存在且设置了 IP_TRANSPARENT则返回 NF_ACCEPT if (sk nf_tproxy_sk_is_transparent(sk)) { // 设置 skb 的 mark skb-mark (skb-mark ~mark_mask) ^ mark_value; pr_debug(redirecting: proto %hhu %pI4:%hu - %pI4:%hu, mark: %x\n, iph-protocol, iph-daddr, ntohs(hp-dest), laddr, ntohs(lport), skb-mark); nf_tproxy_assign_sock(skb, sk); return NF_ACCEPT; } // 如果 tproxy 目标 socket 不存在或者 socket 存在但没有设置 IP_TRANSPARENT返回 NF_DROP pr_debug(no socket, dropping: proto %hhu %pI4:%hu - %pI4:%hu, mark: %x\n, iph-protocol, iph-saddr, ntohs(hp-source), iph-daddr, ntohs(hp-dest), skb-mark); return NF_DROP; }可以看到这段代码的逻辑是先找是否有已经建连好的连接没有找到已经建连的 socket查找 tproxy 重定向地址/端口的 listener如果 tproxy 目标 socket 存在且设置了 IP_TRANSPARENT则返回 NF_ACCEPT如果 tproxy 目标 socket 不存在或者 socket 存在但没有设置 IP_TRANSPARENT返回 NF_DROP为了验证我们之前的结论我们可以使用 SystemTap 来深入分析 tproxy_tg4 函数的行为。通过对比设置和未设置 IP_TRANSPARENT 选项时 tproxy_tg4 函数的返回值脚本如下probe begin { printf(probe begin!\n) } probe module(xt_TPROXY).function(tproxy_tg4) { printf(Entering tproxy_tg4, args: %s\n, $$parms) iphdr __get_skb_iphdr($skb); saddr format_ipaddr(__ip_skb_saddr(iphdr), %{ AF_INET %}) daddr format_ipaddr(__ip_skb_daddr(iphdr), %{ AF_INET %}) tcphdr __get_skb_tcphdr($skb); dport __tcp_skb_dport(tcphdr); sport __tcp_skb_sport(tcphdr); printf([skb]: [src]%s:%d - [dst]%s:%d\n, saddr, sport, daddr, dport); } probe module(xt_TPROXY).function(tproxy_tg4).return { printf(Exiting tproxy_tg4, return : %d\n, $return); }未设置 IP_TRANSPARENT 时probe begin! Entering tproxy_tg4, args: net0xffff9f7a9b9a3600 skb0xffff9f7da21c9e00 laddr0x0 lport0x9e3a mark_mask0xffffffff mark_value0x539 [skb]: [src]172.100.36.0:40756 - [dst]172.100.1.2:8080 Exiting tproxy_tg4, return : 0我们可以观察到mark 值确实被设置为 0x539与我们的预期一致。返回值为 0对应内核中的 NF_DROP表示这个数据包被丢弃。#define NF_DROP 0 #define NF_ACCEPT 1设置 IP_TRANSPARENT 后$ sudo stap -g tproxy_tg4_test.stp probe begin! Entering tproxy_tg4, args: net0xffff9f7a9b9a3600 skb0xffff9f7b8c83e200 laddr0x0 lport0x9e3a mark_mask0xffffffff mark_value0x539 [skb]: [src]172.100.36.0:38938 - [dst]172.100.1.2:8080 Exiting tproxy_tg4, return : 1设置 IP_TRANSPARENT 后tproxy_tg4 返回值为 1对应内核中的 NF_ACCEPT表示这个数据包被接受。完整的代码见github.com/arthur-zhan…下一步是让我们的 tproxy-rs 程序与真正的后端服务建立连接实现完整的透明代理功能。理想情况下tproxy-rs 将作为中间人将流量从客户端无缝转发到 http-server。这个过程可以描述如下为了实现完整的透明代理功能我们需要让 tproxy-rs 程序执行以下步骤接收来自客户端的连接与后端 http-server 建立新的连接在两个连接之间转发数据connect 如何指定源 ip伪装 IP 地址在网络编程中通常 connect() 操作不需要显式指定源 IP 地址操作系统会根据路由规则自动选择合适的源 IP。# 在容器 A 内请求本机服务 curl http://172.100.1.2:8080/hello对应的 tcpdump 抓包如下可以看到此时选择的网络接口是 lo源 ip 地址是本机 ip172.100.1.2这很合理。为了让后端服务器感知到真实的源 IP (172.100.36.0)我们需要在建立连接时强制指定源 IP。虽然通常 connect 操作不需要指定源 IP但在这种特殊情况下我们可以使用 bind 来实现。运行代码后出现绑定失败的错误因为 172.100.36.0 这个 IP 地址并不属于 A 容器的网络命名空间。内核对应的源码如下在net/ipv4/af_inet.c的inet_bind函数我们重点看红框中的部分代码如果当前 bind 的地址无法被分配则开始判断如果系统不允许非本地绑定(sysctl_ip_nonlocal_bind 为 0)且套接字没有设置 freebind 或 transparent 标志且提供的 IP 地址不是 INADDR_ANY且地址类型不是本地、多播或广播则返回 EADDRNOTAVAIL 错误如果想让 bind 成功我们可以对 socket 设置 IP_TRANSPARENT 选项。设置此选项后socket 将被允许绑定到非本地 IP 地址。但 curl 并没有正常返回。我们在 A 容器中抓包发现与 172.100.1.2:8080 的三次握手有问题从 lo 收到了 SYN 包回复的 SYN ACK 是从 eth0 网卡随后收到了 RST 包。这个问题的过程如下我们伪造了源 IP 地址发送请求没有问题。当需要回复 SYNACK 包时内核会查询系统的主路由表来决定使用哪个网卡发送数据包。在这种情况下由于 172.100.36.0 不是本机的 IP 地址内核选择了默认路由即通过 eth0 网卡发送。为了解决这个问题我们需要实现一种特殊的处理方式使内核将 172.100.36.0 视为本机 IP 地址。这就需要用到策略路由Policy Routing策略路由Policy-based Routing根据路由决策的方式不同路由可以分为策略路由根据 IP 源地址、端口、报文长度等灵活来进行路由选择普通路由仅根据报文的目的地址来选择出接口和下一跳的地址策略路由更加灵活功能更加强大比如你可以通过策略路由实现将 SSH 流量通过一个网关发送而 HTTP 流量通过另一个网关发送从而实现负载均衡。策略路由的使用分为两部分自定义路由表和匹配策略。Linux 系统默认有三个路由表:本地路由表(Local table路由表编号 255由内核自动维护负责本地接口地址、广播地址的路由主路由表(Main table路由表编号 254负责单播目的地的路由我们route -n默认会查这个表默认路由表(Default table路由表编号 253一般都是空的除了上述默认表, 管理员还可以添加自定义路由表, 表 ID 取值范围是 1~252。自定义路由表的创建和使用与内置表没有什么区别可以使用 ip route 命令将路由添加和查看自定义路由表。# 新增规则到编号为 128 的自定义路由表表 $ ip route add 192.168.10.0/24 via 172.100.1.1 dev eth0 table 128 # 查看编号为 128 的自定义路由表 $ ip route list table 128 192.168.10.0/24 via 172.100.1.1 dev eth0除了自定义路由表策略路由另外一个重要的组成部分是匹配策略。策略路由提供了很多种类型的匹配规则比如from、to、tos、fwmark、iif和oif。比如from根据数据包的源地址来匹配规则fwmark根据数据包的防火墙标记(firewall mark)来匹配规则。有了上面的基础我们来看一下策略路由如何在透明代理应用。以 istio 为例它创建一个编号为 133 的自定义路由表# 创建一条路由规则到编号为 133 的路由表 $ sudo ip route add local 0.0.0.0/0 dev lo table 133这条路由表项表示所有目的地为 0.0.0.0/0即所有地址的数据包都通过 lo本地回环接口处理。同时增加一条路由策略规则# 增加策略路由 $ sudo ip rule add fwmark 0x539 lookup 133fwmark 0x539 表示匹配防火墙标记为 0x539 的数据包如果匹配成功, 就查找路由表 133 来确定如何路由这个数据包。这个时候策略路由是有了这还不够我们还需要对包打上标记 0x539这样才可以命中策略路由规则。SO_MARK选项SO_MARK 是一个强大的套接字选项它允许我们给通过特定套接字发送的所有数据包打上标记。以下是 SO_MARK 的典型使用方式uint32_t mark 0x539; // 设置标记值为 0x539 setsockopt(sockfd, SOL_SOCKET, SO_MARK, mark, sizeof(mark));通过这样的设置从该套接字发出的所有数据包都会带有 0x539 这个标记。这个标记可以被后续的网络处理过程如 iptables 规则和路由决策识别和利用。我们来测试一下修改 tproxy-rs 的代码新增这个值。我们测试一下实际上没有什么变化。这是因为我们只是通过 SO_MARK 我们只是对发送的 SYN 包打了标记回复的 SYNACK 并没有这个标记这样这个回复的 SYNACK 就不会命中策略路由出口路由依旧选择了 eth0。为了验证一下这个结论我们先开启 iptables 的 trace 日志。这些规则将记录所有 TCP 数据包在 iptables 规则链中的流转过程。# iptables -t raw -A PREROUTING -p tcp -j TRACE # iptables -t raw -A OUTPUT -p tcp -j TRACE通过分析 trace 日志我们可以看到发起的 SYN 包通过 SO_MARK 选项成功地带上了 0x539 标记。回复的 SYNACK 包没有携带 0x539 标记。作为响应包SYNACK 是由内核自动生成的没有经过我们的应用程序处理它没有被设置 SO_MARK。没有标记的 SYNACK 包无法匹配我们的策略路由规则内核使用默认路由表进行路由决策选择了 eth0 作为出口。要解决这个问题我们需要确保 SYNACK 包也能带上正确的标记。这可以通过使用 conntrack 模块来实现conntrack 可以跟踪整个连接的状态。我们可以配置 iptables 规则使用 conntrack 模块保存和恢复连接的标记首先我们需要弄清楚「连接标记(Connection Mark)」与「数据包标记(Packet Mark)」的区别数据包标记 (Packet Mark)只应用于单个数据包连接标记 (Connection Mark)存储在连接跟踪表中跨越整个连接的生命周期连接跟踪标记通常在数据包进入时被设置。比如-m connmark --mark 0x539的作用是匹配那些属于入站时被打上 0x539 标记的连接的所有数据包。-m mark --mark 0x539的作用是匹配被打上 0x539 标记的单个数据包conntrack 模块提供了几个关键的操作来管理这些标记--set-mark / --set-xmark设置单个数据包的标记 示例iptables -t mangle -A PREROUTING -j MARK --set-mark 0x539--save-mark将数据包的标记保存到连接跟踪表中 示例iptables -t mangle -A PREROUTING -j CONNMARK --save-mark--restore-mark从连接跟踪表中恢复标记到数据包 示例iptables -t mangle -A OUTPUT -j CONNMARK --restore-mark接下来就是要用 conntrack 模块匹配数据包的连接状态。使得本来不带 MARK 的 SYNACK 包也能打上 MARK使得包可以走到 133 策略路由规则。因为 istio 的 iptables 规则为了支持更多的特性比较复杂为了更清楚的知道透明代理相关的功能我简化了最需要的几条规则完整的 iptables 规则如下创建自定义链 iptables -t mangle -N MY_INBOUND # 将所有入站 TCP 流量导向自定义链 iptables -t mangle -A PREROUTING -p tcp -j MY_INBOUND # 对已标记的包直接返回, 避免重复处理 iptables -t mangle -A MY_INBOUND -p tcp -m mark --mark 0x539 -j RETURN # 对已建立的连接设置标记 iptables -t mangle -A MY_INBOUND -p tcp -m conntrack --ctstate RELATED,ESTABLISHED -j MARK --set-xmark 0x539/0xffffffff # 使用 TPROXY 重定向非本地流量到代理端口 iptables -t mangle -A MY_INBOUND ! -d 127.0.0.1/32 -p tcp -j TPROXY --on-port 15006 --on-ip 0.0.0.0 --tproxy-mark 0x539/0xffffffff # 保存数据包标记到连接 iptables -t mangle -A PREROUTING -p tcp -m mark --mark 0x539 -j CONNMARK --save-mark --nfmask 0xffffffff --ctmask 0xffffffff # 恢复连接标记到数据包 iptables -t mangle -A OUTPUT -p tcp -m connmark --mark 0x539 -j CONNMARK --restore-mark --nfmask 0xffffffff --ctmask 0xffffffff ro通过上面的规则我们可以做到入流量被正确标记和重定向连接状态被跟踪出流量能够恢复正确的标记通过这种配置, 我们可以确保所有相关的数据包, 包括 SYNACK, 都能被正确标记并通过策略路由规则进行处理。包在 iptables 规则链中的流转全过程我们来梳理一下整个包的过程先来看前半部分也就是红框中的流量部分。内核收到 B 容器发过来的 SYN 包SRC 172.100.36.0:12345 DST 172.100.1.2:8080):初始时SYN 包不携带任何 MARK 标记。当该包经过 MY_INBOUND 链时它匹配到该链的第三条规则。这条规则执行以下操作将 TCP 包劫持并重定向至 15006 端口为包添加 0x539 MARK 标记终止在 PREROUTING 链中的后续匹配过程完成 PREROUTING 链的处理后系统会进行路由判断以确定该包的目标地址是否为本机。 在本例中包的目标 IP 地址为 172.100.1.2。经过路由表匹配系统判定这是一个发往本机的数据包。内核回复 SYNACK 给对端内核向对端发送 SYNACK 响应包。此时SYNACK 包不携带任何 MARK 标记conntrack 连接也没有 MARK 标记。因此该包不会匹配 OUTPUT 链中的任何规则。 经过出路由规则判断后SYNACK 包将直接通过 eth0 接口发送出去。内核收到对端的 ACK 包当收到 ACK 包时包会首先经过 MY_INBOUND 链的第二条规则并被设置为 MARK 0x539。随后包会匹配 MY_INBOUND 链的第三条规则通过 TPROXY 劫持到 15006 端口。接着系统进行路由决策判断该包的目标地址是否为本机发往本机继续处理。内核收到 HTTP 包内容内核收到 PSH 包的处理流程与收到 ACK 包的一致。接下来我们来看 tproxy-rs 与后端服务器通信的部分即下图红框所示的部分。connect 发送 SYN当我们使用 connect 发送 SYN 包时会设置 SO_MARK将包标记为 0x539 MARK。然而此时 conntrack 的 MARK 仍为空因此该包不会匹配 OUTPUT 链中的任何规则。经过路由决策后SYN 包将通过 lo 接口发出。内核收到 SYN上一步发出去的 SYN 包由于是在本机依旧是本机内核处理将会经历以下步骤命中 MY_INBOUND 的第一条规则因为包含 0x539 MARK跳出 MY_INBOUND 链经过 PREROUTING 链中继续处理并匹配到 --mark 0x539 规则执行 save-mark 操作将包的 MARK 保存到连接的 MARK 中。完成 PREROUTING 链的处理后包进入路由规则匹配阶段。系统发现这是一个发往本机的包随后将其交给本机处理。内核回复 SYNACK回复的 SYNACK 自然是不带 0x539 这个 MARK 的但由于 conntrack 关联的连接具有该 MARKSYNACK 包会匹配 OUTPUT 链中的条件触发 --restore-mark 操作将连接的 MARK 应用到 SYNACK 包上。这样 SYNACK 数据包就有了 0x539 这个 MARK它将在后续的路由匹配中命中我们自定义的 133 路由表。尽管目的地址172.100.36.0本来不是本机地址SYNACK 包本应通过 eth0 接口发出但由于策略路由的使用使得原本非本机的目的地址172.100.36.0被当作本地地址来处理。内核收到 SYNACK当内核收到 SYNACK 包后将会经过以下 iptables 链的处理命中 MY_INBOUND 的第一条规则跳出 MY_INBOUND 链继续 PREROUTING 链在 PREROUTING 链中包的 MARK 被保存到 conntrack 的 MARK 中经过策略路由匹配后包被判定为发往本机并交由本机处理。内核回复 ACK这个比较简单过程如下图所示。剩下的流程与之前基本上差不多就不再赘述。至此我们就把 TPROXY 模式所涉及的方方面面介绍清楚了。完整代码见 github.com/arthur-zhan…除 TPROXY 的另外的选择REDIRECT 模式相比于 TPROXY 复杂的 iptables 规则REDIRECT 模式要简单得多。只需要一条规则即可实现iptables -t nat -A PREROUTING -p tcp -j REDIRECT --to-ports 15006然而这里存在一个大问题经过 NAT 后流量被劫持到 15006 端口的服务时在代理应用中获取到的 TCP 连接的目标端口变为了我们监听的 15006 端口。accept new connection, peer[172.100.36.0:39882]-local[172.100.1.2:15006]这样一来我们如何知道将这个请求转发到后端服务的哪个端口呢使用 conntrack 获取原始目标端口NAT 的功能实际上是通过 conntrack 实现的我们可以通过 conntrack 来获取原始的目标端口。通过查看 conntrack我们可以看到如下映射172.100.36.0:39882-172.100.1.2:8080被 NAT 到172.100.1.2:15006 - 172.100.36.0:39882conntrack -L tcp 6 431997 ESTABLISHED src172.100.36.0 dst172.100.1.2 sport39882 dport8080 src172.100.1.2 dst172.100.36.0 sport15006 dport39882 [ASSURED] mark0 use1通过这种方式我们可以确定将请求转发到后端服务的正确端口。不过我们不需要直接操作 conntracksocket 提供了一个 api 可以用来获取典型的用法如下struct sockaddr_in orig_dst; socklen_t orig_dst_len sizeof(orig_dst); getsockopt(sock, SOL_IP, SO_ORIGINAL_DST, orig_dst, orig_dst_len); printf(Original destination: %s:%d\n, inet_ntoa(orig_dst.sin_addr), ntohs(orig_dst.sin_port));这个功能是由内核 netfilter conntrack 提供对应的与源码如下。于是我们可以修改对应的 rust 代码这样我们就可以实现了代理的功能不过这个时候后端获取的来源 ip 是本地地址 127.0.0.1。$ curl http://172.100.1.2:8080/hello method: GET url: /hello peer addr: 127.0.0.1:41757完整的代码见github.com/arthur-zhan…至此REDIRECT 模式就介绍结束了。后记TPROXY 相比于 REDIRECT 的优势是少了一个 DNAT 的过程且后端可以获取到真实的客户端 IP但是实现相对复杂一点点。istio 两个模式都提供了实测 istio 这两种模式性能差别不大。