本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。
若无特殊说明,内核源码版本为4.16.1
引言
Cool!一种完全透明的流量劫持方式。
从功能来看类似于让操作系统充当路由器,或者内置一个代理服务器,并允许部分流量被转移到用户空间处理,除此之外与iptables REDIRECT
不同,基于tproxy
的方案并不会修改源地址和端口,对于IP相关的过滤以及再次重定向来说是一个必要的功能点。
在[1][2]中原作者对于使用方式以及阐述的很清楚了,一个简单demo的基本步骤如下:
iptables -t mangle -N DIVERT
在nat表上新建名为DIVERT自定义链iptables -t mangle -A DIVERT -j MARK --set-mark 1
进入DIVERT的数据包设置标记(skb->cb?看看源码吧)iptables -t mangle -A DIVERT -j ACCEPT
默认情况下,内核会丢弃数据包,现在要确保不会iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT
已建立的socket的TCP数据包执行DIVERTip rule add fwmark 1 lookup 100
所有带有1标记的数据包都不再使用默认路由表,而是使用100ip route add local 0.0.0.0/0 dev lo table 100
添加一个路由规则,使得所有数据包(0.0.0.0)最终都被认为是本地的包iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY --tproxy-mark 0x1/0x1 --on-port 1234 --on-ip 192.168.123.1
所有发送到80端口的TCP请求会被标记0x1并被转发到192.168.123.1:1234
注意路由表的修改是必要的,不然无法认为绑定的IP是所谓”本地范围“的,也就没法转发到绑定IP_TRANSPARENT
的套接字上了。
只是理论太过单调,我们来看看iptables tproxy
在内核中到底做了什么。具体调用链如下 tproxy_tg4_v0
->
tproxy_tg4
->
nf_tproxy_get_sock_v4
源码解析
tproxy_tg4_v0
static unsigned int
tproxy_tg4_v0(struct sk_buff *skb, const struct xt_action_param *par)
{
const struct xt_tproxy_target_info *tgi = par->targinfo;
/*
struct xt_tproxy_target_info {
__u32 mark_mask;
__u32 mark_value;
__be32 laddr;
__be16 lport;
};
*/
return tproxy_tg4(xt_net(par), skb, tgi->laddr, tgi->lport,
tgi->mark_mask, tgi->mark_value);
}
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;
// 实际调用__skb_header_pointer,拿到skb的包头
hp = skb_header_pointer(skb, ip_hdrlen(skb), sizeof(_hdr), &_hdr);
if (hp == NULL)
return NF_DROP;
// 首先检查这个数据包是否存在已经连接的套接字
sk = nf_tproxy_get_sock_v4(net, skb, hp, iph->protocol,
iph->saddr, iph->daddr,
hp->source, hp->dest,
skb->dev, NFT_LOOKUP_ESTABLISHED);
laddr = tproxy_laddr4(skb, laddr, iph->daddr);
if (!lport)
lport = hp->dest;
/* UDP has no TCP_TIME_WAIT state, so we never enter here */
if (sk && sk->sk_state == TCP_TIME_WAIT)
/* reopening a TIME_WAIT connection needs special handling */
sk = tproxy_handle_time_wait4(net, skb, laddr, lport, sk);
else if (!sk)
// 在listen队列中查找监听套接字,
sk = nf_tproxy_get_sock_v4(net, skb, hp, iph->protocol,
iph->saddr, laddr,
hp->source, lport,
skb->dev, NFT_LOOKUP_LISTENER);
// 为了节省内存,一个套接字的不同阶段内部结构并不相同,这里检查此sock是否有transparent属性
if (sk && tproxy_sk_is_transparent(sk)) {
/* This should be in a separate target, but we don't do multiple
targets on the same rule yet */
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);
/*
static void
nf_tproxy_assign_sock(struct sk_buff *skb, struct sock *sk)
{
skb_orphan(skb);
skb->sk = sk;
skb->destructor = sock_edemux;
}
*/
// 修改此数据包的的对应套接字,其实就是转发
nf_tproxy_assign_sock(skb, sk);
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;
}
nf_tproxy_get_sock_v4
// 这个函数逻辑比较简单,就是根据四元组和传入的参数去尝试拿到对应的套接字
static inline struct sock *
nf_tproxy_get_sock_v4(struct net *net, struct sk_buff *skb, void *hp,
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;
struct tcphdr *tcph;
switch (protocol) {
case IPPROTO_TCP:
switch (lookup_type) {
case NFT_LOOKUP_LISTENER:
tcph = hp;
sk = inet_lookup_listener(net, &tcp_hashinfo, skb,
ip_hdrlen(skb) +
__tcp_hdrlen(tcph),
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 NFT_LOOKUP_ESTABLISHED:
sk = inet_lookup_established(net, &tcp_hashinfo,
saddr, sport, daddr, dport,
in->ifindex);
break;
default:
BUG();
}
break;
case IPPROTO_UDP:
sk = udp4_lib_lookup(net, saddr, sport, daddr, dport,
in->ifindex);
if (sk) {
int connected = (sk->sk_state == TCP_ESTABLISHED);
int wildcard = (inet_sk(sk)->inet_rcv_saddr == 0);
/* 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
*/
if ((lookup_type == NFT_LOOKUP_ESTABLISHED && (!connected || wildcard)) ||
(lookup_type == NFT_LOOKUP_LISTENER && connected)) {
sock_put(sk);
sk = NULL;
}
}
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;
}
可以发现tproxy的代码实现非常简单,让我大声为您朗读一遍:tproxy
尝试从连接或者监听套接字列表中找到一个符合四元组的套接字,然后如果发现此套接字含有IP_TRANSPARENT
就修改此数据包的sock结构,这个行为其实就是转发。
所以要实际转发的话首先我们需要一个绑定了IP_TRANSPARENT
选项的套接字,
IP_TRANSPARENT
IP_TRANSPARENT
的作用不仅仅是用于tproxy
的转发,还可以使得被设置的套接字绑定非本地的IP地址,但是必须设置路由表,否则数据包会被路由、丢弃,根本无法被转发到绑定IP_TRANSPARENT
到套接字。除非如果目标地址与本地地址匹配,则由系统本身接受处理,这就需要手动设置一个单独的路由表,并由mark去指示。man文档中[10]中描述如下:
Setting this boolean option enables transparent proxying on this socket. This socket option allows the calling application to bind to a nonlocal IP address and operate both as a client and a server with the foreign address as the local endpoint. NOTE: this requires that routing be set up in a way that packets going to the foreign address are routed through the TProxy box (i.e., the system hosting the application that employs the IP_TRANSPARENT socket option). Enabling this socket option requires superuser privileges (the CAP_NET_ADMIN capability). TProxy redirection with the iptables TPROXY target also requires that this option be set on the redirected socket.
设置此布尔选项可在此套接字上启用透明代理。 此套接字选项允许调用应用程序绑定到非本地 IP 地址,并以外部地址作为本地端点作为客户端和服务器运行。 注意:这需要设置路由,使去往外部地址的数据包通过 TProxy box(即托管使用 IP_TRANSPARENT 套接字选项的应用程序的系统)进行路由。 启用此套接字选项需要超级用户权限(CAP_NET_ADMIN 功能)。 使用 iptables TPROXY 目标的 TProxy 重定向还要求在重定向的套接字上设置此选项。
总结
打开思路,透明代理实际是在网络数据包从程序实际面对的套接字到物理网卡之间加入了一个处理数据包的过程,而且因为此数据包的处理流程不像是eBPF一样在内核中充满阻碍的处理代码,而是在被转发到用户态的套接字,这意味着我们可以捕获流量实现任意的注入,统计,分析等等。
但是注意基于容器做透明代理是还是需要注意network namespace
的设置,因为ehash
和lhash
在做哈希时实际是带着network namespace
选项的[11],这会在tproxy
实际实行转发时被使用。
一个简单的使用demo如下:tproxy-http_hijacking。
参考: