透明代理 UDP 為什么要用 TProxy?

假設(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)呢?

iptables flow

從數(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 簡(jiǎn)介 用簡(jiǎn)單的話來(lái)定義tcpdump,就是:dump the traffic on a network,根據(jù)使用者...
    JasonShi6306421閱讀 1,351評(píng)論 0 1
  • 簡(jiǎn)介 用簡(jiǎn)單的話來(lái)定義tcpdump,就是:dump the traffic on a network,根據(jù)使用者...
    保川閱讀 6,079評(píng)論 1 13
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類(lèi)型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,658評(píng)論 1 32
  • 個(gè)人認(rèn)為,Goodboy1881先生的TCP /IP 協(xié)議詳解學(xué)習(xí)博客系列博客是一部非常精彩的學(xué)習(xí)筆記,這雖然只是...
    貳零壹柒_fc10閱讀 5,195評(píng)論 0 8
  • 1.為什么使用TPROXY才能代理UDP 在進(jìn)行TCP的代理時(shí),只要在NET表上無(wú)腦進(jìn)行REDIRECT就好了。例...
    zhjwang閱讀 30,351評(píng)論 0 5

友情鏈接更多精彩內(nèi)容