假設(shè)你在路由器上搭建透明代理,要將局域網(wǎng)發(fā)過(guò)來(lái)的 UDP 流量(不包括路由器本身發(fā)出的流量)轉(zhuǎn)發(fā)到透明代理,你需要執(zhí)行以下這些命令:
ip rule add fwmark 1 lookup 100
ip route add local default dev lo table 100
iptables -t mangle -I PREROUTING -p udp --dport 53 -j TPROXY --on-port 1090 --tproxy-mark 0x01/0x01
iptables -t mangle -I OUTPUT -p udp --dport 53 -j MARK --set-mark 1
# 透明代理端口為 1090,假設(shè)只轉(zhuǎn)發(fā) 53 端口的 DNS 查詢流量
如果你要轉(zhuǎn)發(fā)的流量是 TCP,那你只需要執(zhí)行一條命令就可以了:
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 1090
# 假設(shè)只轉(zhuǎn)發(fā) 80 端口的流量
為什么轉(zhuǎn)發(fā) UDP 不能像轉(zhuǎn)發(fā) TCP 那樣配置?
先來(lái)看一個(gè)問(wèn)題。轉(zhuǎn)發(fā) TCP 時(shí),REDIRECT 后包的目的地址已經(jīng)變了,理論上來(lái)說(shuō),原目的地址已經(jīng)丟了,透明代理是拿不到原目的地址的。那為什么透明代理能正確轉(zhuǎn)發(fā)包呢?
實(shí)際上 netfilter 框架(iptables 也是基于這個(gè)框架暴露的勾子開(kāi)發(fā)的)有 SO_ORIGINAL_DST 選項(xiàng)可以支持拿到原目的地址,netfilter 會(huì)將包的原目的地址放在 socket 的 SO_ORIGINAL_DST 屬性里。
那么為什么 TCP 可以通過(guò) SO_ORIGINAL_DST 拿到原目的地址,UDP 卻不行?
原因跟 netfilter 的實(shí)現(xiàn)有關(guān),SO_ORIGINAL_DST 是在 socket 上實(shí)現(xiàn)的,當(dāng) TCP 建立連接后,從對(duì)端的 socket 上就能拿到這個(gè)連接的原目的地址,實(shí)現(xiàn)成本很低。
int main()
{
int sockfd, connfd, len;
struct sockaddr_in servaddr, cli;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
bind(sockfd, (SA*)&servaddr, sizeof(servaddr));
listen(sockfd, 5);
len = sizeof(cli);
connfd = accept(sockfd, (SA*)&cli, &len);
// 從 connfd 就能拿到該連接的原目的地址
// getsockopt(connfd, SOL_IP, SO_ORIGINAL_DST, destaddr, &socklen);
close(sockfd);
}
而 UDP 不是面向連接的,無(wú)法從 socket 里去拿原目的地址:
int main() {
int sockfd;
char buffer[MAXLINE];
char *hello = "Hello from server";
struct sockaddr_in servaddr, cliaddr;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
servaddr.sin_family = AF_INET; // IPv4
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr));
int len, n;
n = recvfrom(sockfd, (char *)buffer, MAXLINE,
MSG_WAITALL, ( struct sockaddr *) &cliaddr,
&len);
buffer[n] = '\0';
// 由于 UDP 不是面向連接的,編程模型中也就沒(méi)有對(duì)端 socket 了
// 那么非要實(shí)現(xiàn),去拿原目的地址的話只能從 sockfd 去拿
// getsockopt(sockfd, SOL_IP, SO_ORIGINAL_DST, destaddr, &socklen);
// 但問(wèn)題是從上面 recvfrom 的調(diào)用到這個(gè)調(diào)用之間可能會(huì)有新包進(jìn)來(lái),
// 改變內(nèi)部 socket 的狀態(tài),那這時(shí)調(diào)用 getsockopt,
// 是無(wú)法知道哪個(gè)原目的地址才是當(dāng)前處理包的目的地址的
sendto(sockfd, (const char *)hello, strlen(hello),
MSG_CONFIRM, (const struct sockaddr *) &cliaddr,
len);
printf("Hello message sent.\n");
return 0;
}
上面就是 UDP 的透明代理轉(zhuǎn)發(fā)不能用 REDIRECT 的原因。
再看看最開(kāi)始的 TProxy 方案為什么可以。
ip rule add fwmark 1 lookup 100
ip route add local default dev lo table 100
iptables -t mangle -I PREROUTING -p udp --dport 53 -j TPROXY --on-port 1090 --tproxy-mark 0x01/0x01
iptables -t mangle -I OUTPUT -p udp --dport 53 -j MARK --set-mark 1
# 透明代理端口為 1090,假設(shè)只轉(zhuǎn)發(fā) 53 端口的 DNS 查詢流量
TProxy 可以不改變包的頭,將包重定向到本地 socket,所以
iptables -t mangle -I PREROUTING -p udp --dport 53 -j TPROXY --on-port 1090 --tproxy-mark 0x01/0x01
這一句直接就將包原封不動(dòng)地投遞到本地 1090 的 udp socket 了,那么為何還要搞個(gè) --tproxy-mark 0x01/0x01 的選項(xiàng)呢?

從數(shù)據(jù)包流向知道,PREROUTING 之后可能走 INPUT,也可能走 FORWARD,那到底走哪條,是由路由表決定的,因此,得有一條路由指示該包就是給本機(jī)的。
系統(tǒng)中初始就有兩張路由表,一張 local (ID 255),一張 main (ID 254),可通過(guò) ip route show table tableID 查看。這兩張表都是按目的地址來(lái)路由的,如果照這兩張表去走,這個(gè)包妥妥地就走 FORWARD 轉(zhuǎn)發(fā)流程了,因?yàn)榘哪康牡刂凡皇潜緳C(jī)。
于是,就需要配置路由表,不能按目的地址來(lái)路由,所以配置按 mark 來(lái)路由:
ip rule add fwmark 1 lookup 100 # 對(duì)于 fwmark 為 1 的包,去 table 100 查找路由
ip route add local default dev lo table 100 # 添加一條默認(rèn)路由,直接走 lo 出
這就是為什么 TProxy 要設(shè)置 mark 的原因,實(shí)際上不在 TProxy 設(shè)置 mark,單獨(dú)在 TProxy 后面再加一個(gè)設(shè)置 mark 的 iptables 規(guī)則應(yīng)該也是可以的。
由此,iptables 將包投給了本地透明代理進(jìn)程。
但本地代理進(jìn)程理論上來(lái)說(shuō)應(yīng)該是不接收的,因?yàn)椴徽撌?TCP 還是 UDP,編程模型都需要 bind 本機(jī)地址(或者 0.0.0.0),不是給本機(jī)的包,進(jìn)程不收。但 2.6.24 的內(nèi)核出了個(gè) IP_TRANSPARENT 的 socket 選項(xiàng),打開(kāi)這個(gè)選項(xiàng),就可以接收任意目的地址的包了。
以上是局域網(wǎng)過(guò)來(lái)的包的重定向,那路由器本身出去的 UDP 要如何重定向到透明代理呢?
TProxy 只能在 PREROUTING 鏈上設(shè)置,不能在 OUTPUT 上設(shè)置,想想也是,如果能設(shè)在 OUTPUT 上,本來(lái)要出去的包又給塞回來(lái),直接就死循環(huán)了。
這種情況,只能讓包出去,再回來(lái),回來(lái)的時(shí)候再通過(guò) TProxy 規(guī)則重定向到透明代理。
那怎么讓它從本網(wǎng)口出去又馬上回到本網(wǎng)口來(lái)呢?還是走路由,在 OUTPUT 鏈給包打上 1 的mark,出去的時(shí)候就會(huì)查路由表,一查發(fā)現(xiàn)是給到 lo 口的,于是又回來(lái)了。
iptables -t mangle -I OUTPUT -p udp --dport 53 -j MARK --set-mark 1
當(dāng)然,等到包從透明代理出去的時(shí)候,它的原目的地址和端口已經(jīng)變了,不會(huì)再被 OUTPUT 鏈上的 mark 規(guī)則匹配到了,否則又會(huì)死循環(huán)的(所以這個(gè)規(guī)則配置要注意,配置不當(dāng),還是有死循環(huán)的可能的)。
參考資料:
https://blog.cloudflare.com/how-we-built-spectrum/
https://elixir.bootlin.com/linux/v4.16.1/source/net/netfilter/xt_TPROXY.c#L119
http://man7.org/linux/man-pages/man8/iptables-extensions.8.html
https://www.kernel.org/doc/Documentation/networking/tproxy.txt
https://linux.die.net/man/8/ip
http://man7.org/linux/man-pages/man7/ip.7.html
https://github.com/ahupowerdns/tproxydoc/blob/master/tproxy.md
https://www.digitalocean.com/community/tutorials/a-deep-dive-into-iptables-and-netfilter-architecture
http://lists.netfilter.org/pipermail/netfilter-devel/2001-February/000564.html