100日チャレンジ day32 (自作IP/TCP/TLS/HTTP1.1&2スタック)

昨日
100日チャレンジに感化されたので、アレンジして自分でもやってみます。
やりたいこと
- 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
- 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
- 生成aiをつかったバイブコーティングになれる
- 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)
できたもの

IP / TCP / HTTP スタックを作る

まずは IP レイヤーから
承知いたしました。Day 32 で IP レイヤーの実装を目指しましょう。まずは Phase 1 として IP 部分の仕様書を作成します。
Day 32: ユーザー空間 IP パケットパーサー (Phase 1: IP) 仕様書
1. 概要
本プログラムは、Go 言語を用いてユーザー空間で動作する IP パケットパーサーです。オペレーティングシステムのネットワークスタックを経由せず、TUN デバイスを通じて直接 IP パケットを読み取り、そのヘッダー情報を解析して標準出力に表示します。Phase 1 では IPv4 ヘッダーの解析に焦点を当てます。
目的:
- Go 言語による低レベルネットワークプログラミングの基礎を学ぶ。
- TUN デバイスの基本的な使い方を理解する。
- IPv4 ヘッダーの構造と各フィールドの意味をコードレベルで理解する。
- 将来的なユーザー空間 TCP/IP スタック実装の第一歩とする。
2. 機能要件
-
TUN デバイスからの読み取り:
- 指定された TUN デバイス (例:
utunN
) をオープンし、IP パケットを含むバイトストリームを継続的に読み取る。 - デバイス名はコマンドライン引数または設定ファイルで指定可能とする (初期実装ではハードコードでも可)。
- 指定された TUN デバイス (例:
-
IPv4 ヘッダー解析:
- 読み取ったバイト列が IPv4 パケットであると仮定し、ヘッダー部分を解析する。
- 以下のフィールドを抽出する:
- Version (バージョン、常に 4 であることを確認)
- IHL (Internet Header Length、ヘッダー長)
- TOS (Type of Service)
- Total Length (IP パケット全体の長さ)
- Identification (識別子)
- Flags (DF: Don't Fragment, MF: More Fragments)
- Fragment Offset (フラグメントオフセット)
- TTL (Time to Live)
- Protocol (上位プロトコル番号: TCP=6, UDP=17, ICMP=1 など)
- Header Checksum (ヘッダーチェックサム) - 値の抽出のみ。検証は必須ではない。
- Source IP Address (送信元 IP アドレス)
- Destination IP Address (宛先 IP アドレス)
- Options (IP オプション、存在する場合) - オプション内容の解析は必須ではないが、存在することと長さを認識できることが望ましい。
-
情報表示:
- 解析したヘッダー情報を、人間が読みやすい形式で標準出力に表示する。
- 最低限、送信元/宛先 IP アドレス、プロトコル番号、全長を表示する。可能であれば他のフィールドも表示する。
- エラー発生時 (不正なヘッダーなど) は、エラーメッセージを出力する。
3. 非機能要件
-
パフォーマンス: 大量のパケットをリアルタイムで処理する必要はないが、一般的な
ping
等の応答に対して遅延なく動作すること。 - プラットフォーム: 主に macOS での動作を想定する。
- エラーハンドリング: TUN デバイスのオープン失敗、読み取りエラー、ヘッダー解析エラーなどを適切にハンドリングし、エラーメッセージを出力して処理を継続または終了する。
4. 実装詳細
- 使用言語: Go
-
主要ライブラリ候補:
- TUN/TAP インターフェース:
songgao/water
(https://github.com/songgao/water) - バイナリデータ操作:
encoding/binary
,bytes
- ネットワーク関連型:
net
- コマンドライン引数:
flag
- TUN/TAP インターフェース:
-
データ構造: IPv4 ヘッダーを表す Go の
struct
を定義する。
// 例: IPv4 ヘッダー構造体 (簡略版)
type IPv4Header struct {
Version uint8
IHL uint8 // Header Length in 32-bit words
TOS uint8
TotalLength uint16
ID uint16
Flags uint8 // 3 bits flags + 13 bits Fragment Offset の上位3bit
FragmentOffset uint16 // 3 bits flags + 13 bits Fragment Offset の下位13bit
TTL uint8
Protocol uint8
Checksum uint16
SrcIP net.IP
DstIP net.IP
Options []byte // 可変長オプション (存在する場合)
// 必要に応じてフィールドを追加・変更
}
-
主要処理フロー:
- プログラム起動、コマンドライン引数解析 (TUN デバイス名など)。
- TUN デバイスのオープンと設定 (IP アドレス割り当て等は外部の手動設定を前提とする)。
- 無限ループ開始:
a. TUN デバイスからパケット (バイト列) を読み取る。
b. 読み取ったバイト列をIPv4Header
構造体にパースする関数を呼び出す。
i. バイト列から各フィールドをencoding/binary
などを使って抽出する (エンディアンに注意)。
ii. IHL を見てヘッダー長を決定し、オプションがあれば読み取る。
iii.SrcIP, DstIP をnet.IP
型に変換する。
c. パース結果を整形して標準出力に表示する。
d. エラー発生時はエラーメッセージを表示する。 - (シグナルハンドリング等で) 終了処理。TUN デバイスをクローズする。
5. 環境設定 (macOS 向け - 手動設定)
-
tuntap のインストール (推奨):Homebrew のインストールが必要です。
brew install tuntap
-
TUN デバイスの作成と設定 (例):
ターミナルで以下のコマンドを実行 (要管理者権限)# TUNデバイスを作成 (例: utun3、システムによって番号は変わる可能性あり) # Goプログラム側で water.New() を呼ぶと自動的に作成されることが多いが、 # 事前に確認・手動作成が必要な場合がある。 # IPアドレスとルーティングを設定 sudo ifconfig utun3 10.0.0.1 10.0.0.2 netmask 255.255.255.0 up sudo route add -net 10.0.0.0/24 10.0.0.2
utun3
の部分は実際に利用可能なデバイス名に適宜変更してください。
Go プログラム (songgao/water
を使用する場合) は、起動時に指定した名前 (なければ自動) の utun デバイスを作成しようとします。上記はプログラムが使う IP アドレス範囲を設定する例です。
6. 実行方法
# Go プログラムをビルド
go build -o ip_parser main.go
# 実行 (例: utun3 を使用する場合)
sudo ./ip_parser -dev utun3
TUN デバイスへのアクセスには通常管理者権限が必要です。
7. 出力フォーマット例
Packet Received (60 bytes) on utun3
IPv4 Header:
Version: 4, IHL: 5 (20 bytes), TOS: 0x00
Total Length: 60, ID: 0x1234, Flags: DF, Fragment Offset: 0
TTL: 64, Protocol: 6 (TCP)
Checksum: 0xabcd
Source IP: 10.0.0.1
Destination IP: 10.0.0.2
---
Packet Received (84 bytes) on utun3
IPv4 Header:
Version: 4, IHL: 5 (20 bytes), TOS: 0x00
Total Length: 84, ID: 0x5678, Flags: None, Fragment Offset: 0
TTL: 64, Protocol: 17 (UDP)
Checksum: 0xef01
Source IP: 10.0.0.2
Destination IP: 10.0.0.1
---
8. フェーズ 1 のスコープ外
- IPv6 ヘッダーの解析
- IP オプションフィールドの詳細な解析
- ヘッダーチェックサムの計算と検証
- IP パケットのフラグメンテーションと再構築
- IP パケットの送信 (書き込み)
- TCP, UDP, ICMP など上位レイヤープロトコルの解析
- プログラム内での TUN デバイスの自動作成・設定 (環境依存性が高いため)
この仕様書で Phase 1 を進めてよろしいでしょうか? よろしければ、Go プロジェクトのセットアップから開始します。

❯ sudo ./ip_parser
Password:
2025/05/03 16:17:38 Attempting to setup TUN device ''...
2025/05/03 16:17:38 TUN device 'utun9' created by water.
2025/05/03 16:17:38 Configuring device 'utun9' with IP 10.0.0.1, Peer 10.0.0.2, Mask 255.255.255.0, MTU 1500
2025/05/03 16:17:38 ifconfig output:
2025/05/03 16:17:38 Adding route for network 10.0.0.0/24 via 10.0.0.2
2025/05/03 16:17:38 Route add output: add net 10.0.0.0: gateway 10.0.0.2
2025/05/03 16:17:38 TUN device 'utun9' created and configured successfully.
2025/05/03 16:17:38 Interface IP: 10.0.0.1, Peer IP: 10.0.0.2, Subnet Mask: 255.255.255.0
2025/05/03 16:17:38 Listening for packets...
--- Packet Received (84 bytes) ---
IPv4 Header (20 bytes):
Version: 4, IHL: 5 (20 bytes), TOS: 0x00
Total Length: 84, ID: 0xb654
Flags: None, Fragment Offset: 0
TTL: 64, Protocol: 1 (ICMP)
Checksum: 0xb052
Source IP: 10.0.0.1
Destination IP: 10.0.0.2
----------------------------------
とりあえず tun つくって読み取れるところまで

ICMP echo reply に成功するようになった

承知いたしました。次は TCP の実装に進みましょう。
TCP (Transmission Control Protocol) は IP や ICMP と比べて格段に複雑です。コネクションの概念があり、以下のような要素を扱う必要があります。
- 状態管理: コネクションの状態 (LISTEN, SYN_SENT, SYN_RECEIVED, ESTABLISHED, FIN_WAIT, CLOSE_WAIT, CLOSED など) を管理するステートマシン。
- ハンドシェイク: コネクション確立のための 3 ウェイハンドシェイク (SYN, SYN-ACK, ACK)。
- シーケンス番号と確認応答 (ACK): データの順序保証と到達確認。
- ウィンドウ制御: フロー制御。
- 再送制御: パケットロスへの対応。
- 切断処理: FIN パケットによる正常な切断。
これらすべてを一度に実装するのは非常に大変です。そこで、Phase 2: TCP として、まずは TCP ヘッダーの解析と、基本的なフラグ (SYN, ACK, FIN, RST) の認識 に焦点を当てるのはいかがでしょうか?
Phase 2 の目標:
-
TCPHeader
構造体を定義する。 -
processPackets
関数内で、受信した IP パケットのプロトコルが TCP (番号 6) であることを検知する。 -
handleTCPPacket
関数を作成し、TCP ヘッダー部分をパースする。 - パースした結果から、送信元ポート、宛先ポート、シーケンス番号、確認応答番号、および主要なフラグ (SYN, ACK, FIN, RST) を抽出し、ログに出力する。
- このフェーズでは、TCP コネクションの状態管理や応答パケットの送信は行いません。 まずは受信した TCP パケットのヘッダーを正しく読み取れることを目標とします。
このステップを踏むことで、TCP プロトコルの基本構造を理解し、より複雑な状態管理や送受信ロジックに進むための準備ができます。
このスコープで Phase 2 を進めてよろしいでしょうか?

自分からスコープを小さくしてくれている。素晴らしい

2025/05/03 16:25:53 TCP Packet: 10.0.0.1:56465 -> 10.0.0.2:80 Seq: 2273195199 Ack: 0 Flags: [SYN] Win: 65535 Len: 0
--- Packet Received (64 bytes) ---
IPv4 Header (20 bytes):
Version: 4, IHL: 5 (20 bytes), TOS: 0x00
Total Length: 64, ID: 0x0000
Flags: [DF], Fragment Offset: 0
TTL: 64, Protocol: 6 (TCP)
Checksum: 0x26b6
Source IP: 10.0.0.1
Destination IP: 10.0.0.2
----------------------------------
2025/05/03 16:25:54 TCP Packet: 10.0.0.1:56465 -> 10.0.0.2:80 Seq: 2273195199 Ack: 0 Flags: [SYN] Win: 65535 Len: 0
ここまでできた

3way handshake 成功!
❯ sudo go run main.go
2025/05/03 16:28:28 Attempting to setup TUN device ''...
2025/05/03 16:28:28 TUN device 'utun9' created by water.
2025/05/03 16:28:28 Configuring device 'utun9' with IP 10.0.0.1, Peer 10.0.0.2, Mask 255.255.255.0, MTU 1500
2025/05/03 16:28:28 ifconfig output:
2025/05/03 16:28:28 Adding route for network 10.0.0.0/24 via 10.0.0.2
2025/05/03 16:28:28 Route add output: add net 10.0.0.0: gateway 10.0.0.2
2025/05/03 16:28:28 TUN device 'utun9' created and configured successfully.
2025/05/03 16:28:28 Interface IP: 10.0.0.1, Peer IP: 10.0.0.2, Subnet Mask: 255.255.255.0
2025/05/03 16:28:28 Listening for packets...
--- Packet Received (64 bytes) ---
IPv4 Header (20 bytes):
Version: 4, IHL: 5 (20 bytes), TOS: 0x00
Total Length: 64, ID: 0x0000
Flags: [DF], Fragment Offset: 0
TTL: 64, Protocol: 6 (TCP)
Checksum: 0x26b6
Source IP: 10.0.0.1
Destination IP: 10.0.0.2
----------------------------------
2025/05/03 16:28:34 TCP RCV: 10.0.0.1:56630 -> 10.0.0.2:80 Seq: 397498099 Ack: 0 Flags: [SYN,ECE,CWR] Win: 65535 Len: 0
2025/05/03 16:28:34 Handling SYN for new connection 10.0.0.1:56630-10.0.0.2:80
2025/05/03 16:28:34 TCP SEND: 10.0.0.2:80 -> 10.0.0.1:56630 Seq: 2313623659 Ack: 397498100 Flags: [SYN,ACK] Len: 0
--- Packet Received (40 bytes) ---
IPv4 Header (20 bytes):
Version: 4, IHL: 5 (20 bytes), TOS: 0x00
Total Length: 40, ID: 0x0000
Flags: [DF], Fragment Offset: 0
TTL: 64, Protocol: 6 (TCP)
Checksum: 0x26ce
Source IP: 10.0.0.1
Destination IP: 10.0.0.2
----------------------------------
2025/05/03 16:28:34 TCP RCV: 10.0.0.1:56630 -> 10.0.0.2:80 Seq: 397498100 Ack: 2313623660 Flags: [ACK] Win: 65535 Len: 0
2025/05/03 16:28:34 Handling ACK for SYN-ACK for connection 10.0.0.1:56630-10.0.0.2:80
2025/05/03 16:28:34 Connection 10.0.0.1:56630-10.0.0.2:80 ESTABLISHED
--- Packet Received (40 bytes) ---
IPv4 Header (20 bytes):
Version: 4, IHL: 5 (20 bytes), TOS: 0x00
Total Length: 40, ID: 0x0000
Flags: [DF], Fragment Offset: 0
TTL: 64, Protocol: 6 (TCP)
Checksum: 0x26ce
Source IP: 10.0.0.1
Destination IP: 10.0.0.2
----------------------------------
2025/05/03 16:28:34 TCP RCV: 10.0.0.1:56630 -> 10.0.0.2:80 Seq: 397498100 Ack: 2313623660 Flags: [FIN,ACK] Win: 65535 Len: 0
2025/05/03 16:28:34 Received packet for established connection 10.0.0.1:56630-10.0.0.2:80 (Data handling not implemented)
--- Packet Received (40 bytes) ---
IPv4 Header (20 bytes):
Version: 4, IHL: 5 (20 bytes), TOS: 0x00
Total Length: 40, ID: 0x0000
Flags: [DF], Fragment Offset: 0
TTL: 64, Protocol: 6 (TCP)
Checksum: 0x26ce
Source IP: 10.0.0.1
Destination IP: 10.0.0.2
----------------------------------
2025/05/03 16:28:34 TCP RCV: 10.0.0.1:56630 -> 10.0.0.2:80 Seq: 397498100 Ack: 2313623660 Flags: [FIN,ACK] Win: 65535 Len: 0
2025/05/03 16:28:34 Received packet for established connection 10.0.0.1:56630-10.0.0.2:80 (Data handling not implemented)
--- Packet Received (40 bytes) ---
IPv4 Header (20 bytes):
Version: 4, IHL: 5 (20 bytes), TOS: 0x00
Total Length: 40, ID: 0x0000
Flags: [DF], Fragment Offset: 0
TTL: 64, Protocol: 6 (TCP)
Checksum: 0x26ce
Source IP: 10.0.0.1
Destination IP: 10.0.0.2
----------------------------------
2025/05/03 16:28:35 TCP RCV: 10.0.0.1:56630 -> 10.0.0.2:80 Seq: 397498100 Ack: 2313623660 Flags: [FIN,ACK] Win: 65535 Len: 0
2025/05/03 16:28:35 Received packet for established connection 10.0.0.1:56630-10.0.0.2:80 (Data handling not implemented)
--- Packet Received (40 bytes) ---
IPv4 Header (20 bytes):
Version: 4, IHL: 5 (20 bytes), TOS: 0x00
Total Length: 40, ID: 0x0000
Flags: [DF], Fragment Offset: 0
TTL: 64, Protocol: 6 (TCP)
Checksum: 0x26ce
Source IP: 10.0.0.1
Destination IP: 10.0.0.2
----------------------------------
2025/05/03 16:28:35 TCP RCV: 10.0.0.1:56630 -> 10.0.0.2:80 Seq: 397498100 Ack: 2313623660 Flags: [FIN,ACK] Win: 65535 Len: 0
2025/05/03 16:28:35 Received packet for established connection 10.0.0.1:56630-10.0.0.2:80 (Data handling not implemented)
--- Packet Received (40 bytes) ---
IPv4 Header (20 bytes):
Version: 4, IHL: 5 (20 bytes), TOS: 0x00
Total Length: 40, ID: 0x0000
Flags: [DF], Fragment Offset: 0
TTL: 64, Protocol: 6 (TCP)
Checksum: 0x26ce
Source IP: 10.0.0.1
Destination IP: 10.0.0.2
----------------------------------
2025/05/03 16:28:36 TCP RCV: 10.0.0.1:56630 -> 10.0.0.2:80 Seq: 397498100 Ack: 2313623660 Flags: [FIN,ACK] Win: 65535 Len: 0
2025/05/03 16:28:36 Received packet for established connection 10.0.0.1:56630-10.0.0.2:80 (Data handling not implemented)

PSH ができるように

FIN を処理できるように
❯ netstat -ant | grep "10.0.0.2.80"
tcp4 0 0 10.0.0.1.56916 10.0.0.2.80 ESTABLISHED
2025/05/03 16:34:18 TCP RCV: 10.0.0.1:56916 -> 10.0.0.2:80 Seq: 290068616 Ack: 246618297 Flags: [FIN,ACK] Win: 65535 Len: 0
2025/05/03 16:34:18 Received packet for established connection 10.0.0.1:56916-10.0.0.2:80
2025/05/03 16:34:18 Received FIN for connection 10.0.0.1:56916-10.0.0.2:80. Entering CLOSE_WAIT.
2025/05/03 16:34:18 TCP SEND: 10.0.0.2:80 -> 10.0.0.1:56916 Seq: 246618297 Ack: 290068617 Flags: [ACK] Len: 0
2025/05/03 16:34:18 Sending FIN for connection 10.0.0.1:56916-10.0.0.2:80. Entering LAST_ACK.
2025/05/03 16:34:18 TCP SEND: 10.0.0.2:80 -> 10.0.0.1:56916 Seq: 246618297 Ack: 290068617 Flags: [FIN,ACK] Len: 0
--- Packet Received (40 bytes) ---
IPv4 Header (20 bytes):
Version: 4, IHL: 5 (20 bytes), TOS: 0x00
Total Length: 40, ID: 0x0000
Flags: [DF], Fragment Offset: 0
TTL: 64, Protocol: 6 (TCP)
Checksum: 0x26ce
Source IP: 10.0.0.1
Destination IP: 10.0.0.2
----------------------------------
2025/05/03 16:34:18 TCP RCV: 10.0.0.1:56916 -> 10.0.0.2:80 Seq: 290068617 Ack: 246618298 Flags: [ACK] Win: 65535 Len: 0
2025/05/03 16:34:18 Handling ACK for FIN for connection 10.0.0.1:56916-10.0.0.2:80
2025/05/03 16:34:18 Connection 10.0.0.1:56916-10.0.0.2:80 CLOSED normally.

HTTPのリクエスト解析
HTTP の実装ですね! TCP 接続が確立した後に、その上でやり取りされるアプリケーション層のプロトコルです。
ip_parser
が TCP 接続を確立できるようになったので、その接続上で受信したデータを HTTP リクエストとして解釈し、基本的な情報を表示することは可能です。
ただし、HTTP もまた奥が深いプロトコルです。
- リクエストの解析 (メソッド、URI、バージョン、ヘッダー、ボディ)
- レスポンスの生成 (ステータスコード、ヘッダー、ボディ)
- Keep-Alive による接続の再利用
- Chunked Encoding
- HTTPS (TLS暗号化) ...など
これら全てを実装するのは大変です。
提案: Phase 4 - HTTP リクエスト解析 (基本)
まず、確立された TCP コネクション (ポート 80) で受信したデータペイロードを、単純な HTTP/1.1 リクエストとして解析し、そのリクエストライン (例: GET / HTTP/1.1
) とヘッダー情報をログに出力する ところまでを実装するのはいかがでしょうか?
実装方針:
-
handleTCPPacket
のESTABLISHED
ケースを修正:- データペイロード (
tcpPayload
) を受信した場合 (Echo ロジックの後、または Echo の代わりに)、そのペイロードを HTTP リクエストとして解析する関数 (parseHTTPRequest
) を呼び出します。
- データペイロード (
-
parseHTTPRequest
関数を作成:- 引数:
[]byte
(TCP ペイロード) -
bytes.Reader
やbufio.Scanner
を使ってペイロードを行単位で読み取ります。 - 最初の行 (リクエストライン) をパースし、メソッド、URI、HTTP バージョンを取得します。
- 続く行を
:
で分割し、ヘッダー名と値として解釈し、マップ (map[string]string
) に格納します。 - 空行 (
\r\n
) が現れたらヘッダーの終わりとみなします (ボディの解析は Phase 4 では省略)。 - パースしたメソッド、URI、バージョン、ヘッダーマップを返します。
- 引数:
-
ログ出力:
handleTCPPacket
内で、parseHTTPRequest
から返された情報をログに出力します。
このフェーズでのスコープ外:
- HTTP レスポンスの生成・送信
- HTTP リクエストボディの解析
- Keep-Alive, Chunked Encoding 等の高度な機能
これで、curl http://10.0.0.2
のようなコマンドを実行した際に、ip_parser
がどのような HTTP リクエストを受け取ったかを確認できるようになります。
このスコープで HTTP 解析の第一歩に進みますか?

理解のために
- TUN で仮想的なデバイスを作成する
- 自作プログラムがこのデバイスを watch
- データが来たら IP ヘッダをパース
- ヘッダの中身が IP or TCP or UDP で処理を変える
- TCP の場合
- TCP ヘッダを読み取り
- フラグに応じて処理を変える(handshake / disconnect
- PSH の場合は payload をパース
- HTTP の場合
- HTTP としてパース
- HTTP の場合

http parse に成功
2025/05/03 16:41:55 TCP RCV: 10.0.0.1:57275 -> 10.0.0.2:80 Seq: 3645979778 Ack: 2586382155 Flags: [PSH,ACK] Win: 65535 Len: 71
2025/05/03 16:41:55 Received packet for established connection 10.0.0.1:57275-10.0.0.2:80
2025/05/03 16:41:55 Received 71 bytes of data for 10.0.0.1:57275-10.0.0.2:80. Attempting HTTP parse.
2025/05/03 16:41:55 TCP SEND: 10.0.0.2:80 -> 10.0.0.1:57275 Seq: 2586382155 Ack: 3645979849 Flags: [ACK] Len: 0
2025/05/03 16:41:55 HTTP Request Parsed: [GET / HTTP/1.1] from 10.0.0.1:57275
2025/05/03 16:41:55 Header: Host: 10.0.0.2
2025/05/03 16:41:55 Header: User-Agent: curl/8.6.0
2025/05/03 16:41:55 Header: Accept: */*

http payload
❯ curl http://10.0.0.2
<html><body><h1>Hello from userspace TCP/IP!</h1></body></html>
とれるようになった!

いいえ、必ずしも「すべての TCP ペイロードが届いてから」ではありません。 HTTP 処理はもっと段階的です。
ここが TCP のストリーム性と HTTP のメッセージ性の違いを理解する上で非常に重要な点です。
実際の HTTP 処理の流れ:
-
ヘッダーの区切りを探す: TCP コネクションからデータを 少しずつ読み込みます。HTTP パーサーは、まず HTTP メッセージのヘッダー部分の終わりを示す「空行」(
\r\n\r\n
) を探します。データが足りなければ、さらに TCP ストリームから読み込みます。 - ヘッダーの解析: 空行が見つかった時点で、そこまでのデータが完全な HTTP ヘッダーであると判断できます。このヘッダー情報を解析します。
-
ボディの有無と長さを判断: ヘッダーの中にある
Content-Length
やTransfer-Encoding: chunked
といった情報を見て、このリクエスト (またはレスポンス) にボディがあるか、あるならどうやって読み取るべきかを判断します。 -
ボディの読み込み:
-
Content-Length: N
があれば、ヘッダーの直後から N バイト分のデータを TCP ストリームから読み込みます。この N バイトが複数の TCP セグメントに分割されて届く場合、パーサーは必要な N バイトが揃うまで TCP からの読み込みを続けます。 -
Transfer-Encoding: chunked
であれば、チャンクサイズ、チャンクデータ、次のチャンクサイズ...という形式で、最後の0\r\n\r\n
が来るまで読み込みを続けます。これも複数セグメントにまたがる可能性があります。 - ボディがない (GET リクエストなど) か、上記ヘッダーがなければ、ヘッダーの終わり (
\r\n\r\n
) でメッセージの終わりと判断します (HTTP/1.1 の Keep-Alive の場合など)。
-
つまり:
- HTTP ヘッダーは、ヘッダー終端マーカー (
\r\n\r\n
) が TCP ストリーム上で見つかるまで読み込まれます。 - HTTP ボディは、ヘッダーを解析した結果に基づいて、必要な長さまたは終端マーカーが見つかるまで TCP ストリームから読み込まれます。
現状のコードは、この「ストリームから少しずつ読み込んで区切りを探す」部分を単純化し、「最初に届いたデータセグメントの中にヘッダーの終わりがあるはず」と仮定してしまっている、ということです。
ご理解いただけたでしょうか?

それは HTTP/1.1 と HTTP/2 の根本的な違いに関する非常に良い質問ですね!
HTTP/1.1 では、基本的に 1 つの TCP コネクション上で一度に 1 つのリクエストとレスポンスの組しか処理できませんでした (パイプライン処理はありましたが、ヘッドオブラインブロッキング問題がありました)。
HTTP/2 では、ユーザーがおっしゃる通り、多重化 (Multiplexing) という仕組みを使って、単一の TCP コネクション上で複数のリクエストとレスポンスを並行してやり取りします。これを実現するために、HTTP/2 は以下の 2 つの重要な概念を導入しています。
-
フレーム (Frames):
- HTTP/2 では、HTTP リクエストやレスポンスといったメッセージを、より小さなバイナリ形式の単位である「フレーム」に分割します。
- フレームには様々な種類があります。例えば、HTTP ヘッダーを運ぶ
HEADERS
フレーム、リクエスト/レスポンスのボディ (データ) を運ぶDATA
フレーム、接続設定を行うSETTINGS
フレームなどがあります。
-
ストリーム (Streams):
- 各 HTTP リクエストとそれに対応するレスポンスの組は、**独立した双方向の仮想的な「ストリーム」**の上でやり取りされます。
- 各ストリームには**一意の「ストリーム ID」**が割り当てられます。クライアントが開始するストリームは奇数 ID、サーバーが開始するストリーム (サーバープッシュ用) は偶数 ID を持ちます。
区別の仕組み:
HTTP/2 のすべてのフレームには、それが**どのストリームに属するかを示す「ストリーム ID」**が含まれています。
TCP コネクション上では、異なるストリームに属するフレームが混ざり合って (インターリーブされて) 送受信されます。しかし、受信側は各フレームのヘッダーに含まれるストリーム ID を見ることで、どのフレームがどのリクエスト/レスポンス (つまり、どのストリーム) に属するかを正確に識別できます。
例えば、
- クライアントがリクエスト A (ストリーム ID 1) とリクエスト B (ストリーム ID 3) を送信。
- TCP コネクション上には、[ストリーム 1 の HEADERS フレーム] -> [ストリーム 3 の HEADERS フレーム] -> [ストリーム 1 の DATA フレーム] -> [ストリーム 3 の DATA フレーム] ... のように、異なるストリームのフレームが混在して流れる可能性があります。
- サーバーは、各フレームのストリーム ID (1 か 3 か) を見て、それぞれのフレームを対応するリクエストの処理に振り分け、レスポンスを組み立てて、同様にストリーム ID を付けたフレームで返信します。
これにより、1 つの TCP コネクションがブロックされることなく、複数のリクエスト/レスポンスを効率的に並行処理できるわけです。
まとめ:
HTTP/2 は、HTTP メッセージをストリーム ID 付きのバイナリフレームに分割し、それらを単一の TCP コネクション上で混在させて送受信することで、多重化を実現しています。受信側はフレームのストリーム ID を使って、どのフレームがどの通信に属するかを区別します。
これはテキストベースの HTTP/1.1 とは大きく異なる、より複雑な仕組みですね。今回のチャレンジで実装しているのは HTTP/1.1 の基本的な部分なので、HTTP/2 のフレーム処理はさらに高度なステップとなります。

いいえ、ストリーム ID は TCP ヘッダーには含まれていません。
ストリーム ID は、HTTP/2 フレームのヘッダーに含まれる情報です。
TCP はトランスポート層 (Layer 4) のプロトコルであり、その役割は指定されたポート間で信頼性のあるバイトストリームを提供することです。TCP ヘッダーには、送信元/宛先ポート番号、シーケンス番号、ACK 番号、フラグ、ウィンドウサイズ、チェックサムなどの情報は含まれますが、上位のアプリケーション層 (HTTP/2 など) が定義する「ストリーム」や「フレーム」といった概念は全く知りません。
HTTP/2 はアプリケーション層 (Layer 7) のプロトコルです。HTTP/2 は、TCP が提供するバイトストリームの上に、独自のフレーム構造を定義します。
階層構造のイメージ:
+-----------------------------------+
| Ethernet Frame |
| +-------------------------------+ |
| | IP Datagram | |
| | +---------------------------+ | |
| | | TCP Segment | | |
| | | +-----------------------+ | | |
| | | | TCP Payload | | | | <-- TCP はここまでを一つのバイト列として扱う
| | | | (これが HTTP/2 データ) | | | |
| | | | +-------------------+ | | | |
| | | | | HTTP/2 Frame | | | | | <-- HTTP/2 はここから下を定義
| | | | | +---------------+ | | | | |
| | | | | | Frame Header | | | | | |
| | | | | | (Stream ID 等) | | | | | |
| | | | | +---------------+ | | | | |
| | | | | | Frame Payload | | | | | |
| | | | | +---------------+ | | | | |
| | | | +-------------------+ | | | |
| | | | | HTTP/2 Frame | | | | |
| | | | | +---------------+ | | | | |
| | | | | | Frame Header | | | | | |
| | | | | | (Stream ID 等) | | | | | |
| | | | | +---------------+ | | | | |
| | | | | | Frame Payload | | | | | |
| | | | | +---------------+ | | | | |
| | | | +-------------------+ | | | |
| | | | ... | | | |
| | | +-----------------------+ | | |
| | +---------------------------+ | |
| +-------------------------------+ |
+-----------------------------------+
つまり、TCP が運んできたバイト列 (TCP ペイロード) の中身を HTTP/2 パーサーが見て、その先頭にある HTTP/2 フレームヘッダーを読み取り、そこに含まれるストリーム ID を解釈する、という流れになります。TCP 自身は、そのペイロードの中身が HTTP/2 フレームであることすら関知しません。

それも非常に良い質問です! クライアントとサーバーがどのバージョンの HTTP を使うかは、接続を開始する際のネゴシエーション (交渉) によって決まります。「HTTP/1.1 じゃないから HTTP/2 かな?」と後から判断するわけではありません。
主なネゴシエーション方法は 2 つあります。
-
HTTPS (TLS) での ALPN (Application-Layer Protocol Negotiation):
- これが現在最も一般的な方法です。主要なブラウザは HTTP/2 を HTTPS (つまり TLS 暗号化された接続) 上でのみサポートしています。
- TLS ハンドシェイク (暗号化通信を確立するための最初のやり取り) の過程で、ALPN という拡張機能が使われます。
-
クライアントは TLS の最初のメッセージ (
ClientHello
) で、「私はこれらのプロトコルを話せますよ」というリスト (例: "h2", "http/1.1"。"h2" は HTTP/2 を意味します) をサーバーに送ります。 -
サーバーはそのリストを見て、自分が対応できるプロトコルの中から一番優先したいもの (通常は "h2") を選び、「じゃあ、これで行きましょう」と TLS の応答メッセージ (
ServerHello
) でクライアントに伝えます。 - TLS ハンドシェイクが完了したら、両者はその合意したプロトコル (HTTP/2 または HTTP/1.1) で通信を開始します。
-
HTTP/1.1 の
Upgrade
ヘッダー (主に平文 HTTP 通信向け):- 暗号化されていない HTTP (
http://
, 通常ポート 80) で HTTP/2 を使いたい場合に使われる方法です (現在ではあまり使われません)。 -
クライアントは、まず HTTP/1.1 リクエスト を送ります。ただし、そのヘッダーに
Upgrade: h2c
(h2c
は平文の HTTP/2 の意) と、HTTP/2 の設定を含むHTTP2-Settings
ヘッダーを追加します。「できれば HTTP/2 にアップグレードしたいんだけど、設定はこんな感じでどう?」と提案するわけです。 -
サーバーが
h2c
をサポートしていれば、「OK、HTTP/2 に切り替えよう!」という意味で、ステータスコード101 Switching Protocols
とUpgrade: h2c
ヘッダーを含む応答を返します。この応答の後、同じ TCP コネクション上で HTTP/2 の通信が始まります。 - サーバーが
h2c
をサポートしていなければ、提案を無視して通常の HTTP/1.1 レスポンス (例: 200 OK) を返します。クライアントはそれを見て、HTTP/1.1 のまま通信を続けます。
- 暗号化されていない HTTP (
まとめ:
通常は、接続の最初の段階 (HTTPS なら TLS ハンドシェイク中、平文 HTTP なら最初の HTTP/1.1 リクエスト) で、クライアントとサーバーがどのバージョンの HTTP を使うか明示的に合意します。後から「試してみて判断する」わけではありません。HTTPS での ALPN が現在の主流です。

TLS
承知いたしました。HTTP/2 を実現するために必要な TLS の要素を、Go の標準 crypto/*
パッケージを部品として使いつつ、ハンドシェイクやレコード処理のロジックを自前で実装するという前提で、最低限のステップに分解してみます。
注意: これは非常に簡略化したリストであり、各ステップにはさらに多くの詳細な処理やエラーハンドリングが含まれます。また、これは典型的な TLS 1.2 の流れ (例: ECDHE + AES-GCM) を想定しています。
HTTP/2 のための最低限 TLS 実装ステップ (サーバー側視点)
Phase 0: 準備
-
サーバー証明書と秘密鍵: サーバー認証と鍵交換に必要な証明書ファイル (
.crt
) と秘密鍵ファイル (.key
) を用意します (テスト用には自己署名証明書でも可)。Go のcrypto/x509
やcrypto/rsa
,crypto/ecdsa
で扱える形式にします。 -
暗号プリミティブの理解:
crypto/rand
,crypto/sha256
,crypto/hmac
,crypto/aes
,crypto/cipher
(特に GCM),crypto/ecdhe
,crypto/rsa
などの基本的な使い方を確認しておきます。
Phase 1: ハンドシェイク開始 (Hello)
- TCP 接続待受 & 受信: クライアントからの TCP 接続を受け付けます。
-
ClientHello 受信 & 解析: 接続後、最初のデータとして TLS レコード (タイプ: Handshake) を受信し、中の ClientHello メッセージをバイナリ形式でパースします。
- 最低限、クライアントが提示する TLS バージョン候補、暗号スイート候補リスト、Client Random、そして拡張機能の中から ALPN (Application-Layer Protocol Negotiation) 拡張を探し、"h2" (HTTP/2) が含まれているか確認します。
-
パラメータ選択: サーバーがサポートする TLS バージョン (例: 1.2) と暗号スイート (例:
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
) を、クライアントの提示候補から選びます。ALPN で "h2" を選択します。 - ServerHello 生成 & 送信: 選択したバージョン、暗号スイート、生成した Server Random、そして ALPN で "h2" を選択したことを示す情報を盛り込んだ ServerHello メッセージをバイナリ形式で構築し、Handshake レコードに入れて送信します。
Phase 2: サーバー証明書と鍵交換
5. Certificate 生成 & 送信: 用意したサーバー証明書を Certificate メッセージとしてバイナリ形式で構築し、Handshake レコードに入れて送信します。
6. ServerKeyExchange 生成 & 送信 (ECDHE の場合):
* サーバー側の一時的な ECDHE 鍵ペアを生成します (crypto/ecdhe
)。
* 楕円曲線の種類、サーバーの一時公開鍵、そしてこれらのパラメータに対する署名 (サーバーの秘密鍵を使用、例: crypto/rsa.SignPKCS1v15
) を含んだ ServerKeyExchange メッセージをバイナリ形式で構築します。
* Handshake レコードに入れて送信します。
7. ServerHelloDone 生成 & 送信: サーバーからのハンドシェイクメッセージ送信完了を示す ServerHelloDone メッセージ (中身は空) を Handshake レコードに入れて送信します。
Phase 3: クライアントからの鍵交換と暗号化開始
8. ClientKeyExchange 受信 & 解析: クライアントからの ClientKeyExchange メッセージ (クライアントの一時公開鍵を含む) を Handshake レコードとして受信し、バイナリ形式でパースします。
9. 共通鍵 (PreMaster Secret -> Master Secret) の導出:
* 受信したクライアント一時公開鍵と、ステップ 6 で生成したサーバー一時秘密鍵を使って ECDHE 計算を行い、PreMaster Secret を導出します。
* Client Random, Server Random, PreMaster Secret を入力として、TLS の PRF (Pseudo-Random Function) (通常は HMAC-SHA256 をベースに実装) を使って Master Secret を計算します。
10. 暗号化パラメータの導出: Master Secret などから、さらに PRF を使って、実際の通信で使うセッションキー (例: AES-GCM 用のキーと Nonce の一部) をクライアント送信用・サーバー送信用にそれぞれ導出します。
11. ChangeCipherSpec 受信: クライアントから「これから暗号化します」という ChangeCipherSpec レコードを受信します。
12. クライアント Finished 受信 & 検証:
* クライアントから送られてくる最初の暗号化された Handshake レコード (Finished メッセージ) を受信します。
* 導出したサーバー受信用の鍵を使ってレコードを復号化し、中の Finished メッセージを取り出します。
* Finished メッセージの内容 (これまでのハンドシェイクメッセージ全体のハッシュ値) が正しいか、自前で計算したハッシュ値と比較して検証します。これが成功すれば、クライアント側の鍵導出と暗号化が正しく行われたことを確認できます。
Phase 4: サーバーからの暗号化開始と完了
13. ChangeCipherSpec 送信: サーバーも「これから暗号化します」という ChangeCipherSpec レコードを送信します。
14. サーバー Finished 生成 & 送信:
* サーバーも同様に、これまでのハンドシェイクメッセージ全体のハッシュ値を計算し、Finished メッセージをバイナリ形式で構築します。
* 導出したクライアント受信用の鍵を使って Finished メッセージを暗号化し、Handshake タイプの暗号化された TLS レコードとして送信します。
Phase 5: 暗号化通信 (HTTP/2 フレームの送受信)
15. ハンドシェイク完了: クライアントがサーバーからの Finished メッセージを検証できれば、TLS ハンドシェイクは成功です。
16. HTTP/2 フレームの暗号化/復号化: これ以降、HTTP/2 のフレームデータを、導出したセッションキーと合意した暗号アルゴリズム (例: AES-GCM) を使って暗号化し、Application Data タイプの TLS レコードに入れて送信します。受信した Application Data レコードは復号化して中の HTTP/2 フレームを取り出します。
これが HTTP/2 を動かすために最低限実装が必要となる TLS のステップです。見ての通り、「最低限」とはいえ、各ステップで正確なバイナリデータの処理、暗号計算、状態管理が必要となり、極めて複雑です。
このステップリストを見て、現実的なチャレンジとして進められそうか、改めてご判断いただけますでしょうか?

TLS ClientHello
2
----------------------------------
2025/05/03 20:39:14 TCP RCV: 10.0.0.1:60819 -> 10.0.0.2:443 Seq: 393456242 Ack: 2154312651 Flags: [PSH,ACK] Win: 65535 Len: 301
2025/05/03 20:39:14 TCP SEND: 10.0.0.2:443 -> 10.0.0.1:60819 Seq: 2154312651 Ack: 393456543 Flags: [ACK] Len: 0
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Entering handleTLSData with 301 bytes payload.
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Payload written to buffer. Buffer length now: 301. Calling handleTLSBufferedData.
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Entering handleTLSBufferedData. Buffer len: 301
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Loop start. Buffer len: 301
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Peeked header bytes: 1603010128
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Parsed Record Header: Type=22, Version=0x0301, Length=296
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Consuming full record (301 bytes) from buffer.
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Consumed record. Payload length: 296. Remaining buffer: 0
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Dispatching to handleTLSHandshakeRecord.
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Entering handleTLSHandshakeRecord. Payload len: 296
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Parsed Handshake Header: Type=1, Length=292
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Dispatching to handleClientHello.
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Entering handleClientHello. Message len: 292
2025/05/03 20:39:14 [TLS Debug] Entering parseClientHello. Message len: 292
2025/05/03 20:39:14 [TLS Debug] Parsed Version: 0x0303, Offset: 2
2025/05/03 20:39:14 [TLS Debug] Parsed Random (32 bytes), Offset: 34
2025/05/03 20:39:14 [TLS Debug] Parsed SessionIDLen: 32, Offset: 35
2025/05/03 20:39:14 [TLS Debug] Parsed SessionID (32 bytes), Offset: 67
2025/05/03 20:39:14 [TLS Debug] Parsed CipherSuitesLen: 98, Offset: 69
2025/05/03 20:39:14 [TLS Debug] Parsed 49 Cipher Suites, Offset: 167
2025/05/03 20:39:14 [TLS Debug] Parsed CompressionMethodsLen: 1, Offset: 168
2025/05/03 20:39:14 [TLS Debug] Skipped Compression Methods (1 bytes), Offset: 169
2025/05/03 20:39:14 [TLS Debug] Parsed ExtensionsLen: 121, Offset: 171
2025/05/03 20:39:14 [TLS Debug] Extensions data present (121 bytes), parsing TBD. Final Offset: 292
2025/05/03 20:39:14 [TLS Debug] Exiting parseClientHello successfully.
2025/05/03 20:39:14 [TLS Info - 10.0.0.1:60819-10.0.0.2:443] Parsed ClientHello:
2025/05/03 20:39:14 [TLS Info - 10.0.0.1:60819-10.0.0.2:443] Version: 0x0303
2025/05/03 20:39:14 [TLS Info - 10.0.0.1:60819-10.0.0.2:443] Random: ceca2c1cd4db4b51f5c8458ec0f16d4d8def3d8c4573aaf8ab461b63c8f19fbd
2025/05/03 20:39:14 [TLS Info - 10.0.0.1:60819-10.0.0.2:443] SessionID Length: 32
2025/05/03 20:39:14 [TLS Info - 10.0.0.1:60819-10.0.0.2:443] Cipher Suites Count: 49
2025/05/03 20:39:14 [TLS Info - 10.0.0.1:60819-10.0.0.2:443] Cipher Suites (first 5): [4867 4866 4865 52393 52392]
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Exiting handleClientHello.
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Exiting handleTLSHandshakeRecord.
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Exiting handleTLSBufferedData. Buffer len: 0
2025/05/03 20:39:14 [TLS Debug - 10.0.0.1:60819-10.0.0.2:443] Exiting handleTLSData.
--- Packet Received (40 bytes)

了解です。
Linux上でNICに届いたパケットがHTTPサーバ(ポート80)まで到達する一連の流れを、パケット受信からユーザ空間のアプリケーションまで、できる限り正確かつ詳細にステップバイステップで説明します。
⸻
🧭 前提
• Linuxカーネルを使用
• HTTPサーバはポート80でbind()済み
• TCPで接続(例: クライアントからのHTTPリクエスト)
• iptablesなど特殊なパケットフィルタは設定されていないと仮定
⸻
🧵 1. NICでパケット受信(L2)
1. NICがイーサネットフレームを受信(宛先MACは自分のNICのもの)。
2. NICはDMAを使ってフレームをカーネルのRXリングバッファに転送。
3. NICは割り込みを発生(またはNAPIでポーリング)。
4. カーネルのネットワークドライバが割り込み処理 or NAPIスケジューリング。
⸻
🌐 2. カーネルネットワークスタックに渡される(L3-L4)
L2(Ethernet)→ L3(IP)
5. netif_receive_skb()(もしくは napi_gro_receive())が呼ばれる。
6. eth_type_trans() でEtherTypeをチェック → IPv4と判定。
7. ip_rcv() に渡される(IPv4スタックに入る)。
L3(IP)→ L4(TCP)
8. ip_rcv() にてルーティング(ip_route_input_noref())実行。
9. IPヘッダ検査後、ip_local_deliver() → ip_local_deliver_finish() 呼び出し。
10. ip_local_deliver_finish() 内で inet_protos[tcp] のハンドラ = tcp_v4_rcv() 呼び出し。
⸻
🔢 3. TCPスタックによる処理(L4)
11. tcp_v4_rcv() はTCPヘッダをパース。
12. __inet_lookup_skb() で (src_ip, src_port, dst_ip, dst_port) に合致するソケットをハッシュテーブルから検索。
• ハッシュテーブルは inet_hashinfo を利用。
13. 該当ソケットが LISTEN 状態であれば tcp_v4_conn_request() にてSYNキューに入れる。
14. 3-way-handshakeが完了後、新しいソケット構造体(struct sock)が生成され、accept queueに入る。
⸻
🧑💻 4. ユーザ空間アプリケーションへの受け渡し
15. アプリケーションが accept() を呼ぶと、accept queue から新規接続を取り出す。
16. 以後、アプリはそのソケットFDに対して read() / recv() でHTTPリクエストを受信。
17. read() のたびに tcp_recvmsg() → skb_copy_datagram_msg() でスキバッファからデータがユーザ空間にコピーされる。
⸻
🔚 補足:重要な内部構造や関数
要素 説明
struct sk_buff パケットの中身を保持する主要構造体(すべてのレイヤを通過)
struct sock 各TCP接続を表す構造体(アプリのFDに対応)
inet_listen_hash, tcp_hashinfo ソケットの検索に使われるTCPソケットのハッシュテーブル
tcp_input.c tcp_v4_rcv() や tcp_rcv_established() など、TCPの受信処理の中核
NAPI ソフトIRQ負荷軽減のための仕組み、割り込み→ポーリングへの最適化
⸻
🔍 オプション:途中で介在する可能性のある機能
• iptables / nftables による PREROUTING, INPUT チェーンのフック
• netfilter hook (NF_INET_PRE_ROUTING, NF_INET_LOCAL_IN)
• cgroup-bpf による socket filter
• SELinux / AppArmor によるアクセス制御
• XDP(eXpress Data Path)による超低レベルでのフィルタ(スキップされることも)
⸻
📌 まとめ図(簡略)
[NIC]
↓ DMA + 割り込み
[ドライバ]
↓ netif_receive_skb()
[L2: Ethernet]
↓ eth_type_trans()
[L3: IPv4]
↓ ip_rcv() → ip_local_deliver()
[L4: TCP]
↓ tcp_v4_rcv() → ソケット検索 → accept queue
[ユーザ空間]
↓ accept() → read()/recv()
[HTTP Server]
⸻
さらに深掘りして、tcp_v4_rcv() 以降の tcp_state 遷移のコードや、socket buffer のメモリ管理まで見たいですか?

RXリングバッファ(Receive Ring Buffer)とは、NIC(ネットワークインターフェースカード)とOSカーネルの間でパケットを受け渡すためのメモリ領域です。簡単に言うと、NICが受信したパケットを一時的に置いておくFIFO型のリング構造のバッファです。
⸻
🔧 役割と仕組み
1. NICがパケットを受信すると、そのパケットをDMA(Direct Memory Access)でRXリングバッファに書き込みます。
2. OS(Linuxカーネル)は、NICが置いたパケットを順番に読み取って処理します。
3. 読み取りが終わると、そのスロットは再利用されます(リング状に循環)。
⸻
💡 リングバッファの「リング」とは?
• バッファの先頭と末尾がつながった構造(循環バッファ)
• 高速に先入れ先出し(FIFO)できる
• NICとカーネルが同時に使えるよう、ポインタ(head, tail)で管理
⸻
🧠 なぜ重要か?
• 受信パケットが高速すぎるとドロップが発生するため、バッファがなければ追いつけない
• バッファサイズ(デフォルトやチューニング)は性能に大きく影響
• ethtool -g eth0 などで確認・変更可能
⸻
もしイメージ付きで見たい場合は図解も出せます。可視化しますか?

TLS の ServerKeyExchange で苦労してる

dummy の証明書は curl が許さないので本物を使う

TLS の暗号化を実装する
承知いたしました。暗号化/復号化の実装に進むのは素晴らしい判断だと思います。より本格的な TLS スタックに近づきますね。
まずは、ここまでの「TLS 上での HTTP 処理」の流れを作ろうとした変更をコミットしましょう。
- 変更をステージングします。
git add day32_userspace_net/main.go
- 変更をコミットします。
-m
オプションでコミットメッセージを指定してください (例:git commit -m "feat(day32): Route Application Data to HTTP handler and send response via TLS record"
).
git commit -m "feat(day32): Route Application Data to HTTP handler and send response via TLS record"
コミットが完了しました。
それでは、TLS の暗号化/復号化の実装に進みましょう。これは複雑なプロセスになるため、段階的に進めるのが良いでしょう。
実装ステップの提案:
-
鍵導出の実装:
-
ClientKeyExchange
メッセージからクライアントの ECDHE 公開鍵を取得します。 - サーバーの ECDHE 秘密鍵とクライアントの ECDHE 公開鍵を使用して、Pre-Master Secret (PMS) を計算します (
ecdh.Curve.ECDH
)。 - PMS、Client Random、Server Random を使用して、TLS 1.2 の PRF (Pseudo-Random Function、HMAC-SHA256 ベース) を用いて Master Secret を導出します。
- Master Secret、Client Random、Server Random を使用して、PRF を用いて必要な鍵ブロック (Key Block) を生成します。
- Key Block から、クライアント/サーバーそれぞれの書き込み用 MAC キー、書き込み用暗号化キー、書き込み用 IV (Initialization Vector、GCM の場合は暗黙的) を抽出します。
- これらの鍵情報を
TCPConnection
構造体に保存するフィールドを追加します。
-
-
AEAD (AES-GCM) 暗号化/復号化の実装:
-
crypto/cipher
パッケージを使用して、導出した鍵と IV から AES-GCM の AEAD インスタンスを作成します (クライアント書き込み用とサーバー書き込み用の2つ)。 -
ChangeCipherSpec
を受信/送信したタイミングで、後続の TLS レコードの暗号化/復号化を有効にするフラグをTCPConnection
に追加します。 -
sendRawTLSRecord
を修正: 暗号化が有効な場合、ペイロードを AEAD で暗号化し、Nonce や Tag を含む完全な TLS レコードを構築して送信します。追加認証データ (AAD) としてシーケンス番号やレコードヘッダーを使用します。 -
handleTLSBufferedData
を修正: 暗号化が有効な場合、受信したレコードのペイロード部分を AEAD で復号化します。復号化に失敗した場合はエラーとして処理します。
-
-
Finished メッセージの検証/生成:
- ハンドシェイク中に送受信されたすべてのメッセージのハッシュ (Transcript Hash) を計算します。
- PRF を使用して、Master Secret と Transcript Hash からクライアント/サーバーそれぞれの Verify Data を生成します。
- クライアントから受信した (暗号化された) Finished メッセージを復号化し、Verify Data を比較して検証します。
- サーバー側の Finished メッセージを生成し、暗号化して送信します (
sendServerCCSAndFinished
内)。
まずはステップ1の「鍵導出の実装」から始めましょう。crypto/tls/prf.go
の pHash
や prf12
が参考になりますが、自前で HMAC-SHA256 を使って実装する必要があります。
準備ができたら指示してください。

TLS の実装結構苦しんでるなー、OIDC Provider の時の再来だ

openssl コマンドって https 通信できるのか
❯ echo | openssl s_client -connect 10.0.0.2:443 -tls1_2 -cipher ECDHE-RSA-AES128-GCM-SHA256 -debug -state -msg
CONNECTED(00000003)
write to 0x1256abbe0 [0x1256abb60] (106 bytes => 106 (0x6A))
0000 - 16 03 03 00 65 01 00 00-61 03 03 b7 7f 54 15 e4 ....e...a....T..
0010 - 01 62 a3 c7 86 e1 71 ca-ad 99 c4 f0 75 c1 ad 4a .b....q.....u..J
0020 - 58 87 e6 46 bf a9 69 08-f7 05 f9 00 00 04 c0 2f X..F..i......../
0030 - 00 ff 01 00 00 34 00 0b-00 02 01 00 00 0a 00 0a .....4..........
0040 - 00 08 00 1d 00 17 00 18-00 19 00 23 00 00 00 0d ...........#....
0050 - 00 18 00 16 08 06 06 01-06 03 08 05 05 01 05 03 ................
0060 - 08 04 04 01 04 03 02 01-02 03 ..........
>>> TLS 1.3 Handshake [length 0065], ClientHello
01 00 00 61 03 03 b7 7f 54 15 e4 01 62 a3 c7 86
e1 71 ca ad 99 c4 f0 75 c1 ad 4a 58 87 e6 46 bf
a9 69 08 f7 05 f9 00 00 04 c0 2f 00 ff 01 00 00
34 00 0b 00 02 01 00 00 0a 00 0a 00 08 00 1d 00
17 00 18 00 19 00 23 00 00 00 0d 00 18 00 16 08
06 06 01 06 03 08 05 05 01 05 03 08 04 04 01 04
03 02 01 02 03
read from 0x1256abbe0 [0x125688750] (5 bytes => 5 (0x5))
0000 - 16 03 03 00 2a ....*
read from 0x1256abbe0 [0x126908e15] (42 bytes => 42 (0x2A))
0000 - 02 00 00 26 03 03 89 51-f8 f5 9a 9d 46 31 00 8c ...&...Q....F1..
0010 - 17 94 16 d2 e7 97 a2 9c-95 24 3a 38 35 93 4d b1 .........$:85.M.
0020 - c4 0f eb 65 ca 5d 00 c0-2f ...e.]../
002a - <SPACES/NULS>
<<< TLS 1.3 Handshake [length 002a], ServerHello
02 00 00 26 03 03 89 51 f8 f5 9a 9d 46 31 00 8c
17 94 16 d2 e7 97 a2 9c 95 24 3a 38 35 93 4d b1
c4 0f eb 65 ca 5d 00 c0 2f 00
SSL_connect:SSLv3 read server hello A
read from 0x1256abbe0 [0x127817003] (5 bytes => 5 (0x5))
0000 - 05 00 30 13 31 ..0.1
write to 0x1256abbe0 [0x12780e800] (7 bytes => 7 (0x7))
0000 - 15 03 03 00 02 02 46 ......F
>>> TLS 1.2 Alert [length 0002], fatal protocol_version
02 46
SSL3 alert write:fatal:protocol version
SSL_connect:error in SSLv3 read server certificate A
8703101952:error:1400610B:SSL routines:CONNECT_CR_CERT:wrong version number:/AppleInternal/Library/BuildRoots/91a344b1-f985-11ee-b563-fe8bc7981bff/Library/Caches/com.apple.xbs/Sources/libressl/libressl-3.3/ssl/ssl_pkt.c:378:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 52 bytes and written 7 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
Protocol : TLSv1.2
Cipher : 0000
Session-ID:
Session-ID-ctx:
Master-Key:
Start Time: 1746278324
Timeout : 7200 (sec)
Verify return code: 0 (ok)
---

ここにきて独自実装したIP/TCPがぶっ壊れてるんじゃないかという疑い

--mode
オプションを追加して、切り分け中

TCP モードでの openssl s_client
接続テスト、ありがとうございます。ログを確認しました。
分析結果:
- ハンドシェイクの進捗: TCP モードでは、ClientHello から ServerHello、Certificate、ServerKeyExchange、ServerHelloDone まで正常に進み、クライアントからの ClientKeyExchange、ChangeCipherSpec、Finished も受信できています。これは大きな前進で、以前 TUN モードで発生していた「wrong version number」エラーが、自作 IP/TCP レイヤーに起因していた可能性が高いことを示唆しています。
-
サーバーからの CCS と Finished: サーバーは Client Finished を受信した後、期待通りに ChangeCipherSpec (
14 03 03 ...
) を送信しています。 -
問題点: 問題はサーバーが送信する Encrypted Finished メッセージ (
16 03 03 00 19 ...
) にあります。- TLS レコードヘッダー (
16 03 03 00 19
) が示すペイロード長は0x19
= 25 バイトです。 - しかし、AES-GCM で暗号化された Finished メッセージ(現在のダミー実装では Verify Data が 0 バイト)のペイロードは、
explicit_nonce (8バイト) + 暗号化されたVerify Data (0バイト) + 認証タグ (16バイト)
で 合計 24 バイトになるはずです。 - この 1 バイトのずれにより、クライアント (OpenSSL) は不正なレコードを受信したと判断し、
fatal illegal_parameter
アラート (02 2f
) を送信して接続を切断しています。
- TLS レコードヘッダー (
結論:
TCP モードへの切り替えにより、IP/TCP レイヤーの問題は解消された可能性が高いですが、TLS レイヤーの暗号化された Finished メッセージの構築(特に長さ計算)に新たな問題があることが判明しました。
次のステップ:
-
encryptRecord
関数とsendRawTLSRecord
関数を再確認し、特に Finished メッセージ (平文ペイロードが 0 バイト) を暗号化する際のペイロード (payloadToSend
) と最終的なレコード (finalRecord
) の長さ計算、およびレコードヘッダーに設定される長さ (0x0019
) が正しく 24 バイト (0x0018
) になるように修正します。 - サーバー側の Go プログラム実行時のログを確認し、
sendRawTLSRecord
で Finished メッセージを送信する際のログ出力(特に計算された長さと実際のヘッダーバイト)をopenssl s_client
のログと比較して、どこで不整合が発生しているか特定します。
まずは、encryptRecord
と sendRawTLSRecord
のコードを再確認し、長さ計算周りに誤りがないか見てみましょう。特に finalRecord
を構築する部分が怪しいです。
sendRawTLSRecord
の該当箇所を読み込みます。
Read file: day32_userspace_net/main.go
コードを確認しました。sendRawTLSRecord
内の暗号化パスにおける finalRecord
の構築ロジックは以下のようになっています。
// 2. Encrypt the payload
payloadToSend, err := encryptRecord(conn, plaintextPayload, outerRecordType, recordVersion)
if err != nil {
// ... error handling ...
}
// Log values just before building the final record header
log.Printf("[Send Raw Debug - %s] Building final record (Encrypted). OuterType: %d, OuterVersion: 0x%04x, PayloadLen: %d",
connKey, outerRecordType, recordVersion, len(payloadToSend)) // <-- Log PayloadLen
// 3. Build the final TLS record with the encrypted payload
finalRecord := make([]byte, TLSRecordHeaderLength+len(payloadToSend)) // Allocate based on PayloadLen
finalRecord[0] = outerRecordType
binary.BigEndian.PutUint16(finalRecord[1:3], recordVersion)
binary.BigEndian.PutUint16(finalRecord[3:5], uint16(len(payloadToSend))) // <-- Set header length based on PayloadLen
copy(finalRecord[TLSRecordHeaderLength:], payloadToSend)
encryptRecord
が返す payloadToSend
(8バイトnonce + 0バイト暗号文 + 16バイトタグ = 24バイト) の長さをそのまま使用して finalRecord
のサイズを確保し、レコードヘッダーの長さフィールドにもその値を設定しています。
コード上は正しく 24 バイト (0x18
) が長さとして設定されるように見えます。
考えられる原因:
-
encryptRecord
のバグ:encryptRecord
が、0バイト平文の場合に誤って 25 バイトのpayloadToSend
を返している可能性。 -
ログの誤解: サーバーログで Finished メッセージ送信時の
PayloadLen
が実際にいくつになっているかを確認する必要があります。もし 24 とログに出ていれば、sendRawTLSRecord
のヘッダー構築部分 (binary.BigEndian.PutUint16
) や送信部分に問題があるかもしれません。 -
Go の crypto/cipher の挙動: 標準ライブラリの
aead.Seal
が 0 バイト平文に対して予期しない長さ(例えばパディングを含むなど)を返す可能性(低いとは思いますが)。
次のアクション:
サーバーを実行 (go run main.go -mode tcp -port 8443
) し、再度 openssl s_client
で接続を試みてください。そして、サーバー側のコンソールに出力されるログのうち、Finished メッセージを送信する直前の以下のログを探して、その内容を教えていただけますでしょうか?
[Send Raw Debug - ...] Building final record (Encrypted). OuterType: 22, OuterVersion: 0x0303, PayloadLen: ??
[Send Raw Debug Bytes - ...] Final Encrypted Record Header Bytes: ?? ?? ?? ?? ??
特に PayloadLen:
の値と、Header Bytes:
の最後の2バイト(これがレコードヘッダーの長さフィールド)がいくつになっているかを確認したいです。これにより、問題が encryptRecord
にあるのか、sendRawTLSRecord
のヘッダー構築部分にあるのかを切り分けられます。

ログの分析、ありがとうございます。原因が特定できました!
分析:
- サーバー ChangeCipherSpec 暗号化ログ:
[Encrypt Debug - ...] aead.Seal result (ciphertextWithTag) Length: 17
- 平文 1 バイトに対し、暗号文+タグが 17 バイト。これは正しいです (1 + 16 = 17)。
- サーバー Finished 暗号化ログ:
-
[Send Raw Encrypt - ...] Encrypting record. Type: 22, Plaintext Len: 4, SeqNum: 0
-
ここが問題点です!
buildDummyFinishedMessage
が生成した Finished メッセージの平文ペイロード (record[TLSRecordHeaderLength:]
) が 4 バイトになっています。これはおそらく Finished ハンドシェイクヘッダー (Type=20, Length=0
) の 4 バイトそのものです。
-
ここが問題点です!
-
[Encrypt Debug - ...] aead.Seal result (ciphertextWithTag) Length: 20
- この 4 バイトの平文に対して
aead.Seal
を呼び出した結果、暗号文+タグの長さが 20 バイト (4 + 16 = 20) になっています。これはaead.Seal
の動作としては正しいです。
- この 4 バイトの平文に対して
-
[Encrypt - ...] Encrypted Payload Len: 28
- 最終的なペイロードは 28 バイト (8 バイト nonce + 20 バイト ciphertextWithTag)。これも計算としては正しいです。
-
[Send Raw Debug Bytes - ...] Final Encrypted Record Header Bytes: 160303001c
- 送信されるレコードヘッダーの長さも 28 (
0x1c
)。これも計算通りです。
- 送信されるレコードヘッダーの長さも 28 (
-
結論:
encryptRecord
や aead.Seal
、sendRawTLSRecord
のヘッダー構築には問題はありませんでした。根本的な原因は sendServerCCSAndFinished
関数が buildDummyFinishedMessage
から受け取ったペイロード (Finished ハンドシェイクメッセージ全体) をそのまま buildTLSRecord
に渡し、それが encryptRecord
に渡されていたことです。
Finished メッセージの構造は HandshakeHeader (4 bytes) + VerifyData (12 bytes)
です。暗号化すべきはレコード全体のペイロードではなく、ハンドシェイクメッセージの本体 (VerifyData
) です... が、TLS の仕様では、Finished メッセージはハンドシェイクメッセージとして構築された後、その全体が暗号化レコードのペイロードとなります。
したがって、問題は buildDummyFinishedMessage
が返すペイロードの長さが間違っていることです。Finished メッセージはハンドシェイクヘッダー (4バイト) + VerifyData (TLS 1.2 では通常 12 バイト) で構成されるべきです。現在の buildDummyFinishedMessage
は VerifyData を含まず、ヘッダー 4 バイトのみを返しているため、encryptRecord
に渡される平文が 4 バイトになってしまっています。
修正:
buildDummyFinishedMessage
を修正し、12 バイトのダミー VerifyData を含む、合計 16 バイトの Finished ハンドシェイクメッセージペイロードを生成するようにします。
buildDummyFinishedMessage
を修正しました。これで 12 バイトの VerifyData を含む 16 バイトの Finished メッセージペイロードが生成されるはずです。
これにより、encryptRecord
に渡される平文は 16 バイトとなり、
-
ciphertextWithTag
= 16 バイト暗号文 + 16 バイトタグ = 32 バイト -
encryptedPayload
= 8 バイト nonce + 32 バイト = 40 バイト - TLS レコードヘッダー長 =
0x0028
(40)
となるはずです。
再度サーバーを実行し、openssl s_client
で接続して、ハンドシェイクが最後まで成功するか確認してください。今度こそ illegal_parameter
アラートは出ずに、正常に完了するはずです(ダミーの Finished なので通信自体はその後失敗するかもしれませんが、ハンドシェイクは通るはず)。

やっと自作TLS越しに http できた!!
❯ curl -kv https://localhost:8443 --http1.1
* Host localhost:8443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8443...
* Connected to localhost (::1) port 8443
* ALPN: curl offers http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 / [blank] / UNDEF
* ALPN: server did not agree on a protocol. Uses default.
* Server certificate:
* subject: CN=10.0.0.2
* start date: May 3 12:00:18 2025 GMT
* expire date: May 3 12:00:18 2026 GMT
* issuer: CN=10.0.0.2
* SSL certificate verify result: self signed certificate (18), continuing anyway.
* using HTTP/1.x
> GET / HTTP/1.1
> Host: localhost:8443
> User-Agent: curl/8.6.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=utf-8
< Content-Length: 75
<
* Connection #0 to host localhost left intact
<html><body><h1>Hello from userspace HTTP/1.1! (Port 80)</h1></body></html>

きたー。TUN / IP / TCP / TLS / HTTP まで自作で動くようになった!!
❯ curl --cacert cert.pem --http1.1 https://10.0.0.2:443/
<html><body><h1>Hello from userspace HTTPS/1.1! (Port 443)</h1></body></html>

http2 をつくってる
引き継ぎ情報: Day32 ユーザースペースネットワークスタック (Go)
プロジェクト概要
Go言語を使用し、TUNデバイス経由のネットワークトラフィック(最終的にはHTTPS)を処理できるユーザースペースのネットワークスタック(IP/TCP/TLS 1.2/HTTP)を構築しています。
現在の状況
- IP/TCPレイヤー (TUNモード用) と TLS 1.2 ハンドシェイクおよび暗号化通信 (TCPモードで動作確認済み、TUNモードはシーケンス番号の問題でデバッグ中断中) の基本的な実装が完了しています。
- TLS ハンドシェイク中に ALPN (
h2
) のネゴシエーションに成功しました。 - ALPN で "h2" がネゴシエートされた場合に呼び出される
http.go
内のhandleHTTP2Data
関数の実装に着手しています。 -
重要な制約: ユーザの意向により、標準の
golang.org/x/net/http2
ライブラリは使用せず、HTTP/2 のフレーム処理をごく限定的に手動で実装します。目標は、単純な GET リクエストに対して固定のレスポンスを返すことです。
直近の課題と次のステップ
http.go
の handleHTTP2Data
関数内で、以下の処理を実装する必要があります。
-
クライアント接続プリフェイスの読み取りと検証: クライアントから送信される
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
というバイトシーケンスを確認します。これは TLS で暗号化された Application Data として受信されます。 -
サーバー SETTINGS フレームの手動構築と送信:
- 空の SETTINGS フレーム (Type=0x4, Flags=0x0, StreamID=0, Payload=[]) をバイト列として手動で構築します。HTTP/2 のフレームヘッダ (9バイト) を含みます。
- 構築したフレームを
buildTLSRecord
で Application Data レコードにラップし、sendRawTLSRecord
を使用してクライアントに送信します。
- フレーム処理ループの準備: クライアントからの後続フレーム (最低限 SETTINGS フレームと HEADERS フレーム) を読み取り、処理するための基本的なループ構造と、フレームのデコード/エンコードのためのヘルパー関数の雛形を用意します。
関連ファイル
-
http.go
:handleHTTP2Data
関数の実装場所。 -
tls.go
:handleTLSBufferedData
,buildTLSRecord
,sendRawTLSRecord
など、TLS レコード処理の関数が含まれます。 -
main.go
:TCPConnection
構造体の定義が含まれます。
注意点
- HTTP/2 フレームのエンコード/デコードは手動で行う必要があります (例:
encoding/binary
を使用)。 - TUNモードでのシーケンス番号の問題は未解決ですが、現在は HTTP/2 の実装にフォーカスしています。

HTTP/2 も成功した!
❯ curl -v --cacert cert.pem --http2 https://10.0.0.2:443/
* Trying 10.0.0.2:443...
* Connected to 10.0.0.2 (10.0.0.2) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: cert.pem
* CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
* subject: CN=10.0.0.2
* start date: May 3 12:00:18 2025 GMT
* expire date: May 3 12:00:18 2026 GMT
* common name: 10.0.0.2 (matched)
* issuer: CN=10.0.0.2
* SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://10.0.0.2:443/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: 10.0.0.2]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.6.0]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: 10.0.0.2
> User-Agent: curl/8.6.0
> Accept: */*
>
< HTTP/2 200
< content-type: text/plain
<
* Connection #0 to host 10.0.0.2 left intact
Hello from User-Space HTTP/2!

http/3 は桁違いに難易度が高いと言われ断念、見せる化の方に舵を切る

承知いたしました。「アイデア2: パケットインジェクション・テストツール」の方向で進めましょう。Day32 で構築したユーザースペースネットワークスタックの動作検証やデバッグに役立つツールになりそうですね。
以下にツールの仕様案を提案します。
Day 33: パケットインジェクション・テストツール
1. 目的
- Day32 で作成したユーザースペースネットワークスタック (以降、「対象スタック」) に対して、手動で定義したパケットシーケンスを送信 (インジェクト) する。
- 対象スタックの応答やログを観察し、プロトコル処理の正当性や堅牢性をテストする。
- 正常系シナリオ (例: TCPハンドシェイク、TLSハンドシェイク、HTTP/2リクエスト) および異常系シナリオ (例: 不正なフラグ、予期しないシーケンス番号、不正なフレーム) を実行可能にする。
2. システム構成
-
テストツール本体 (Go):
- パケット生成エンジン: gopacket ライブラリを利用し、IP, TCP, (簡略化された)TLSレコード, (簡略化された)HTTP/2フレームを含むバイト列を生成する。
- パケットインジェクション機能: 生成したパケットを指定された TUN デバイス (対象スタックが使用しているもの) に書き込む。
- シナリオローダー: JSON または YAML ファイルからテストシナリオ (送信パケットシーケンス) を読み込む。
- APIサーバー (オプション、Web UI を使う場合): Next.js フロントエンドからのリクエストに応じてシナリオ一覧の提供やシナリオ実行のトリガーとなる。
-
Web UI (Next.js) (推奨):
- 利用可能なテストシナリオの一覧表示。
- シナリオを選択し、実行をトリガーするボタン。
- シナリオ実行結果の簡単な表示 (成功/失敗、送信パケット数など)。
- 対象スタックのログ表示エリア (リアルタイム連携は必須とせず、ユーザーが別ターミナルで確認する前提でも可)。
-
対象スタック (Day32):
- 別途
sudo go run *.go --mode tun ...
で起動しておく。テストツールはこのプロセスが使用する TUN デバイスに対してパケットを送信する。
- 別途
3. 主要機能
-
パケット生成:
- IPヘッダ: 送信元/宛先 IP を指定可能。
- TCPヘッダ: 送信元/宛先ポート、シーケンス番号、ACK番号、各種フラグ (SYN, ACK, PSH, FIN, RST など)、ウィンドウサイズを指定可能。チェックサムは自動計算。
- TLSレコード (簡略版): レコードタイプ (Handshake, AppData など)、バージョン、ペイロードを指定して生成。暗号化は行わない (対象スタックの復号処理をテストするため)。
- HTTP/2フレーム (簡略版): フレームタイプ (SETTINGS, HEADERS, DATA など)、フラグ、ストリームID、ペイロードを指定して生成。
- 不正値設定: 上記フィールドに意図的に不正な値 (例: 存在しないフラグ、巨大な長さ) を設定する機能。
-
パケットインジェクション:
- 生成したパケット (
[]byte
) を、設定ファイルまたは UI で指定された TUN デバイス名に書き込む。 - シナリオに基づき、複数のパケットを順番に、指定された間隔 (オプション) で送信する。
- 生成したパケット (
-
シナリオ定義:
- JSON または YAML 形式でテストシナリオを記述。
- 各ステップで送信するパケットのパラメータ (IP, TCP, TLS, H2) を定義。
- シナリオ例:
-
tcp_handshake.yaml
: 正常な TCP 3ウェイハンドシェイク。 -
tls_client_hello.yaml
: TCP 接続後に ClientHello を送信。 -
http2_settings.yaml
: TLS 接続後に Client Preface と SETTINGS フレームを送信。 -
invalid_tcp_flags.yaml
: ESTABLISHED 状態で予期しない SYN パケットを送信。 -
large_tls_record.yaml
: 仕様を超える長さの TLS レコードを送信。
-
-
Web UI (推奨):
-
scenarios/
ディレクトリ内のシナリオファイル (.yaml or .json) を読み込み、一覧表示。 - シナリオ名をクリックすると詳細 (送信パケット概要など) を表示 (オプション)。
- 「実行」ボタンで選択したシナリオを Go バックエンドに指示。
- 実行結果 (Go バックエンドからの成否、エラーメッセージ) を表示。
- 対象スタックのログは、ユーザーが別途起動したターミナルで確認することを基本とする (UI へのリアルタイム表示は必須ではない)。
-
4. 技術スタック (推奨)
-
テストツール本体 (Go):
-
google/gopacket
: パケット生成/解析 -
songgao/water
: TUN デバイスへの書き込み -
net
,encoding/binary
: 標準ライブラリ -
gopkg.in/yaml.v3
orencoding/json
: シナリオファイルの読み込み - (Web UI の場合)
net/http
またはフレームワーク (Gin, Echo など): API サーバー
-
-
Web UI (Next.js):
- TypeScript
- Tailwind CSS
-
fetch
oraxios
: Go バックエンドとの通信
5. 実装ステップ案
-
プロジェクトセットアップ:
day33_packet_injector
ディレクトリ作成、Go モジュール初期化、Next.js プロジェクト作成。 - Go: 基本パケット生成: gopacket を使い、IP/TCP パケットを生成・TUN デバイスに書き込む基本機能実装。
- Go: シナリオ定義とローダー: シナリオファイルのフォーマット決定、YAML/JSON パーサー実装、基本シナリオ作成。
- Go: パケット生成拡張: TLSレコード、HTTP/2フレームの生成機能、不正値設定機能を追加。
- Go: シナリオ実行エンジン: シナリオファイルに基づきパケットシーケンスを生成・インジェクトする機能実装。
- (Web UI の場合) Go API: シナリオ一覧取得、シナリオ実行 API を実装。
- (Web UI の場合) Next.js UI: シナリオ一覧表示、実行ボタン、結果表示を実装。
- テスト: Day32 スタックを起動し、作成したツールで各種シナリオを実行、対象スタックのログを確認して挙動を検証。
6. デザイン
- Web UI はミニマリストまたは機能的なデザイントレンドを採用。Tailwind CSS でシンプルかつ分かりやすいインターフェースを目指す。
この仕様でいかがでしょうか? 特にパケット生成の詳細度や、UI の要否、シナリオフォーマットなど、ご希望があれば調整します。

↑はあきらめた

- 3way handshake まで
- TLS connection established まで
- http/2 通信完了まで

やった〜〜〜。
絶対1人ではできないandやらなかった
- IPスタック
- TCPスタック
- TLSスタック
- HTTP/1.1 と HTTP/2 スタック
を実装することに成功した

学び
- TLS のシーケンスが多すぎるし、ちょっとミスるだけで通信がなりたたない
- TLSは不要なもの(暗号化アルゴリズムが複数あるとか)を削ぎ落とさないとやばい
- OSSで作ってくれてるの本当に感謝
- TCP seq number の数え間違いで全ての通信が壊れるのトラップすぎる
- HTTP/1.1 に対して HTTP/2 の実装複雑すぎる
- TUN で仮想デバイス簡単に作れる、IP までは爆速だった
- TCP もまあちょっとづつ分解してやればシンプル
- IP スタックは TUN デバイスを polling してパケットを取得していた