開発ログ:TCP/IP プロトコルスタック自作 with Rust

KLab Expert Camp 6 をやる
リポジトリ
参考
KLab Expert Camp 6
リファレンス実装
Google Drive 資料集
ブログ
build-essential に関する参考資料

Day 1:導入、デバイスとプロトコルの管理
目標とタスク
PR
資料

TCP/IP
プロトコルスタックとは

ソケット API とソケット
ソケットとは
ソケット socket とは、OS が提供する、プロセス間の通信エンドポイントである。同一ホスト内、またはネットワークを介した異なるホスト上のプロセス同士がデータをやり取りする際に、この「ソケット」が通信の出入り口の役割を果たす。
アプリケーション(=プロセス)において、ソケットはファイルディスクリプタとして扱うことができる。2 つのプロセス同士がソケットを介してデータをやりとりするため、両端のプロセスがファイルディスクリプタとしてソケットを管理している状態になっている。
- 引用元:(Qiita, 2025/06) ソケット通信を一緒に理解しよう!!
先述したようにアプリケーションから見れば、ファイルディスクリプタ file descriptor として扱うことができる。以下の例では、fd がソケットである。
int fd = socket(AF_INET, SOCK_STREAM, 0);
ソケット API とは
ソケット API は、OS が提供する、アプリケーションからソケットを操作するための API である。
位置づけ
ソケット API は、位置づけとしてはユーザ空間とカーネル空間の境界に存在する。
アプリケーションはソケット API を呼び出すことで、カーネル内部の TCP/IP プロトコルスタックを利用できる。ソケット API は POSIX で定義され、ほとんどの UNIX 系 OS(Linux, BSD, macOS)や Windows に実装されている。
ソケット API により、アプリケーションはソケットをファイルディスクリプタとして扱えるようになるため、他のプロセスとの通信を通常のファイル I/O(ファイルへの読み書き)と同じインタフェースで実現することができる。
具体的な関数群
ソケット API を構成する主な関数(システムコール)は以下の通り:
-
socket()
: ソケット生成(ファイルディスクリプタを返す) -
bind()
: ソケットに IP アドレスとポート番号を割り当てる -
listen()
: ソケットをサーバ用に待ち受け状態にする -
accept()
: 接続要求を受け付け、新しいソケットを返す -
connect()
: クライアントからサーバへの接続を開始する -
send()
/recv()
: データ送受信 -
close()
: ソケットを閉じる

ソケット API の定義と役割
POSIX における仕様の定義
- POSIX 標準 (IEEE 1003.1, The Open Group Base Specifications)
- ソケット API(
socket()
,bind()
,connect()
,listen()
,accept()
,send()
,recv()
,close()
等)の関数インターフェイスと挙動を標準として定義している。 - もともとは 4.2 BSD における「BSD ソケット API」が起源だが、POSIX に取り込まれて事実上の標準となりました。
- ソケット API(
ソケット API の実装(実体)はどこにあるのか
C 言語のアプリケーションを例に。
宣言部
C 言語のアプリケーションを実装するうえでは、ソケット API(socket()
などの関数群)の定義はヘッダファイル <sys/socket.h>
に定義されている。
int socket(int domain, int type, int protocol);
のように、このヘッダファイルで関数プロトタイプを宣言を行っている。ヘッダファイルには関数本体の実装は存在せず、コンパイラに「こういう関数がある」と知らせる役割のみである。
ソケット API の実装
ソケット API の実装は libc(glibc, musl, BSD libc 等)に存在する。libc はシステムコールのラッパーであり、ソケット API の 関数シンボルとその実装を提供している。
たとえば socket()
関数の中で syscall(SYS_socket, …)
を呼び出し、カーネル空間へ処理を委譲する。アプリケーションが呼んでいるのは libc の socket()
関数である。
もちろん低レベルな API である syscall(SYS_socket, ...)
を直接呼び出すことでシステムはできるが、一般的なアプリケーションでこれをやることは少ないと思われる。
アプリケーションで libc の socket()
-> syscall(SYS_socket, …)
が実行されると、カーネルへ制御が移る。 CPU の命令(syscall
命令など)でユーザ空間からカーネル空間へコンテキストスイッチが行われる。
カーネル内部の実装
カーネルに制御が移ると、カーネルに実装されている sys_socket
という関数が実行される。この関数では、struct socket
を生成し、適切なプロトコルスタックに紐付ける。そして、新しいファイルディスクリプタをアプリケーションに返す。

ソケット API を使ってみる
「アプリケーションからソケット API を呼び出してソケットが生成されるまで」の流れを整理してみる。
ソースコード
以下にサンプルコードを示す。
2 つのソケットを生成してそのファイルディスクリプタの番号を出力するというかなりシンプルなコードである。
プロセス起動時に、プロセスにファイルディスクリプタ 0, 1, 2 が割り当てられるため、おそらく 2 つのソケットを生成したらファイルディスクリプタは 3, 4 が割り当てられるはずである。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> // AF_INET 等
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int create_socket(void) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("socket");
return -1;
}
return fd;
}
int main(void) {
printf("Start creating sockets...\n");
// IPv4/TCP ソケットを作る(プロトコルは 0 で自動選択)
// fds: file descriptors
// fd: file descriptor
// create array of file descriptors
int fds[2];
for (int i = 0; i < 2; i++) {
fds[i] = create_socket();
if (fds[i] == -1) {
perror("Failed to create socket");
return 1;
}
printf("socket fd%d = %d\n", i, fds[i]);
}
// close all sockets
for (int i = 0; i < 2; i++) {
close(fds[i]);
}
printf("All sockets closed.\n");
return 0;
}
ビルド & 実行
gcc create_socket.c -o main
./main
出力
Start creating sockets...
socket fd0 = 3
socket fd1 = 4
All sockets closed.
strace でシステムコールを追ってみる
gcc create_socket.c -o main
strace -o create_socket.log ./main
cat create_socket.log
create_socket.log
execve("./main", ["./main"], 0xffffd0a08c90 /* 32 vars */) = 0
brk(NULL) = 0xca3e1ccf5000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xed52ae6ce000
faccessat(AT_FDCWD, "/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=23783, ...}) = 0
mmap(NULL, 23783, PROT_READ, MAP_PRIVATE, 3, 0) = 0xed52ae6c8000
close(3) = 0
openat(AT_FDCWD, "/lib/aarch64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\360\206\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1722920, ...}) = 0
mmap(NULL, 1892240, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_DENYWRITE, -1, 0) = 0xed52ae4c7000
mmap(0xed52ae4d0000, 1826704, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xed52ae4d0000
munmap(0xed52ae4c7000, 36864) = 0
munmap(0xed52ae68e000, 28560) = 0
mprotect(0xed52ae66a000, 77824, PROT_NONE) = 0
mmap(0xed52ae67d000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0xed52ae67d000
mmap(0xed52ae682000, 49040, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xed52ae682000
close(3) = 0
set_tid_address(0xed52ae6cefb0) = 18451
set_robust_list(0xed52ae6cefc0, 24) = 0
rseq(0xed52ae6cf600, 0x20, 0, 0xd428bc00) = 0
mprotect(0xed52ae67d000, 12288, PROT_READ) = 0
mprotect(0xca3de8bdf000, 4096, PROT_READ) = 0
mprotect(0xed52ae6d3000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0xed52ae6c8000, 23783) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x1), ...}) = 0
getrandom("\x45\x9a\x98\x85\x7b\xc3\x8b\xf8", 8, GRND_NONBLOCK) = 8
brk(NULL) = 0xca3e1ccf5000
brk(0xca3e1cd16000) = 0xca3e1cd16000
write(1, "Start creating sockets...\n", 26) = 26
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
write(1, "socket fd0 = 3\n", 15) = 15
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 4
write(1, "socket fd1 = 4\n", 15) = 15
close(3) = 0
close(4) = 0
write(1, "All sockets closed.\n", 20) = 20
exit_group(0) = ?
+++ exited with 0 +++
socket
, close
に絞ってみる:
strace -o create_socket.mini.log -e socket,close ./main
cat create_socket.mini.log
出力。一部関係のない部分は省略。ソケット API を利用してsocket
システムコールを発行できた。
...
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 4
close(3) = 0
close(4) = 0

全体像
登場人物
-
Application
- ユーザ空間の実行バイナリ。
-
#include <sys/socket.h>
によりint socket(int domain, int type, int protocol);
の宣言を得て、libc のsocket()
関数を呼び出す。
-
libc(ソケット API 実装)
- glibc/musl 等の動的ライブラリ。
- ソケット API の
socket()
関数の実体を提供し、内部でシステムコール(SYS_socket
)を発行して Kernel に処理を委譲する。 - ここが「POSIX ソケット API」を実装している主体。
-
Kernel
- システムコール
sys_socket
を実装。 -
struct socket
を生成し、プロトコルスタックに紐付け、fd を割り当てて返却します。
- システムコール
※ ヘッダ <sys/socket.h>
は宣言のみ(コンパイル時の役割)。実体は libc。処理本体は Kernel。
ソケット生成の処理の流れ
-
Application
-
#include <sys/socket.h>
によりsocket()
の関数プロトタイプを得る。 - 実行時、
socket(AF_INET, SOCK_STREAM, 0)
を libc のsocket()
に解決して呼び出す。
-
-
libc
-
socket()
の内部でシステムコール(SYS_socket
)を発行し、ユーザ空間からカーネル空間へ制御を移す。 - 引数(
AF_INET
,SOCK_STREAM
,0
)はそのまま Kernel に渡される。
-
-
Kernel
-
sys_socket
が呼ばれ、構造体struct socket
を 生成。 - 対応するプロトコルスタック(例:TCP/IPv4)にソケットを紐付け。
- 新規ファイルディスクリプタ(int 型)を割り当て、戻り値としてユーザ空間へ返す。
-
-
libc → Application
- libc は Kernel から受け取ったファイルディスクリプタ
fd
をsocket()
の戻り値として Application に返す。 - 以後、アプリはこの
fd
に対してconnect()
/bind()
/listen()
/accept()
/read()
/write()
/close()
等を行える。
- libc は Kernel から受け取ったファイルディスクリプタ
シーケンス図

再掲

microps のチュートリアル DONE
VM 内で、サンプルアプリケーションに対して ping による疎通確認と nc による TCP 接続が確認できた。Makefile については以下の参照。
microps の構造

とりあえずテストプログラムの hexdump まで。
./test/step0.exe
14:40:27.068 [D] main: Hello, World! (test/step0.c:7)
+------+-------------------------------------------------+------------------+
| 0000 | 45 00 00 30 00 80 00 00 ff 01 bd 4a 7f 00 00 01 | E..0.......J.... |
| 0010 | 7f 00 00 01 08 00 35 64 00 80 00 01 31 32 33 34 | ......5d....1234 |
| 0020 | 35 36 37 38 39 30 21 40 23 24 25 5e 26 2a 28 29 | 567890!@#$%^&*() |
+------+-------------------------------------------------+------------------+

いよいよ開発

STEP 1: デバイスの管理

TCP/IP のネットワークインタフェース層を実装していく。プロトコルは Ethernet
プロトコルを使用する。
※Ethernet が支配的だが他のリンクプロトコル用のネットワークデバイスも存在する
ここは IP パケットの出入り口である。IP パケットはネットワークデバイスを通じてネットワークインタフェース層のプロトコル(リンクプロトコル、データリンクプロトコル)によって運ばれる。

構造としては以下のようになる。
アプリケーション
→ カーネル [
→ ソケット API
→ TCP/IP プロトコルスタック
→ デバイスドライバ
] → ネットワークデバイス(NIC: Network Interface Card。物理 NIC。VM の場合、仮想 NIC → 物理 NIC という流れになる。)
物理 NIC のイメージ:
引用元:ネットワークエンジニアとして - サーバのハードウェア

実装タスク

clang-format のインストール
sudo apt update -y && apt install -y clang-format
clangd の設定
sudo apt update -y && sudo apt install -y clangd
which clangd
# /usr/bin/clangd
clangd --version
# Ubuntu clangd version 18.1.3 (1ubuntu1)
# Features: linux+grpc
# Platform: aarch64-unknown-linux-gnu