Linux使用TPROXY进行UDP的透明代理

字数 989阅读 2969

1.为什么使用TPROXY才能代理UDP

在进行TCP的代理时,只要在NET表上无脑进行REDIRECT就好了。例如使用ss-redir,你只要把tcp的流量redirect到ss-redir监听的端口上就OK了。但是当你使用这种方法的时候,就会不正常,因为对于UDP进行redirect之后,原始的目的地址和端口就找不到了。

这是为什么呢?
ss-redir的原理很简单:使用iptables对PREROUTING与OUTPUT的TCP/UDP流量进行REDIRECT(REDIRECT是DNAT的特例),ss—redir在捕获网络流量后,通过一些技术手段获取REDIRECT之前的目的地址(dst)与端口(port),连同网络流量一起转发至远程服务器。
针对TCP连接,的确是因为Linux Kernel连接跟踪机制的实现才使获取数据包原本的dst和port成为可能,但这种连接跟踪机制并非只存在于TCP连接中,UDP连接同样存在,conntrack -p udp便能看到UDP的连接跟踪记录。内核中有关TCP与UDP的NAT源码/net/netfilter/nf_nat_proto_tcp.c和/net/netfilter/nf_nat_proto_udp.c几乎一模一样,都是根据NAT的类型做SNAT或DNAT。

那这究竟是怎么一回事?为什么对于UDP连接就失效了呢?
回过头来看看ss-redir有关获取TCP原本的dst和port的源码,核心函数是getdestaddr:

static int
getdestaddr(int fd, struct sockaddr_storage *destaddr)
{
    socklen_t socklen = sizeof(*destaddr);
    int error         = 0;

    error = getsockopt(fd, SOL_IPV6, IP6T_SO_ORIGINAL_DST, destaddr, &socklen);
    if (error) { // Didn't find a proper way to detect IP version.
        error = getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, destaddr, &socklen);
        if (error) {
            return -1;
        }
    }
    return 0;
}

在内核源码中搜了下有关SO_ORIGINAL_DST的东西,看到了getorigdst:

static int
getorigdst(struct sock *sk, int optval, void __user *user, int *len)
{
    const struct inet_sock *inet = inet_sk(sk);
    const struct nf_conntrack_tuple_hash *h;
    struct nf_conntrack_tuple tuple;

    memset(&tuple, 0, sizeof(tuple));

    lock_sock(sk);
    tuple.src.u3.ip = inet->inet_rcv_saddr;
    tuple.src.u.tcp.port = inet->inet_sport;
    tuple.dst.u3.ip = inet->inet_daddr;
    tuple.dst.u.tcp.port = inet->inet_dport;
    tuple.src.l3num = PF_INET;
    tuple.dst.protonum = sk->sk_protocol;
    release_sock(sk);

    /* We only do TCP and SCTP at the moment: is there a better way? */
    if (tuple.dst.protonum != IPPROTO_TCP &&
        tuple.dst.protonum != IPPROTO_SCTP) {
        pr_debug("SO_ORIGINAL_DST: Not a TCP/SCTP socket\n");
        return -ENOPROTOOPT;
    }

We only do TCP and SCTP at the moment。Oh,shit!只针对TCP与SCTP才能这么做,并非技术上不可行,只是人为地阻止罢了。

2.TPROXY

为了在redirect UDP后还能够获取原本的dst和port,ss-redir采用了TPROXY。Linux系统有关TPROXY的设置是以下三条命令:

ip rule add fwmark 0x2333/0x2333 pref 100 table 100
ip route add local default dev lo table 100
iptables -t mangle -A PREROUTING -p udp -j TPROXY --tproxy-mark 0x2333/0x2333 --on-ip 127.0.0.1 --on-port 1080

大意就是在mangle表的PREROUTING中为每个UDP数据包打上0x2333/0x2333标志,之后在路由选择中将具有0x2333/0x2333标志的数据包投递到本地环回设备上的1080端口;对监听0.0.0.0地址的1080端口的socket启用IP_TRANSPARENT标志,使IPv4路由能够将非本机的数据报投递到传输层,传递给监听1080端口的ss-redir。IP_RECVORIGDSTADDR与IPV6_RECVORIGDSTADDR则表示获取送达数据包的dst与port。
可问题来了:要知道mangle表并不会修改数据包,那么TPROXY是如何做到在不修改数据包的前提下将非本机dst的数据包投递到换回设备上的1080端口呢?
这个问题在内核中时如何实现的,还待研究,但是确定是TPROXY做了某些工作。

TPROXY主要功能:

    1. 重定向一部分经过路由选择的流量到本地路由进程(类似NAT中的REDIRECT)
    1. 在非本地IP上起监听。监听后就可以转发了(神奇吧)

TPROXY要解决的两个重要的问题

  • 1.套接字如何监听到非本地IP地址。
    先用setsockopt函数为套接字设置IP_TRANSPARENT标识,再去监听0.0.0.0地址这样的方式来实现监听任意IP。
  • 2.如何获取的原始目标的端口 。
    先调用setsockopt (s, IPPROTO_IP, IP_RECVORIGDSTADDR, &n, sizeof(int))函数为套接字设置IP_RECVORIGDSTADDR标识,然后通过recvmsg函数从tproxy那边接受发过来的msghdr结构体信息,并循环遍历cmsghdr成员最终获取到原始目标的地址和端口,也就是说tproxy会向msghdr(附属数据结构)填入原始目标ip和端口信息,再通过sendmsg函数发送给代理应用。

参考:
https://blog.csdn.net/ts__cf/article/details/78942294
https://vvl.me/2018/06/09/from-ss-redir-to-linux-nat/

推荐阅读更多精彩内容