最近由於考試周臨近,所以博客這邊都沒怎麼更新。不過斷斷續續研究了幾天,總算是摸索出了一個讓自己相對滿意的透明代理方案,因此就抽空寫了篇博客,權當記錄。事先說明:這篇博客僅僅描述了一個透明代理方案,並 包含任何代理服務器搭建的內容。方案的大致結構如下圖,具體細節和配置我會在後文中詳敘。

起因

對我而言,透明代理最重要的利好就是局域網設備接入和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 來的流量,那爲什麼還是選擇了 ipt2socksTPROXY 呢?的確,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 層面對 dnsmasqchinadns 的請求進行分流。這還沒完,如果你的後端代理不支持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

iptablesset 模塊可以實現按規則集路由,而規則集的添加就是通過 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適用的規則集了( chnroutechnroute6 )。

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

這裏注意,由於要對 clashoverture 的流量直連,因此我選擇使用 owner 擴展,將用戶 clash 的流量全部直連處理。之後將 clashoverture 進程運行在用戶 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,你還需要安裝 ipt2socksoverture (AUR都有對應包: ipt2socksoverture )。此外,還需要一個支持Socks協議的代理(我用的是clash,當然其他可以)。按照文中配置完後,修改 transparent_proxy.sh 的開頭爲你配置的相關內容即可。

缺陷

令人遺憾的是,這份配置還是有不完美之處的。不過好在都不是什麼大問題,也可以曲線救國。

  1. 對於域名形式的代理服務器,必須給代理程序配置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 的問題,所以如果它修就可以解決。
  2. 本機直連DNS。之前說DNS配置時提到,必須對本機的直連DNS設置直連規則。而這就導致本機無法攔截對直連DNS服務器的DNS請求。解決方案很簡單,就是本機DNS別設置成直連DNS那幾個服務器就行。
  3. overture 不支持UDP via Socks。這個倒也無所謂,TCP查詢就行,對性能的影響可以忽略。

兄啊,怎麼都是DNS的問題啊

Reference

  1. [v1.0] Tun+MITMProxy 初探( https://blog.yesterday17.cn/post/transparent-proxy-with-mitmproxy/
  2. zfl9/ss-tproxy( https://github.com/zfl9/ss-tproxy/
相關文章