さらに、これらのアドレスへの応答をローカルホストで受信できるようにし、socket の bind も許可する必要がある。だからといって、数百個、ましてや 2^{84} 個のアドレスを一つずつインターフェースに設定するわけにはいかない。まず lo に local タイプのルートを二つ追加し、プレフィックス全体へのパケットをローカルで処理するようにする。
BASH
ip -4 route add local 44.31.223.0/24 dev lo
ip -6 route add local 2a0e:aa07:e0a0::/44 dev lo
IPv4 なら、ip addr add 44.31.223.0/24 dev lo でもプレフィックス全体がローカルルートになる。これはループバックインターフェースに対する特別な処理だ。dummy インターフェースにこのアドレスを置く場合も、プレフィックス全体に対して明示的に local ルートを設定する必要があり、ip addr add だけでは足りない。
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 をアドレスプール内のいずれかのアドレスに bind する。
そのため、まずはこれらのアドレスへの戻りの経路を用意する必要がある。つまり、BIRD でプレフィックスを広報しておかなければならない。そうすれば、そのアドレスから送ったパケットに対する応答が、こちらへ戻ってこられる。広報の設定は以前紹介したとおり、protocol static にプレフィックスを書き、BGP セッションで export する。
さらに、これらのアドレスへの応答をローカルホストで受信できるようにし、socket の bind も許可する必要がある。だからといって、数百個、ましてや
2^{84}個のアドレスを一つずつインターフェースに設定するわけにはいかない。まず lo に local タイプのルートを二つ追加し、プレフィックス全体へのパケットをローカルで処理するようにする。この二つのルートは応答の受信を担当するが、IPv4 と IPv6 では bind のチェックに少し違いがある。IPv4 では、この local ルートがカバーするアドレスへ直接 bind できる。一方、IPv6 では、インターフェースに個別に設定していないアドレスへの bind も許可する必要がある。
この設定は一時的なものだ。再起動後も維持するには、/etc/sysctl.d/ 配下の設定ファイルに net.ipv6.ip_nonlocal_bind=1 を書いておく。
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 も同じ書き方で、u128 を u32 に変えればよい。
HTTP プロキシ
プロキシ部分は tokio 上で hyper を使って書いた。HTTP プロキシには二種類のリクエストがあるため、処理も二通りに分かれる。
一つは CONNECT だ。HTTPS のサイトへアクセスするとき、クライアントはまず CONNECT リクエストを送り、プロキシに接続先の 443 番ポートへ接続させる。接続後、プロキシは両者のデータを中継するだけになる。そこで fluxy は CONNECT を受け取ると、新しい socket を作り、ランダムなアドレスに bind してから接続先へ接続し、クライアントと接続先の間でデータをやり取りする。
もう一つは通常の 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 のアドレスプールで、どちらも省略可能だ。片方だけでも動く。プールにプレフィックス全体を渡す必要もない。この例のように /44 の中の /48 だけを指定すれば、fluxy はその /48 の範囲でアドレスをランダムに選ぶ。
あとは curl でプロキシ経由で IP 確認サイトにアクセスする。何度か続けてリクエストすると、そのたびに返ってくるアドレスが変わる。