おっしゃるとおり、古典的な TCP/IP スタックの場合は、エフェメラルポートを確保してから connect(2) するので、クライアントとして同時接続数はポートの数に律速されます。一方で、linux の場合は IP_BIND_ADDRESS_NO_PORT というオプションがあり、これを使うと... あとはmanを読んでください
— Kazuho Oku (@kazuho) 2022年8月30日
これを噛み砕くのに CloudFlare の記事を参照したのだが、あんまりちゃんとみてなくて理解に時間がかかったので備忘がてらまとめる。
CloudFlare の問題
エフェメラルポートが枯渇して、自分発の通信ができなくなった。
Linux のデフォルトの挙動
通信をする際には宛先情報と送信元情報が必要になる。Linux のデフォルトでは、OSが送信元IP・ポートを選択し付与する。
※↑引用
cd = socket.socket(AF_INET, SOCK_STREAM) cd.connect(('104.1.1.229', 80))
となるので、connect()
時に送信元を選んでる
通常の方法では
- 宛先が変われば、送信元IP&ポートの組み合わせは使いまわせる
- 宛先が同じならエフェメラルポートの上限が通信数の限界
CloudFlare の送信元IPをアプリで指定する話
CDNは客のオリジンサーバにアクセスするため、顧客がIP制限をできるように意図したIPアドレスをつかう必要がある。そのためOSが決める方式ではなく、アプリで指定している。
bind()
をつかってIPの固定化ができるのだが、bind 時は宛先ポートがわからないので通常の方法のような 送信元IP&ポートの組み合わせができない。
つまり、通常よりも作成できる通信数が少なくなる。
ただしIP_BIND_ADDRESS_NO_PORT
を使うと、送信元ポートの予約を遅らせることができる。これにより IP をとりあえず固定化しておき、送信元ポートは connect 時に発行する。
sd = socket.socket(SOCK_STREAM) sd.setsockopt(IPPROTO_IP, IP_BIND_ADDRESS_NO_PORT, 1) sd.bind( (src_IP, 0) ) sd.connect( (dst_IP, dst_port) )
わかったこと
- 送信元の組み合わせが同じでも、宛先が違うなら使いまわせる
- IP アドレスを増やせば送信元の組み合わせが増えるので、ポート枯渇の心配が減る
- ただしアプリで特定のIPを強制したい場合は、OSではなくアプリ側で制御する必要がある
- このとき
IP_BIND_ADDRESS_NO_PORT
を使わないと、宛先ごとに送信元の組み合わせが同じ物を使いまわせない