SO_REUSEPORTのセキュリティ
SO_REUSEPORTが盛り上がっているみたいですね。
私はコミットログを読んで性能向上のための機能だと認識していて、親プロセスが開いたソケットのfdを子プロセスが使うpreforkのモデルに代わって、子プロセスがそれぞれソケットを開いて使うものだと思い込んでいました。そのため、まったく関係のないプロセスがbindしているポートに対して追加でbindできてしまうというのはなかなか驚きでした。
ただ、何も制限なしにbindできてしまったら、悪意のある何者かが同じ計算機内のすでに使われているポートにbindすることで、一定確率で外部からの通信を奪い取ることができてしまいます。何か制限があるはずだと思い、ソースコードを眺めてみました。
真面目に追い掛けたわけではありませんが、ざっと見たところ、おそらくこの関数で判定しているはず。
int inet_csk_bind_conflict(const struct sock *sk, const struct inet_bind_bucket *tb, bool relax){ struct sock *sk2; struct hlist_node *node; int reuse = sk->sk_reuse; int reuseport = sk->sk_reuseport; kuid_t uid = sock_i_uid((struct sock *)sk);
/* * Unlike other sk lookup places we do not check * for sk_net here, since _all_ the socks listed * in tb->owners list belong to the same net - the * one this bucket belongs to. */
sk_for_each_bound(sk2, node, &tb->owners) { if (sk != sk2 && !inet_v6_ipv6only(sk2) && (!sk->sk_bound_dev_if || !sk2->sk_bound_dev_if || sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) { if ((!reuse || !sk2->sk_reuse || sk2->sk_state == TCP_LISTEN) && (!reuseport || !sk2->sk_reuseport || (sk2->sk_state != TCP_TIME_WAIT && !uid_eq(uid, sock_i_uid(sk2))))) { const __be32 sk2_rcv_saddr = sk_rcv_saddr(sk2); if (!sk2_rcv_saddr || !sk_rcv_saddr(sk) || sk2_rcv_saddr == sk_rcv_saddr(sk)) break; } if (!relax && reuse && sk2->sk_reuse && sk2->sk_state != TCP_LISTEN) { const __be32 sk2_rcv_saddr = sk_rcv_saddr(sk2);
if (!sk2_rcv_saddr || !sk_rcv_saddr(sk) || sk2_rcv_saddr == sk_rcv_saddr(sk)) break; } } } return node != NULL;}EXPORT_SYMBOL_GPL(inet_csk_bind_conflict);
sock
構造体のsk_reuseport
はsetsockopt
でSO_REUSEPORT
を有効にした場合に真になります (net/core/sock.c
)。というわけで、以下の行が関係していそうです。
(!reuseport || !sk2->sk_reuseport ||(sk2->sk_state != TCP_TIME_WAIT && !uid_eq(uid, sock_i_uid(sk2))))
ちょっと読みにくいですが、おそらく以下のような意味でしょう。これ (とその手前にある条件) を満たす場合には2つのsock
のアドレスを確認し、一致するか一方でも0である場合にはbindを禁止しています。
- すでにbindされた
sock
と新しくbindしようとするsock
のどちらか一方でもSO_REUSEPORT
が有効でない - すでにbindされた
sock
がTCP_TIME_WAIT
状態でなく、かつすでにbindされたsock
と新しくbindしようとするsock
のuidが一致する
要するに、SO_REUSEPORT
で同じポートにbindできるのは、同じuidのプロセスだけということになります。
実際に試してみたところ、同じuidであればbindできましたが、uidが異なる場合には、例え後からbindするのがrootであってもbindできませんでした。
当たり前かもしれませんが、よく考えられていると思いました。そもそも、あるソフトウェアの親プロセスだとか子プロセスだとかのセマンティクスはカーネルからしたら知ったことではないので、よく考えればuidくらいしかやりようがないよね、という話かもしれませんが。