monolithic kernel

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_reuseportsetsockoptSO_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されたsockTCP_TIME_WAIT状態でなく、かつすでにbindされたsockと新しくbindしようとするsockのuidが一致する

要するに、SO_REUSEPORTで同じポートにbindできるのは、同じuidのプロセスだけということになります。

実際に試してみたところ、同じuidであればbindできましたが、uidが異なる場合には、例え後からbindするのがrootであってもbindできませんでした。

当たり前かもしれませんが、よく考えられていると思いました。そもそも、あるソフトウェアの親プロセスだとか子プロセスだとかのセマンティクスはカーネルからしたら知ったことではないので、よく考えればuidくらいしかやりようがないよね、という話かもしれませんが。