Linux 透明代理并不是一个独立的功能模块,而是一个功能特性。在使用 Linux 透明代理的时候,需要 iptables, ip-rule, ip-route 和应用程序一起协同工作。

Linux 透明代理相关博客:

透明代理简介

使用透明代理特性的时候,需要系统支持 TPROXY 和策略路由。

本小节翻译自:Transparent proxy support

支持非本地 socket

对于 tcp 连接,需要给对应的网络包打 mark:

1
2
3
4
# iptables -t mangle -N DIVERT
# iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT
# iptables -t mangle -A DIVERT -j MARK --set-mark 1
# iptables -t mangle -A DIVERT -j ACCEPT

然后将打 mark 的网络包送入本地协议栈:

1
2
# ip rule add fwmark 1 lookup 100
# ip route add local 0.0.0.0/0 dev lo table 100

因为 IPv4 路由规则的限制,应用程序需要在 bind 前启用 (SOL_IP, IP_TRANSPARENT)socket 选项:

1
2
3
4
5
6
7
8
9
fd = socket(AF_INET, SOCK_STREAM, 0);
/* - 8< -*/
int value = 1;
setsockopt(fd, SOL_IP, IP_TRANSPARENT, &value, sizeof(value));
/* - 8< -*/
name.sin_family = AF_INET;
name.sin_port = htons(0xCAFE);
name.sin_addr.s_addr = htonl(0xDEADBEEF);
bind(fd, &name, sizeof(name));

设置 socket 选项的 Go 语言实现请参考 go-tproxy

重定向流量

TPROXY 模块不依赖 NAT 就提供了类似 iptables REDIRECT 功能,使用如下 iptables 规则:

1
2
# iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY \
  --tproxy-mark 0x1/0x1 --on-port 50080

对于 listener socket,也需要启用 (SOL_IP, IP_TRANSPARENT)选项。

iptables 扩展

为了使用透明代理,使用的 iptables 需要带有如下模块:

  • NETFILTER_XT_MATCH_SOCKET
  • NETFILTER_XT_TARGET_TPROXY

透明代理的实现分析

主要针对 socket 选项设置、iptables -j TPROXY、内核协议栈收发包过程进行源代码级别的分析,忽略 ip-ruleip-route 的分析。

socket 选项的实现分析

参考:knetstat:查看 socket 的 IP_TRANSPARENT 选项

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// net/ipv4/ip_sockglue.c

int ip_setsockopt(struct sock *sk, int level, int optname, sockptr_t optval,
		unsigned int optlen)
{
	int err;
	...
	err = do_ip_setsockopt(sk, level, optname, optval, optlen);
	...
	return err;
}

static int do_ip_setsockopt(struct sock *sk, int level, int optname,
		sockptr_t optval, unsigned int optlen)
{
	struct inet_sock *inet = inet_sk(sk);

	switch (optname) {
	...
	case IP_TRANSPARENT:
		...
		inet->transparent = !!val; // IP_TRANSPARENT 选项保存在 struct inet_sock.trasparent 中
		break;
	...
	}
}

// include/net/inet_sock.h

struct inet_sock {
	struct sock		sk;
	...
	__u8			recverr:1,
				is_icsk:1,
				freebind:1,
				hdrincl:1,
				mc_loop:1,
				transparent:1,
				mc_all:1,
				nodefrag:1;
	...
};

iptables -j TPROXY

查看文档 iptables-extensionsiptables-tproxy 的参数说明如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
TPROXY

This target is only valid in the mangle table, in the PREROUTING chain and user-defined chains which are only called from this chain. It redirects the packet to a local socket without changing the packet header in any way. It can also change the mark value which can then be used in advanced routing rules. It takes three options:

--on-port port
		This specifies a destination port to use. It is a required option, 0 means the new destination port is the same as the original. This is only valid if the rule also specifies -p tcp or -p udp.

--on-ip address
		This specifies a destination address to use. By default the address is the IP address of the incoming interface. This is only valid if the rule also specifies -p tcp or -p udp.

--tproxy-mark value[/mask]
		Marks packets with the given value/mask. The fwmark value set here can be used by advanced routing. (Required for transparent proxying to work: otherwise these packets will get forwarded, which is probably not what you want.)

其中 --tproxy-mark--on-port 是必选参数,--on-ip 是可选参数,参考 Linux 代理流量回放实验iptables 规则设置。如果没指定 --on-ip,则默认本地地址是 0.0.0.0

1
2
3
4
5
6
7
8
9
// include/uapi/linux/netfilter/xt_TPROXY.h

// 保存 iptables -j TPROXY 参数的结构体
struct xt_tproxy_target_info_v1 {
	__u32 mark_mask;          // mark 的掩码
	__u32 mark_value;         // mark 值
	union nf_inet_addr laddr; // --on-ip
	__be16 lport;             // --on-port
};
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// net/netfilter/xt_TPROXY.c

static struct xt_target tproxy_tg_reg[] __read_mostly = {
	...
	{
		.name		= "TPROXY",
		.family		= NFPROTO_IPV4,  // 只支持 IPv4
		.table		= "mangle",      // 只能在 mangle 表里使用
		.target		= tproxy_tg4_v1, // -j TPROXY 的实现
		.revision	= 1,
		...
	},
	...
};

static unsigned int
tproxy_tg4_v1(struct sk_buff *skb, const struct xt_action_param *par)
{
	const struct xt_tproxy_target_info_v1 *tgi = par->targinfo;
	return tproxy_tg4(xt_net(par), skb, tgi->laddr.ip, tgi->lport,
			  tgi->mark_mask, tgi->mark_value); // 从结构体中取出 IP 地址、本地端口、mark 的掩码和值
}

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;

	// 默认是 tcp socket
	if (sk && sk->sk_state == TCP_TIME_WAIT)
		// 复用处于 TIME_WAIT 状态的 socket
		// 如果是 SYN 包,则不复用该 socket
		sk = nf_tproxy_handle_time_wait4(net, skb, laddr, lport, sk);
	else if (!sk)
		// 没有建立好的 socket,查找监听重定向地址/端口的 listener
		sk = nf_tproxy_get_sock_v4(net, skb, iph->protocol,
					   iph->saddr, laddr, // 使用 --on-ip 指定的目的地址
					   hp->source, lport, // 使用 --on-port 指定的目的端口
					   skb->dev, NF_TPROXY_LOOKUP_LISTENER);

	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); // 消耗一个 socket 引用
		return NF_ACCEPT;
	}

	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;
}

// net/ipv4/netfilter/nf_tproxy_ipv4.c

struct sock *
nf_tproxy_get_sock_v4(struct net *net, struct sk_buff *skb,
		      const u8 protocol,
		      const __be32 saddr, const __be32 daddr,
		      const __be16 sport, const __be16 dport,
		      const struct net_device *in,
		      const enum nf_tproxy_lookup_t lookup_type)
{
	struct sock *sk;

	switch (protocol) {
	case IPPROTO_TCP: {
		struct tcphdr _hdr, *hp;

		hp = skb_header_pointer(skb, ip_hdrlen(skb),
					sizeof(struct tcphdr), &_hdr);
		if (hp == NULL)
			return NULL;

		switch (lookup_type) {
		case NF_TPROXY_LOOKUP_LISTENER:
			// 查找 listener
			sk = inet_lookup_listener(net, &tcp_hashinfo, skb,
						    ip_hdrlen(skb) +
						      __tcp_hdrlen(hp),
						    saddr, sport,
						    daddr, dport,
						    in->ifindex, 0);

			if (sk && !refcount_inc_not_zero(&sk->sk_refcnt))
				sk = NULL;
			/* NOTE: we return listeners even if bound to
			 * 0.0.0.0, those are filtered out in
			 * xt_socket, since xt_TPROXY needs 0 bound
			 * listeners too
			 */
			break;
		case NF_TPROXY_LOOKUP_ESTABLISHED:
			// 查找已经建立好的 socket
			sk = inet_lookup_established(net, &tcp_hashinfo,
						    saddr, sport, daddr, dport,
						    in->ifindex);
			break;
		default:
			BUG();
		}
		break;
		}
	case IPPROTO_UDP:
		...
		break;
	default:
		WARN_ON(1);
		sk = NULL;
	}

	pr_debug("tproxy socket lookup: proto %u %08x:%u -> %08x:%u, lookup type: %d, sock %p\n",
		 protocol, ntohl(saddr), ntohs(sport), ntohl(daddr), ntohs(dport), lookup_type, sk);

	return sk;
}

// nf_tproxy_laddr4 如果 `--on-ip` 指定了地址,则使用该地址;否则使用 skb 进来的网卡的地址。
// 因为已经经过策略路由将 skb 重定向到了某张网卡,所以此时使用的就是该网卡的地址。
// 譬如 ip route add local default dev lo scope host table 100,则使用网卡 lo 的地址。
__be32 nf_tproxy_laddr4(struct sk_buff *skb, __be32 user_laddr, __be32 daddr)
{
	const struct in_ifaddr *ifa;
	struct in_device *indev;
	__be32 laddr;

	if (user_laddr)
		return user_laddr;

	laddr = 0;
	indev = __in_dev_get_rcu(skb->dev);

	in_dev_for_each_ifa_rcu(ifa, indev) {
		if (ifa->ifa_flags & IFA_F_SECONDARY)
			continue;

		laddr = ifa->ifa_local;
		break;
	}

	return laddr ? laddr : daddr;
}

struct sock *
nf_tproxy_handle_time_wait4(struct net *net, struct sk_buff *skb,
			 __be32 laddr, __be16 lport, struct sock *sk)
{
	const struct iphdr *iph = ip_hdr(skb);
	struct tcphdr _hdr, *hp;

	hp = skb_header_pointer(skb, ip_hdrlen(skb), sizeof(_hdr), &_hdr);
	...

	if (hp->syn && !hp->rst && !hp->ack && !hp->fin) {
		// 如果是将 SYN 包发给 TIME_WAIT socket,不如将 SYN 包发给 listener
		struct sock *sk2;

		sk2 = nf_tproxy_get_sock_v4(net, skb, iph->protocol,
					    iph->saddr, laddr ? laddr : iph->daddr,
					    hp->source, lport ? lport : hp->dest,
					    skb->dev, NF_TPROXY_LOOKUP_LISTENER);
		if (sk2) {
			sk = sk2;
		}
	}

	return sk;
}

// include/net/netfilter/nf_proxy.h

static inline bool nf_tproxy_sk_is_transparent(struct sock *sk)
{
	if (inet_sk_transparent(sk))
		return true;

	sock_gen_put(sk);
	return false;
}

// include/net/tcp.h

// 检查 socket 是否设置了 IP_TRANSPARENT 选项
static inline bool inet_sk_transparent(const struct sock *sk)
{
	switch (sk->sk_state) {
	case TCP_TIME_WAIT:
		return inet_twsk(sk)->tw_transparent;
	case TCP_NEW_SYN_RECV:
		return inet_rsk(inet_reqsk(sk))->no_srccheck;
	}
	return inet_sk(sk)->transparent; // 读取 struct inet_sock.transparent 属性
}

内核协议栈收发包过程

以 tcp 连接为例,分析一下 SYN、SYN-ACK、 发数据包的函数调用过程。

接收 SYN 包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// tcp 收包
|-->tcp_v4_rcv()  // net/ipv4/tcp_ipv4.c
    // 找到监听的 socket
    |-->__inet_lookup_skb() // include/net/inet_hashtables.h
    |-->tcp_v4_do_rcv() // net/ipv4/tcp_ipv4.c
        |-->tcp_rcv_state_process() // net/ipv4/tcp_input.c
            |-->icsk->icsk_af_ops->conn_request(sk, skb)
           /
          /
         /
        |-->tcp_v4_conn_request() // net/ipv4/tcp_ipv4.c
                |-->tcp_conn_request() // net/ipv4/tcp_input.c
                    // 取出 IP_TRANSPARENT 选项的值
                    |-->inet_rsk(req)->no_srccheck = inet_sk(sk)->transparent
                    |-->af_ops->route_req(sk, skb, &fl, req)
                   /
                  /
                 /
                // 查 req 的路由表
                |-->tcp_v4_route_req() // net/ipv4/tcp_ipv4.c
                    |-->inet_csk_route_req() // net/ipv4/inet_connection_sock.c
                        |-->inet_sk_flowi_flags() // include/net/inet_sock.h
                        |   // 将 IP_TRANSPARENT 选项的值转到 flags 上
                        |   |-->if (inet_sk(sk)->transparent || inet_sk(sk)->hdrincl) flags |= FLOWI_FLAG_ANYSRC;
                        |-->flowi4_init_output() // include/net/flow.h
                        |   |-->fl4->flowi4_flags = flags;  // IP_TRANSPARENT 选项的值转移到 flowi4_flags
                        |-->ip_route_output_flow() // net/ipv4/route.c
                            |-->__ip_route_output_key() // include/net/route.h
                                |-->ip_route_output_key_hash() // net/ipv4/route.c
                                    |-->ip_route_output_key_hash_rcu()
                                        // 跳过根据源 IP 找网络设备的处理
                                        |-->if (!(fl4->flowi4_flags & FLOWI_FLAG_ANYSRC))
                                        |-->rth = __mkroute_output(res, fl4, orig_oif, dev_out, flags);
                                        |-->return rth;

响应 SYN-ACK 包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// tcp 收包
|-->tcp_v4_rcv()  // net/ipv4/tcp_ipv4.c
    // 找到监听的 socket
    |-->__inet_lookup_skb() // include/net/inet_hashtables.h
    |-->tcp_v4_do_rcv() // net/ipv4/tcp_ipv4.c
        |-->tcp_rcv_state_process() // net/ipv4/tcp_input.c
            |-->icsk->icsk_af_ops->conn_request(sk, skb)
           /
          /
         /
        |-->tcp_v4_conn_request() // net/ipv4/tcp_ipv4.c
                |-->tcp_conn_request() // net/ipv4/tcp_input.c
                    |-->af_ops->send_synack()
                   /
                  /
                 /
                |-->tcp_v4_send_synack() // net/ipv4/tcp_ipv4.c
                    |-->inet_csk_route_req()
                        |-->参考接收 SYN 

TCP 连接发送数据包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// tcp 发送数据包
|-->tcp_sendmsg() // net/ipv4/tcp.c
    |-->tcp_sendmsg_locked()
        |-->tcp_push_one() // net/ipv4/tcp_output.c
            |-->tcp_write_xmit()
                |-->tcp_transmit_skb()
                    |-->__tcp_transmit_skb()
                        |-->ip_queue_xmit() // net/ipv4/ip_output.c
                            |-->__ip_queue_xmit()
                                // 查路由表
                                |-->ip_route_output_ports() // include/net/route.h
                                    |-->inet_sk_flowi_flags() // include/net/inet_sock.h
                                    |   // 将 IP_TRANSPARENT 选项的值转到 flags 上
                                    |   |-->if (inet_sk(sk)->transparent || inet_sk(sk)->hdrincl) flags |= FLOWI_FLAG_ANYSRC;
                                    |-->flowi4_init_output() // include/net/flow.h
                                    |   // 将 IP_TRANSPARENT 选项的值转到 flowi4_flags 上
                                    |   |-->fl4->flowi4_flags = flags
                                    |-->ip_route_output_flow() // net/ipv4/route.c
                                        |-->参考接收 SYN 

总结

“几乎”将内核协议栈 IP 层、TCP 层收发包过程看了遍。

看内核代码的过程有点痛苦,不过最终还是看明白了 Linux 透明代理的具体实现。

以后遇到类似的技术点,也可以通过阅读源代码去理解它的实现。