let bind_addr = self.get_rand_ip();
let mut http = HttpConnector::new();
http.set_local_address(Some(bind_addr));
let client = Client::builder()
.http1_title_case_headers(true)
.http1_preserve_header_case(true)
.build(http);
let res = client.request(req).await?;
每一條連線用了哪個位址,都會印出一行來,例如 example.com:443 via [2a0e:aa07:e0a0:...]:...,可以透過輸出確認位址的切換和使用情況。
自從有了自己的 ASN 之後,我手上就一直有一段 /44 的 IPv6,後來又多了一段 /24 的 IPv4。這些位址透過 BIRD 宣告出去,全世界都能路由到我這裡,可是平時用到的,也就是伺服器上設定的那麼幾個,剩下的絕大部分都閒著。
/24 有 256 個 IPv4 位址,/44 則是
2^{84}個 IPv6 位址,這個數量大到基本可以當作用不完。而我有時候剛好又需要一個位址池,讓不同的請求從不同的位址出去。所以我就寫了 fluxy,一個 HTTP 代理,每一條經過它的連線,都會從我的位址池裡隨機挑一個位址作為來源位址送出去。前提
fluxy 的運作原理是,在發起連線之前,把 socket 綁定到位址池裡的某個位址上。
所以我們首先要讓這些位址能路由回來。也就是說,前綴得先用 BIRD 宣告出去,這樣從這些位址送出去的封包,對方的回應封包才能找到我。宣告的設定之前已經介紹過了,在 protocol static 裡寫上前綴,再從 BGP session 裡匯出。
我們還需要讓本機接收送往這些位址的回應封包,並允許 socket 綁定到這些位址,可是我總不能把幾百個、甚至
2^{84}個位址一個個設到網路介面上。先往 lo 上加兩條 local 類型的路由,讓核心把送往整個前綴的封包都交給本機處理。這兩條路由負責接收回應封包,但 IPv4 和 IPv6 的綁定檢查還有一點差別。IPv4 可以直接綁定到這條 local 路由涵蓋的位址;IPv6 則還需要允許綁定未逐一設定到網路介面上的位址。
這項設定只在目前執行期間生效;如果要在重新啟動後保留,可以把 net.ipv6.ip_nonlocal_bind=1 寫進 /etc/sysctl.d/ 下的設定檔。
IPv4 的話,直接 ip addr add 44.31.223.0/24 dev lo 也能讓整段前綴成為本機路由,這是回環網路介面的特殊處理。如果想用 dummy 網路介面承載這段位址,也需要為整個前綴明確設定 local 路由,不能只靠 ip addr add。
這樣一來,整條路徑就通了。
送出去的時候,封包還是走預設路由,從上游出去,只是來源位址換成了位址池裡的某一個。回來的時候,因為前綴是我宣告的,回應封包會沿著 BGP 的路由回到這台機器,再由 local 路由交給對應的 socket。
隨機挑一個位址
隨機位址的思路很簡單,前綴的網路位元保持不變,主機位元隨機產生,然後把兩者做位元 OR。IPv6 位址本身就是一個 u128,所以直接當整數來算就行。
先右移再左移,把前綴的主機位元清零,得到網路部分。隨機數則反過來,先左移再右移,把高位元清零,只留下主機位元。最後兩部分一合,就是前綴裡的一個隨機位址。/44 的前綴有 84 個主機位元,隨機產生的位址幾乎不可能重複,相當於每一條連線都有一個獨一無二的來源位址。IPv4 也是一樣的寫法,只是換成了 u32。
HTTP 代理
代理部分是用 hyper 寫的,基於 tokio。由於 HTTP 代理的請求分兩種,所以有兩種處理方式:
一種是 CONNECT。存取 HTTPS 網站的時候,用戶端會先發一個 CONNECT 請求,讓代理去連目標的 443 連接埠,連上之後代理就只負責在兩邊之間搬資料。所以 fluxy 收到 CONNECT 之後,自己建立一個 socket,綁定到一個隨機位址,再去連線到目標,最後把用戶端和目標兩邊的流量接起來。
另一種是普通的 HTTP 請求,代理需要替用戶端把請求送出去,再把回應原樣傳回。這裡直接用 hyper 內建的用戶端,它的 HttpConnector 可以設定一個本機位址,所以每個請求都建立一個新的 connector,把隨機位址設進去就行。
每一條連線用了哪個位址,都會印出一行來,例如 example.com:443 via [2a0e:aa07:e0a0:...]:...,可以透過輸出確認位址的切換和使用情況。
發布和使用
fluxy 已經發布到 crates.io,可以直接用 cargo 安裝。
啟動的時候指定監聽位址,以及兩個位址池。
-b 是監聽位址,不寫的話預設是 0.0.0.0:51080。-4 和 -6 分別是 IPv4 和 IPv6 的位址池,兩個都是選填,只設定一個也能執行。位址池也不一定要用整個前綴,像這裡 IPv6 只給了 /44 裡的一個 /48,fluxy 就只會在這個 /48 裡隨機挑選。
然後用 curl 透過代理存取查詢 IP 的網站,連續請求幾次,就能看到每次傳回的位址都不一樣。