探索:適用IPv6、Fullcone NAT的旁路由透明代理方案
最近由於考試周臨近,所以博客這邊都沒怎麼更新。不過斷斷續續研究了幾天,總算是摸索出了一個讓自己相對滿意的透明代理方案,因此就抽空寫了篇博客,權當記錄。事先說明:這篇博客僅僅描述了一個透明代理方案,並 不 包含任何代理服務器搭建的內容。方案的大致結構如下圖,具體細節和配置我會在後文中詳敘。
起因
對我而言,透明代理最重要的利好就是局域網設備接入和CLI程序。原先對於CLI程序我採用的是 proxychain
,也就是hook的方法。但是這種方法沒辦法針對自己實現請求的go程序,而更底層的 graftcp
在DNS上游的處理上也存在問題,因此我最後選擇了使用透明代理進行解決。我之前使用的教程是 新白話文 的 TPROXY
配置,它能解決我幾乎所有網絡方面的痛點。
不過這個方案(主要是 v2ray
)還是有若干問題。首先就是配置的切換非常複雜,需要重啓 v2ray
進程才能做到。其次就是沒法做到Fullcone NAT,這是 v2ray
本身機能所限。後來我更換了 clash
,並保留了 v2ray
作爲透明代理的前置代理。 clash
提供的RESTful API確實很好的解決了我關於配置切換的問題,但是我發現仍然無法做到Fullcone。在後續的調查中我發現這不僅僅是 vmess
協議本身的限制, v2ray
的行爲也註定了靠它沒法做到Fullcone。而且僅使用 v2ray
這樣複雜的程序用來做 clash
的前置也是我無法接受的,因此我纔打算探索新的透明代理方案。
要求
Fullcone NAT是必須的。要問原因的話,就是uu加速器實在是太貴了,以及馬造直連真的很卡。其次就是IPv6的支持,不過這個比較虛無,因爲我家的寬帶似乎不能IPv6,但回校之後其實還是用得上。最後就是性能,由於我的目標是將代理程序部署在旁路由(樹莓派)上,因此代理程序的性能要好、佔用也不能太大。此外就是要儘可能減少數據包路由的次數,儘量把路由工作放在內核空間(netfilter),降低用戶空間切換的開銷。
至於爲什麼不在主路由上部署,原因很簡單:學校裏的主路由性能差。而且設置了旁路由代理就可以通過主路由設置DHCP來控制設備是否啓用代理。此外,還有可以部署在Manjaro以供便攜使用的優勢。
後端代理
後端代理採用 clash
。雖然 v2ray
在配置上更加靈活,但是 clash
在運行狀態時更加靈活。RESTful API對我來說是更加重要的,因爲藉由它就可以使用諸如 yacd
等WebAPP快速的在配置之間進行切換。
中端代理
中端代理我使用了一個小巧的工具 ipt2socks 。通過這個工具可以從 iptables
接受 TPROXY
流量,並轉至 clash
的Socks入口。
瞭解 clash
的朋友可能知道,實際上 clash
本身提供了TUN功能用於處理 iptables
來的流量,那爲什麼還是選擇了 ipt2socks
和 TPROXY
呢?的確,TUN對 iptables
配置的影響不大,而且它的兼容性實際上高於 TPROXY
(部分發行版不自帶),最重要的是,它還節省了一次將數據包包裝Socks協議的過程。
對於這個,我的理由是解耦。不談 clash
的實現是否穩定,可以確定的是,幾乎沒有什麼代理軟件是不支持Socks協議的,而支持TUN的實際上鳳毛麟角。此外,使用Socks還意味着支持諸如MITMProxy此類使用Socks接口的網絡應用。而至於性能,在最終的配置下,大多數請求實際上都不會經由這個Socks接口。加之 ipt2socks
的實現相當純粹、輕量化(編譯後100K不到),因此這一點的性能開銷完全是值得的。
ipt2socks
的配置簡潔到根本沒有配置,所有配置都通過命令行參數來完成。可以使用 systemd
作爲守護進程運行,配置如下
[Unit] Description=utility for converting iptables(redirect/tproxy) to socks5 After=network.target [Service] User=nobody EnvironmentFile=/etc/ipt2socks/ipt2socks.conf CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE NoNewPrivileges=true ExecStart=/usr/bin/ipt2socks -s $server_addr -p $server_port -l $listen_port -j $thread_nums $extra_args Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target
這裏手工強行加入了配置文件 /etc/ipt2socks/ipt2socks.conf
,如果你懷念命令行參數的簡潔,也可以直接修改 ExecStart
。配置文件格式如下
# ipt2socks configure file # # detailed helps could be found at: https://github.com/zfl9/ipt2socks # Socks5 server ip server_addr=127.0.0.1 # Socks5 server port server_port=1080 # Listen port number listen_port=60080 # Number of the worker threads thread_nums=1 # Extra arguments extra_args=
相關配置和編譯流程我已經添加至 AUR 。Archlinux用戶可以直接使用 yay
之類的程序進行安裝。
DNS
我最終的選擇是 overture 。
說到中國特色社會主義DNS解析,大多數人大概第一時間就會想到 chinadns
。的確, chinadns
是一個相當完善可靠的程序,但是 chinadns
也顯然不太適合直接作爲本地DNS服務器——它沒有良好的緩存,並且也不支持複雜的路由規則。所以通常的做法是在前面套一個 dnsmasq
做緩存與分流,然後把 chinadns
作爲上游。但是 dnsmasq
本身並不支持代理訪問,因此你還需要在 iptables
層面對 dnsmasq
和 chinadns
的請求進行分流。這還沒完,如果你的後端代理不支持UDP,你還需要把DNS請求的UDP轉成TCP請求( dns2tcp
工具)。所以最後,你得到了 《世 界 名 畫》 chinadns
+ dnsmasq
+ dns2tcp
。暫且不論來回進出 iptables
的次數已經遠遠超過《半條命》的作品數,光是這個複雜配置我就覺得有夠傻的。
此外另一個可能的選擇就是 clash
的內建DNS。而且 clash
還有fake-ip擴展以減少本地DNS解析的需要。但是問題有二,一個和之前不選擇TUN的理由一致;另一個就是其他方案實際上也可以做到接近的效果,而使用fake-ip是要以缺少DNS緩存和可能得到錯誤的解析內容爲代價的。
所以我找到了 overture
,它支持IPv6、可以方便的替換DNS的Upstream、支持通過Socks代理請求、支持EDNS、有相對完善的Dispatcher,可以說基本滿足了我所有的要求。而且它還額外支持RESTful API(雖然目前只能檢查cache),給進一步的配置管理帶來了可能。
配置參考官方配置就行,AUR軟件包的默認配置也OK。就是注意需要將 WhenPrimaryDNSAnswerNoneUse
改成 AlternativeDNS
。
路由分流
集齊了所有碎片,那下一步就該把他們縫合在一起了。縫合用的道具當然就是 iptables
了(IPv6就是 ipt6ables
,配置幾乎完全一致)。
分流的策略很簡單,就是DNS交給 overture
,私有地址和大陸IP直連,剩下的交給 ipt2socks
。不過爲了實現大陸IP直連,還需要設定相關規則集(因爲規則超級多,都用 iptables
的效果還是很恐怖的),因此先介紹 ipset
相關的配置。
ipset
iptables
的 set
模塊可以實現按規則集路由,而規則集的添加就是通過 ipset
完成的。在 apnic.net 可以查詢到分配給中國大陸的IP地址,因此解析下就可以添加到規則集了。腳本如下
# 下載並解析 route wget --no-check-certificate -O- 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest' | grep CN > tmp_ips cat tmp_ips | grep ipv4 | awk -F\| '{ printf("%s/%d\n", $4, 32-log($5)/log(2)) }' > chnroute.set cat tmp_ips | grep ipv6 | awk -F\| '{ printf("%s/%d\n", $4, 32-log($5)/log(2)) }' > chnroute6.set rm -rf tmp_ips # 導入 ipset 表 sudo ipset -X chnroute &>/dev/null sudo ipset -X chnroute6 &>/dev/null sudo ipset create chnroute hash:net family inet sudo ipset create chnroute6 hash:net family inet6 cat chnroute.set | sudo xargs -I ip ipset add chnroute ip cat chnroute6.set | sudo xargs -I ip ipset add chnroute6 ip
運行後就可以得到IPv4和IPv6適用的規則集了( chnroute
和 chnroute6
)。
iptables
這回是真的開始縫合了。總體的思路還是和新白話文的配置一樣,把 OUTPUT
鏈的包路由至 PREROUTING
鏈,之後再用 TPROXY
模塊進行下一步轉發。至於爲什麼要繞這麼一個大圈就和 TPROXY
本身的實現有關了,可以參考 @某昨 的 TProxy探祕 。
因此規則大概可以分爲三個部分:策略路由、 PREROUTING
鏈、 OUTPUT
鏈。綜合如下:
# fwmark 匹配的包進入本地環回 ip -4 rule add fwmark $lo_fwmark table 100 ip -4 route add local default dev lo table 100 ########## PREROUTING 鏈配置 ########## iptables -t mangle -N TRANS_PREROUTING iptables -t mangle -A TRANS_PREROUTING -i lo -m mark ! --mark $lo_fwmark -j RETURN # 規則路由 iptables -t mangle -A TRANS_PREROUTING -p tcp -m addrtype ! --src-type LOCAL ! --dst-type LOCAL -j TRANS_RULE iptables -t mangle -A TRANS_PREROUTING -p udp -m addrtype ! --src-type LOCAL ! --dst-type LOCAL -j TRANS_RULE # TPROXY 路由 iptables -t mangle -A TRANS_PREROUTING -p tcp -m mark --mark $lo_fwmark -j TPROXY --on-port $tproxy_port --on-ip $loopback_addr --tproxy-mark $tproxy_mark iptables -t mangle -A TRANS_PREROUTING -p udp -m mark --mark $lo_fwmark -j TPROXY --on-port $tproxy_port --on-ip $loopback_addr --tproxy-mark $tproxy_mark # 應用規則 iptables -t mangle -A PREROUTING -j TRANS_PREROUTING ########## OUTPUT 鏈配置 ########## iptables -t mangle -N TRANS_OUTPUT # 直連 @clash iptables -t mangle -A TRANS_OUTPUT -j RETURN -m owner --uid-owner $direct_user iptables -t mangle -A TRANS_OUTPUT -j RETURN -m mark --mark 0xff # (兼容配置) 直連 SO_MARK 爲 0xff 的流量 # 規則路由 iptables -t mangle -A TRANS_OUTPUT -p tcp -m addrtype --src-type LOCAL ! --dst-type LOCAL -j TRANS_RULE iptables -t mangle -A TRANS_OUTPUT -p udp -m addrtype --src-type LOCAL ! --dst-type LOCAL -j TRANS_RULE # 應用規則 iptables -t mangle -A OUTPUT -j TRANS_OUTPUT
這裏注意,由於要對 clash
和 overture
的流量直連,因此我選擇使用 owner
擴展,將用戶 clash 的流量全部直連處理。之後將 clash
和 overture
進程運行在用戶 clash 即可。此外就是由於兩個鏈的路由規則是公共的(對於 PREROUTING
鏈也可以用 fwmark
來路由),因此獨立出了 TRANS_RULE
用來處理公共部分的路由(主要是標記 fwmark
)。
########## 代理規則配置 ########## iptables -t mangle -N TRANS_RULE iptables -t mangle -A TRANS_RULE -j CONNMARK --restore-mark iptables -t mangle -A TRANS_RULE -m mark --mark $lo_fwmark -j RETURN # 避免迴環 # 私有地址 for addr in "${privaddr_array[@]}"; do iptables -t mangle -A TRANS_RULE -d $addr -j RETURN done # ipset 路由 iptables -t mangle -A TRANS_RULE -m set --match-set $chnroute_name dst -j RETURN # TCP/UDP 重路由 PREROUTING iptables -t mangle -A TRANS_RULE -p tcp --syn -j MARK --set-mark $lo_fwmark iptables -t mangle -A TRANS_RULE -p udp -m conntrack --ctstate NEW -j MARK --set-mark $lo_fwmark iptables -t mangle -A TRANS_RULE -j CONNMARK --save-mark
規則很簡單,基本就是不對匹配私有地址和規則集chnroute的數據包進行標記。並且使用 CONNMARK
對整個連接的數據包進行標記,減少匹配次數。此外,由於 OUTPUT
鏈的數據包還會被路由回 PREROUTING
鏈,導致第二次匹配 TRANS_RULE
,因此遇到有 fwmark
的包就不必匹配了(沒有 fwmark
的包也不可能二次匹配)。
然後就是DNS流量的攔截。由於我需要對網絡中所有DNS流量(UDP53)都進行攔截(無論請求哪個地址,這樣就不用再手動改DNS配置了),因此不可避免的需要一次DNAT來將流量轉發至 overture
,所以我們還需要創建 nat
表的轉發規則。但是由於 nat
表的位置靠後,因此需要在匹配 TRANS_RULE
(位於 mangle
表)之前先 RETURN
所有的DNS流量,這樣流量才能進入 nat
表的轉發規則。
# 局域網 DNS 路由 iptables -t mangle -A TRANS_PREROUTING -p udp -m addrtype ! --src-type LOCAL -m udp --dport 53 -j RETURN iptables -t nat -A TRANS_PREROUTING -p udp -m addrtype ! --src-type LOCAL -m udp --dport 53 -j REDIRECT --to-ports $dns_port # 這之後是 PREROUTING 鏈的 TRANS_RULE # ... # 本地 DNS 路由 iptables -t mangle -A TRANS_OUTPUT -p udp -m udp --dport 53 -j RETURN for addr in "${dns_direct_array[@]}"; do iptables -t nat -A TRANS_OUTPUT -d $addr -p udp -m udp --dport 53 -j RETURN done iptables -t nat -A TRANS_OUTPUT -p udp -m udp --dport 53 -j DNAT --to-destination $local_dns # 這之後是 OUTPUT 鏈的 TRANS_RULE
這裏還有個坑,就是 owner
擴展不能很好的識別UDP流量的發送者。因此還需要對直連的DNS服務器單獨增加匹配規則(這點我很不滿意!但是也沒辦法……)。不過還好只需要加在 OUTPUT
鏈,因爲局域網設備就不必直連了。
至於效果麼……BOOM 忽略那感人的網速
Sum up
最終編寫得到了三個腳本:
-
transparent_proxy.sh
:透明代理規則設置,需要開機運行 -
import_chnroute.sh
:下載並配置chnroute規則,至少需要運行一次,並且規則集文件要和transparent_proxy.sh
同目錄(當然你也可以修改配置) -
flush_iptables.sh
:清理所有增加的規則(除了ipset
)
這些代碼都可以在 我的GitHub 找到。編寫的時候,我大量參考了 ss-tproxy 項目的相關代碼,非常感謝這個repo。
要部署這個配置,除了這三個shell,你還需要安裝 ipt2socks 、 overture (AUR都有對應包: ipt2socks 、 overture )。此外,還需要一個支持Socks協議的代理(我用的是clash,當然其他可以)。按照文中配置完後,修改 transparent_proxy.sh
的開頭爲你配置的相關內容即可。
缺陷
令人遺憾的是,這份配置還是有不完美之處的。不過好在都不是什麼大問題,也可以曲線救國。
- 對於域名形式的代理服務器,必須給代理程序配置DNS。由於在代理程序啓動時需要解析代理服務器的真實IP,因此需要請求
overture
。這原本沒有什麼問題,但是爲了性能通常會開啓AlternativeDNSConcurrent
。而此時overture
會請求clash
以訪問備用DNS,但是clash
還沒啓動。其實本來也沒有問題,但是錯就錯在overture
在連接不上clash
的時候竟然會崩潰 !然後clash
因爲解析不到真實IP所以也跟着一起崩潰,然後overture崩,overture崩完clash崩……解決方案有兩個,一個是謹慎的調節啓動順序——iptables
規則要在clash
解析完畢後請求;另一個就是配置clash
本身的DNS,讓請求不走overture
。前者有點麻煩,而後者實際上是又增加一套和overture
處理不同的DNS配置。不過好在普通流量到了clash
都已經完成DNS解析,除了直連clash
的Socks否則不會用到內建DNS,所以我選擇了後者。本質是overture
的問題,所以如果它修就可以解決。 - 本機直連DNS。之前說DNS配置時提到,必須對本機的直連DNS設置直連規則。而這就導致本機無法攔截對直連DNS服務器的DNS請求。解決方案很簡單,就是本機DNS別設置成直連DNS那幾個服務器就行。
-
overture
不支持UDP via Socks。這個倒也無所謂,TCP查詢就行,對性能的影響可以忽略。
兄啊,怎麼都是DNS的問題啊
Reference
- [v1.0] Tun+MITMProxy 初探( https://blog.yesterday17.cn/post/transparent-proxy-with-mitmproxy/ )
- zfl9/ss-tproxy( https://github.com/zfl9/ss-tproxy/ )