HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 代理技术全栈手册

    • 代理技术全栈手册 - HiHuo
    • 原理篇

      • 第01章 代理是什么:正向 / 反向 / 透明 / 隧道的统一模型
      • 第02章 代理与网络层级:L3 / L4 / L5 / L7 在哪里截断流量
      • 第03章 一个请求穿过代理的一生:连接生命周期全景
    • 协议篇

      • 第04章 HTTP 代理协议:绝对 URI、CONNECT 隧道、转发头与连接池
      • 第05章 HTTPS 与 TLS 代理:终止 / 透传 / MITM / SNI / mTLS
      • 第06章 SOCKS 协议:SOCKS4/4a/5 与 UDP ASSOCIATE 报文级解析
      • 第07章 HTTP/2、gRPC 与 HTTP/3(QUIC) 代理的挑战
      • 第08章 代理自动配置:PAC / WPAD / 系统代理 / NO_PROXY
    • 层级与转发篇

      • 第09章 L4 代理:TCP/UDP 转发与连接级负载均衡
      • 第10章 L7 代理:协议感知与基于内容的路由
      • 第11章 透明代理:iptables REDIRECT/DNAT、TPROXY 与 eBPF 劫持
      • 第12章 数据搬运的艺术:splice / sendfile / 零拷贝 / io_uring
    • 组件横评篇

      • 第13章 Nginx / OpenResty:反向代理、upstream 与 Lua 可编程
      • 第14章 HAProxy:L4/L7、ACL、健康检查与 stick table
      • 第15章 Envoy:xDS 动态配置与 filter chain,为何是云原生数据面
      • 第16章 Traefik / Caddy:自动服务发现与自动 HTTPS
      • 第17章 Squid 与正向/缓存代理:企业出网、缓存与审计
      • 第18章 mitmproxy:抓包、改包、脚本化调试
      • 第19章 内网穿透与隧道:frp / gost / SSH 隧道 / ngrok
      • 第20章 科学上网生态的技术原理(技术中立)
    • 多语言手写篇

      • 第21章 Go:100 行手写 HTTP/CONNECT + SOCKS5 代理
      • 第22章 Rust:基于 tokio 的高性能 TCP 代理
      • 第23章 Python:asyncio 实现,适合调试与脚本
      • 第24章 C:epoll 裸写与零拷贝,及语言选型对比
    • 容器与K8s篇

      • 第25章 Docker 里的代理:HTTP_PROXY、build/pull 与 daemon 配置
      • 第26章 Sidecar 与流量劫持:Istio init-container 的 iptables 原理
      • 第27章 Ingress 与南北流量:Ingress-nginx 与 Gateway API
      • 第28章 Egress 与出网治理:出口网关、registry mirror、审计
      • 第29章 Service Mesh 数据面:Envoy Sidecar 全链路
    • 进阶篇

      • 第30章 可编程代理:Lua / Wasm / eBPF / xDS,代理的"软件定义"
      • 第31章 性能调优:并发模型、连接池、超时与重试、压测
      • 第32章 排错决策树:502 / 504 / 握手失败 / 环路 / 泄漏
      • 第33章 代理安全:开放代理、SSRF、凭证泄漏与攻击面
    • 底层机制篇

      • 第34章 代理的背压与流控:一个代理最难的部分
      • 第35章 socket 与 TCP 状态机:半关闭、超时、连接生命周期
      • 第36章 HTTP/2 帧、流控与 HPACK:h2 代理的内部机制
      • 第37章 负载均衡算法推导与韧性状态机
      • 第38章 Capstone:把玩具代理改造成生产级骨架
    • 综合实战篇

      • 第39章 企业多跳转发链:拓扑、协议矩阵与贯穿性难题
      • 第40章 端到端实战:把 6 类流量全代理通
      • 第41章 更刁钻的流量:gRPC、长轮询、WebRTC、大文件、双向流
      • 第42章 可落地完整参考实现:一套能跑的多协议转发栈
    • 附录

      • 附录 A:代理协议报文速查(HTTP / SOCKS / PAC / PROXY protocol)
      • 附录 B:组件选型决策树
      • 附录 C:抓包与命令速查

第11章 透明代理:iptables REDIRECT/DNAT、TPROXY 与 eBPF 劫持

学习目标

  • 理解透明代理的核心难题:被劫持后,代理如何找回"客户端原本想去哪"
  • 掌握三种劫持机制:iptables REDIRECT/DNAT、TPROXY、eBPF,及其取舍
  • 用 SO_ORIGINAL_DST 亲手取回原始目标,理解 Istio Sidecar 零侵入劫持的底层
  • 学会规避透明代理的头号坑:流量环路

前置知识

  • 第01章 透明代理、第02章 L3 转发与 NAT
  • 网络手册 · Netfilter 与防火墙(iptables 表/链)
  • 后续 第26章 Sidecar 流量劫持 是本章在 K8s 的落地

原理

透明代理的核心难题

第01章 说过:透明代理对客户端零配置、零感知,靠内核把流量"拐"进代理端口。但这立刻带来一个棘手问题:

客户端的包本来要去 93.184.216.34:443,被内核劫持改道到了本地代理 :15001。代理 accept() 到这条连接后,用 getpeername() 只能看到客户端地址,用 getsockname() 看到的是自己的 :15001——那客户端原本想去哪,丢了!

代理必须有办法找回"原始目标地址",否则它不知道该把流量转给谁。三种劫持机制的差异,核心就在怎么保存和取回这个原始目标。

机制一:iptables REDIRECT(NAT 表)

REDIRECT 是 DNAT 的特例——把目标地址改写成"本机的某个端口":

# 把本机发往外部 80 端口的 TCP,重定向到本地代理 3128
iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-port 3128
# 网关场景用 PREROUTING(劫持流经本机的别人的流量)
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3128

怎么取回原始目标? REDIRECT 是 NAT,内核 conntrack 记住了"改写前的原始目标"。代理用 getsockopt(SO_ORIGINAL_DST) 向 conntrack 查回来:

内核 conntrack 表:[原始: ...→93.184.216.34:80]  [改写后: ...→127.0.0.1:3128]
代理 getsockopt(fd, SOL_IP, SO_ORIGINAL_DST) ──查──▶ 拿回 93.184.216.34:80 ✅
  • 优点:简单、广泛支持
  • 局限:只支持 TCP(UDP 没有连接,conntrack 取原始目标的方式不适用 REDIRECT);改写了包;只能重定向到本机

机制二:TPROXY(mangle 表,不改包)

TPROXY 是为透明代理量身设计的:不做 NAT、不改写目标地址,而是把包"标记"后交给一个绑定了 IP_TRANSPARENT 的本地 socket。

# mangle 表 PREROUTING:打标记 + 投递到本地 50080,但不改包目标
iptables -t mangle -A PREROUTING -p tcp --dport 80 \
    -j TPROXY --on-port 50080 --tproxy-mark 0x1/0x1

# 策略路由:把带标记的包导向本地(lo)处理
ip rule add fwmark 0x1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100

代理 socket 要开 IP_TRANSPARENT,这样它既能收到目标不是本机的包,又能用 getsockname() 直接拿到原始目标(因为包没被改写)。

对比REDIRECT/DNATTPROXY
改写包目标✅ 改❌ 不改
取原始目标SO_ORIGINAL_DST(查 conntrack)getsockname() 直接得到
支持 UDP❌(TCP 为主)✅ TCP + UDP
需要策略路由否是(ip rule/ip route)
典型使用Istio 旧版、redsocks、squidIstio TPROXY 模式、需要 UDP 时

一句话:REDIRECT 简单但改包、只 TCP;TPROXY 复杂但不改包、支持 UDP、保留原始目标更干净。

机制三:eBPF(Cilium,绕过 iptables)

iptables 在规则多时性能差(线性匹配)、且 NAT/conntrack 有开销。Cilium 用 eBPF 在更靠近 socket 的位置做重定向:

  • socket 层:用 bpf_sk_assign/sockmap,在连接建立时直接把流量定向到代理 socket,连 NAT 都不需要
  • sockmap/sk_msg:同主机两个 socket 间数据直接在内核 redirect,绕过 TCP/IP 栈(也是 第12章 的零拷贝话题)
  • 优势:无 iptables 规则膨胀、低开销、可编程;是 Istio Ambient(无 sidecar)/ Cilium Service Mesh 的基础(第26章)

详见 网络手册 · Cilium 与 eBPF、eBPF 深度实践。

Istio Sidecar 劫持:透明代理的教科书级应用

这是透明代理最重要的现实落地。Istio 给 Pod 注入一个 istio-init init-container,它在 Pod 的网络命名空间里用 iptables 把所有进出流量劫持到 Envoy:

        Pod netns 内
  ┌─────────────────────────────────────────────┐
  │  业务容器                Envoy sidecar        │
  │   │ 出站流量                ▲ :15001(出) ▲    │
  │   ▼                         │ :15006(入) │    │
  │  [iptables REDIRECT] ───────┘            │    │
  │   出站→15001,入站→15006,由 Envoy 接管   │    │
  └─────────────────────────────────────────────┘

业务代码完全无感(零侵入),但所有流量都过了 Envoy——于是 mTLS、路由、重试、遥测(第29章)全平台接管。关键细节:Envoy 自己发出的流量必须排除,否则无限环路——见下。

头号坑:流量环路与如何规避

透明代理最危险的陷阱:代理自己转发出去的流量,又被同一条劫持规则抓回来,形成无限环路。规避靠"按发起者身份排除代理自己":

# Istio 的做法:排除 Envoy 进程的 UID(1337),它的流量不再被劫持
iptables -t nat -A OUTPUT -m owner --uid-owner 1337 -j RETURN
# 普通透明代理同理:用 --uid-owner / fwmark / cgroup 把"代理自己的出流量"放行

这就是为什么 Istio 让 Envoy 以专用 UID 1337 运行——为了在 iptables 里能用 --uid-owner 精确地"放过自己",斩断环路。


️ 实现 / 命令

实验一:REDIRECT 劫持 + 用 SO_ORIGINAL_DST 取回原始目标

一个透明代理(Go)取原始目标的核心代码:

import (
	"net"
	"syscall"
	"unsafe"
)

const SO_ORIGINAL_DST = 80

// 从被 REDIRECT 劫持的 TCP 连接里,取回客户端原本想访问的目标
func originalDst(conn *net.TCPConn) (string, error) {
	raw, err := conn.SyscallConn()
	if err != nil {
		return "", err
	}
	var addr syscall.RawSockaddrInet4
	size := uint32(unsafe.Sizeof(addr))
	var gerr error
	raw.Control(func(fd uintptr) {
		// 向 conntrack 查询改写前的原始目标
		_, _, e := syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd,
			syscall.SOL_IP, SO_ORIGINAL_DST,
			uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&size)), 0)
		if e != 0 {
			gerr = e
		}
	})
	ip := net.IPv4(addr.Addr[0], addr.Addr[1], addr.Addr[2], addr.Addr[3])
	port := int(addr.Port&0xff)<<8 | int(addr.Port>>8) // 网络字节序转主机序
	return net.JoinHostPort(ip.String(), strconv.Itoa(port)), gerr
}

配上劫持规则就能跑(注意用 --uid-owner 放过代理自己防环路):

# 让本机出站 80 进入透明代理 3128,但放过代理进程自己(假设其 uid=1000)
iptables -t nat -A OUTPUT -p tcp --dport 80 -m owner ! --uid-owner 1000 -j REDIRECT --to-port 3128
curl http://example.com/        # 注意:没有 -x!客户端零配置

实验二:查看 Istio 注入的劫持规则

# 在被注入 sidecar 的 Pod 里看 iptables(Istio 生成的 ISTIO_* 链)
kubectl exec <pod> -c istio-proxy -- iptables -t nat -L -n -v
# 关键链:
#   ISTIO_OUTPUT  → 出站流量 REDIRECT 到 15001(放过 uid 1337)
#   ISTIO_INBOUND → 入站流量 REDIRECT 到 15006
#   -m owner --uid-owner 1337 -j RETURN   ← 放过 Envoy 自己,防环路

实验三:TPROXY 透明代理 UDP(概念验证)

# TPROXY 能劫持 UDP(REDIRECT 不行)
iptables -t mangle -A PREROUTING -p udp --dport 53 \
    -j TPROXY --on-port 5353 --tproxy-mark 0x1/0x1
ip rule add fwmark 0x1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
# 代理 socket 需 setsockopt(IP_TRANSPARENT) 才能收非本机目标的包并拿到原始目标

排错

现象根因解决
代理拿不到原始目标,全转到自己用 getsockname() 而非 SO_ORIGINAL_DSTREDIRECT 必须查 conntrack 取原始目标
连接无限循环、CPU 打满代理自身流量又被劫持--uid-owner/fwmark 放过代理自己
UDP 劫持不生效用了 REDIRECT(不支持 UDP)改用 TPROXY
TPROXY 配了不通缺策略路由补 ip rule add fwmark + ip route ... table
SO_ORIGINAL_DST 返回代理自己地址conntrack 没记录(包没走 NAT)确认 REDIRECT 规则命中、conntrack 开启
Pod 里 sidecar 没拦到流量init-container 未注入/失败查 istio-init 日志、命名空间注入标签
iptables 规则多导致延迟高NAT/conntrack + 线性匹配开销迁移到 eBPF(Cilium)

本章小结

  • 透明代理的核心难题是找回原始目标:REDIRECT 靠 SO_ORIGINAL_DST 查 conntrack,TPROXY 靠不改包 + getsockname()。
  • 三种劫持:REDIRECT/DNAT(简单、改包、TCP 为主)、TPROXY(不改包、支持 UDP、要策略路由)、eBPF(绕 iptables、低开销、Ambient/Cilium 基础)。
  • Istio Sidecar 用 init-container 在 Pod netns 里 iptables 劫持全流量到 Envoy,业务零侵入。
  • 流量环路是头号坑,靠 --uid-owner 等"放过代理自己"斩断(Istio 的 UID 1337 即为此)。

下一章 第12章 数据搬运的艺术,我们钻到代理性能的最底层:字节到底怎么在两个 socket 间移动,splice/sendfile/io_uring/eBPF 如何做到零拷贝,以及为什么 L7 代理享受不到。

Prev
第10章 L7 代理:协议感知与基于内容的路由
Next
第12章 数据搬运的艺术:splice / sendfile / 零拷贝 / io_uring