ephemeral ports 範囲からどうやってポート番号が選択されるのか調べてみた

すっかり遅くなりましたが、明けましておめでとうございます. 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 インタフェースなので、こういう変換アダプタを買いました.

先のコード読みも新たに作った環境で検証してみようと思います.

では.