すっかり遅くなりましたが、明けましておめでとうございます. 2025年もよろしくお願いいたします.
年末年始は毎年恒例、鳴子温泉でゆっくり過ごしました. 何かと忙しかった2024年でしたが,その疲れも鳴子温泉のお湯ですっかり癒されました.
さて,年明け早々, 長らく書いてなかった技術ネタで2025年は幕開けしたいと思います.
TCPで通信する際, ソースポートはどうやって決まるんだろね?,ということを職場で話してて, 実際 Linux Kernelの中ではどのようにしてるんだろ? と疑問に思ったのが今回の記事を書くきっかけです.
ただ, 初心者のにわかLinux Kernelのコード読者ですので, コードの深堀が不足している点、あらかじめご了承ください.
Linux の エフェメラルポート(ephemeral ports)
TCP クライアントがサーバーに対して明示的に bind せずに connect した場合, 通信に使うソースポートは Linux Kernel が自動的に決定します.
Linux Kernel はプロトコルによって固定的に決まっているポート番号(System Ports)以外、用途が決まっていないポート番号から選びます. これを一時的なポートという意味で, エフェメラルポート / ephemeral ports と呼ばれます.
ephemeral ports として利用されるポート番号範囲は /proc/sys/net/ipv4/ip_local_port_range より観ることが出来ます.
kawauso@linux$ cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999
また、上記のファイルの直接書くことによってポート範囲を設定することも可能です.
ephemeral ports からポート番号の選択
github から取ってきた最近の Linux Kernel のコード(6.14)を見ると, エフェメラルポートからソースポート番号を決定するのは linux/net/ipv4/inet_hashtables.c の __inet_hash_connect()で行われてるようです.
以下は, linux/net/ipv4/inet_hashtables.c の__inet_hash_connect() の抜粋です.
int __inet_hash_connect(struct inet_timewait_death_row *death_row,
struct sock *sk, u64 port_offset,
int (*check_established)(struct inet_timewait_death_row *,
struct sock *, __u16, struct inet_timewait_sock **))
{
struct inet_hashinfo *hinfo = death_row->hashinfo;
struct inet_bind_hashbucket *head, *head2;
struct inet_timewait_sock *tw = NULL;
int port = inet_sk(sk)->inet_num;
struct net *net = sock_net(sk);
struct inet_bind2_bucket *tb2;
struct inet_bind_bucket *tb;
bool tb_created = false;
u32 remaining, offset;
int ret, i, low, high;
bool local_ports;
int step, l3mdev;
u32 index;
if (port) {
local_bh_disable();
ret = check_established(death_row, sk, port, NULL);
local_bh_enable();
return ret;
}
l3mdev = inet_sk_bound_l3mdev(sk);
local_ports = inet_sk_get_local_port_range(sk, &low, &high);
step = local_ports ? 1 : 2;
high++; /* [32768, 60999] -> [32768, 61000[ */
remaining = high - low;
if (!local_ports && remaining > 1)
remaining &= ~1U;
get_random_sleepable_once(table_perturb,
INET_TABLE_PERTURB_SIZE * sizeof(*table_perturb));
index = port_offset & (INET_TABLE_PERTURB_SIZE - 1);
offset = READ_ONCE(table_perturb[index]) + (port_offset >> 32);
offset %= remaining;
/* In first pass we try ports of @low parity.
* inet_csk_get_port() does the opposite choice.
*/
if (!local_ports)
offset &= ~1U;
other_parity_scan:
port = low + offset;
for (i = 0; i < remaining; i += step, port += step) {
if (unlikely(port >= high))
port -= remaining;
if (inet_is_local_reserved_port(net, port))
continue;
head = &hinfo->bhash[inet_bhashfn(net, port,
hinfo->bhash_size)];
spin_lock_bh(&head->lock);
/* Does not bother with rcv_saddr checks, because
* the established check is already unique enough.
*/
inet_bind_bucket_for_each(tb, &head->chain) {
if (inet_bind_bucket_match(tb, net, port, l3mdev)) {
if (tb->fastreuse >= 0 ||
tb->fastreuseport >= 0)
goto next_port;
WARN_ON(hlist_empty(&tb->bhash2));
if (!check_established(death_row, sk,
port, &tw))
goto ok;
goto next_port;
}
}
- /proc/sys/net/ipv4/ip_local_port_range にあるポート範囲の最小値, 最大値を読む. 29行目
- 最大値から最小値を引いて利用可能なポート数を算出. 33行目
- 乱数の配列
table_perturb
を生成. 37行目 - 乱数からオフセット値を決定. 41行目
- 利用可能なポート番号の最小値にオフセット値を加算し, 使用するポート番号の候補 port を決める. 50行目
- 使用するポート番号の候補 port が利用可能か判定し, 利用可能であれば採用. 利用不可であれば port 番号に1加算し, 次の port番号を評価. これを利用可能なポート範囲内で実行. 51行目
以前のKernelでは /proc/sys/net/ipv4/ip_local_port_range にあるポート範囲の中で毎回ランダムなポート番号を決めていたようなコードですが, 最近のコードでは最初の候補はランダム, その中で施行するポート番号は連続になっているようです.
最近自宅にUbuntu環境を作りました
古くなって第一線を退いてもらった自宅PCにUbuntu24.04を入れて,ちょっとした動作確認できるような環境を作りました.
ついでに, 自宅リビングのPCから Ubuntu PCをアクセスできるようにリモートディスクトップの設定と,,やっぱりコードを触るにはこのキーボードしか無い, ということでHappy Hacking Keyboad Liteを引っ張りだしてきました.
ただ, これ PS/2 インタフェースなので、こういう変換アダプタを買いました.
先のコード読みも新たに作った環境で検証してみようと思います.
では.