atmos/kernel/net/tcp_state.rs
1// L4 (Transport) — TCP 受信側状態機械の「純粋ロジック」。
2//
3// ハードウェア(NIC)・グローバル状態(`TCP_SOCKETS`/`NET_LOCK`)・ログ出力に
4// 一切依存しない自己完結モジュールとして `tcp.rs` から切り出した
5// (`addr_util.rs` と同じ狙い)。これによりホスト側ユニットテスト
6// (`tests/`, `make test`)から `#[path]` で直接取り込み、
7// パケットロス・順序逆転・重複といった**異常系を含めて**検証できる。
8//
9// 呼び出し側 (`tcp::parse_tcp_ipv4_packet`) の責務:
10// 1. IPv4/TCP ヘッダから `Segment` を組み立てる
11// 2. `on_segment()` を呼ぶ
12// 3. 戻り値 `SegmentOutcome` に従って rx_buffer への追記と ACK 送信を行う
13// 状態(`RecvState`)の更新は `on_segment()` が直接行う。
14
15/// TCP フラグビット。
16pub const FLAG_FIN: u8 = 0x01;
17pub const FLAG_SYN: u8 = 0x02;
18pub const FLAG_RST: u8 = 0x04;
19pub const FLAG_PSH: u8 = 0x08;
20pub const FLAG_ACK: u8 = 0x10;
21
22#[derive(Copy, Clone, PartialEq, Eq, Debug)]
23pub enum TcpState {
24 Closed,
25 SynSent,
26 Established,
27 FinWait1,
28 FinWait2,
29 TimeWait,
30 CloseWait,
31 LastAck,
32}
33
34pub fn tcp_state_name(state: TcpState) -> &'static str {
35 match state {
36 TcpState::Closed => "CLOSED",
37 TcpState::SynSent => "SYN-SENT",
38 TcpState::Established => "ESTABLISHED",
39 TcpState::FinWait1 => "FIN-WAIT-1",
40 TcpState::FinWait2 => "FIN-WAIT-2",
41 TcpState::TimeWait => "TIME-WAIT",
42 TcpState::CloseWait => "CLOSE-WAIT",
43 TcpState::LastAck => "LAST-ACK",
44 }
45}
46
47/// 受信した 1 セグメントのうち、状態遷移の判断に必要な情報だけを抜き出したもの。
48#[derive(Copy, Clone, Debug)]
49pub struct Segment {
50 pub seq: u32,
51 pub ack: u32,
52 pub flags: u8,
53 pub payload_len: u32,
54}
55
56/// ソケットのうち、受信側状態機械が読み書きする部分だけを抜き出したもの。
57#[derive(Copy, Clone, PartialEq, Eq, Debug)]
58pub struct RecvState {
59 pub state: TcpState,
60 /// 次に自分が送信するときに使うシーケンス番号。
61 pub snd_nxt: u32,
62 /// 次に受信を期待するシーケンス番号(これ未満は受信済み)。
63 pub rcv_nxt: u32,
64 /// 相手からの FIN を「順序どおりに」受け取ったか。
65 /// これが true になると上位層は EOF と解釈するため、**データの取りこぼしが
66 /// ある状態で true にしてはならない**(レスポンス途中切れの直接原因になる)。
67 pub fin_received: bool,
68}
69
70/// `on_segment` の結果として呼び出し側が行うべき副作用。
71#[derive(Copy, Clone, PartialEq, Eq, Debug)]
72pub struct SegmentOutcome {
73 /// true なら、このセグメントのペイロードを rx_buffer へ追記してよい
74 /// (順序どおりに到着したデータ)。false ならペイロードは破棄する。
75 pub accept_payload: bool,
76 /// true なら ACK を送り返す。
77 pub send_ack: bool,
78 /// `accept_payload` のとき、ペイロード先頭から飛ばすバイト数
79 /// (既受信部分と重なった再送の、重なり部分。通常は 0)。
80 pub payload_skip: u32,
81}
82
83/// ソケットテーブルから空きスロットを 1 つ選ぶ(純粋ロジック)。
84///
85/// 実体の `TcpSocket` は `Vec` を含み `Copy` でないため配列ごと渡せない。
86/// 代わりに「長さ」と「その添字が使用中か」を返すクロージャだけを受け取ることで、
87/// グローバル状態から完全に切り離してテストできるようにしている。
88///
89/// 満杯時のエラー文字列は呼び出し側・テストの双方が依存するため定数化してある。
90pub const ERR_TABLE_FULL: &str = "tcp socket table full";
91
92pub fn find_free_slot(len: usize, is_occupied: impl Fn(usize) -> bool) -> Result<usize, &'static str> {
93 for i in 0..len {
94 if !is_occupied(i) {
95 return Ok(i);
96 }
97 }
98 Err(ERR_TABLE_FULL)
99}
100
101/// エフェメラルポートカウンタの次の値を、既存の生きたソケットと衝突しない
102/// ところまで進めて返す(純粋ロジック)。
103///
104/// 【2026-08-31】カウンタは 40000 起点で単調増加し、65535 到達で
105/// 40000 へラップアラウンドする。ラップアラウンド後、テーブルに残っている
106/// 生きたソケットと同じローカルポートを再び払い出すと、`tcp_find_socket`
107/// は `local_port + remote_ip + remote_port` の一致だけで判定するため、
108/// 別の生きた接続と誤って混線するリスクがある
109/// (`TCP_SOCKET_TABLE_SIZE` が小さくポート範囲が広いため実害の可能性は
110/// 低いが、理論上は起こりうる。`spec/TODO.md` 参照)。
111///
112/// `is_in_use` が真を返す間は候補を進め続ける。全ポートが使用中
113/// (テーブルが空いているのにこれが起きることは無いはずだが、
114/// 呼び出し側の不整合に備えて)なら `None` を返し、
115/// 呼び出し側は従来どおり衝突未確認のまま払い出すか、エラーにする。
116///
117/// 計算量: 最悪 O(ポート範囲) だが、通常は 0〜数回で衝突なしと判定できる。
118pub fn next_ephemeral_port(start: u16, is_in_use: impl Fn(u16) -> bool) -> Option<u16> {
119 const MIN: u16 = 40000;
120 let mut p = if start < MIN { MIN } else { start };
121 let first = p;
122 loop {
123 if !is_in_use(p) {
124 return Some(p);
125 }
126 p = if p == u16::MAX { MIN } else { p + 1 };
127 if p == first {
128 return None;
129 }
130 }
131}
132
133/// RFC 1982 準拠のシリアル番号比較: `a` が `b` より新しければ true。
134/// 単純な `a > b` では 2^32 の折り返し(wraparound)で誤判定するため必須。
135fn seq_gt(a: u32, b: u32) -> bool {
136 (a.wrapping_sub(b) as i32) > 0
137}
138
139/// 受信セグメント 1 個を状態機械へ適用する。
140pub fn on_segment(st: &mut RecvState, seg: &Segment) -> SegmentOutcome {
141 let mut out = SegmentOutcome {
142 accept_payload: false,
143 send_ack: false,
144 payload_skip: 0,
145 };
146
147 // --- RST: 状態を問わず即座に接続断 ---
148 // これを見落とすと、正当な接続拒否を受けても SYN 再送タイムアウトが
149 // 尽きるまで無駄に待ち続けることになる。
150 if seg.flags & FLAG_RST != 0 {
151 st.state = TcpState::Closed;
152 return out;
153 }
154
155 match st.state {
156 TcpState::SynSent => {
157 // SYN+ACK のみを受理する。SYN 単独(同時オープン)は本スタックでは非対応。
158 if seg.flags & (FLAG_SYN | FLAG_ACK) == (FLAG_SYN | FLAG_ACK) {
159 st.state = TcpState::Established;
160 st.rcv_nxt = seg.seq.wrapping_add(1);
161 st.snd_nxt = seg.ack;
162 out.send_ack = true;
163 }
164 }
165
166 TcpState::Established
167 | TcpState::FinWait1
168 | TcpState::FinWait2
169 | TcpState::CloseWait
170 | TcpState::LastAck => {
171 // --- ACK 処理 ---
172 // 【重要】以前は `snd_nxt = ack` と無条件に代入していたため、
173 // 遅延・重複した古い ACK が届くと snd_nxt が**巻き戻り**、
174 // 次の送信が既に送った番号を再利用して TCP ストリームを破壊して
175 // いた。snd_nxt は決して後退させない(前進のみ)。
176 if seg.flags & FLAG_ACK != 0 && seq_gt(seg.ack, st.snd_nxt) {
177 st.snd_nxt = seg.ack;
178 }
179
180 // --- ペイロード処理 ---
181 // 順序どおり(seq == rcv_nxt)のときのみ受理する。本スタックは
182 // 順序逆転セグメントの再組み立てバッファを持たないため、
183 // ギャップがある場合は破棄するしかない。
184 let in_order_data = seg.payload_len > 0 && seg.seq == st.rcv_nxt;
185 // 【2026-09-26 バグ修正】既受信部分と重なった再送
186 // (`seq < rcv_nxt < seq + len`)は、新しい後ろ側だけ受理する。
187 // 再送はまとめ直されて届くことがあり(QEMU user-net / Linux)、
188 // 従来は丸ごと捨てていたため欠落が永久に埋まらず、5.3MB のフォント
189 // 取得が末尾で 15 秒タイムアウトして途中切れになっていた。
190 let seg_end = seg.seq.wrapping_add(seg.payload_len);
191 let overlapping = seg.payload_len > 0
192 && seq_gt(st.rcv_nxt, seg.seq)
193 && seq_gt(seg_end, st.rcv_nxt);
194 if in_order_data {
195 st.rcv_nxt = st.rcv_nxt.wrapping_add(seg.payload_len);
196 out.accept_payload = true;
197 out.send_ack = true;
198 } else if overlapping {
199 out.payload_skip = st.rcv_nxt.wrapping_sub(seg.seq);
200 st.rcv_nxt = seg_end;
201 out.accept_payload = true;
202 out.send_ack = true;
203 } else if seg.payload_len > 0 {
204 // 【改善】順序逆転・重複データを黙って捨てるのではなく、
205 // 現在の rcv_nxt を載せた ACK(重複 ACK)を返す。相手はこれを
206 // 見て欠落セグメントを再送できる。以前は無反応だったため、
207 // 相手側の RTO が切れるまで回復の手がかりが無かった。
208 out.send_ack = true;
209 }
210
211 // --- FIN 処理 ---
212 // 【重要な修正】FIN はシーケンス空間上、ペイロードの直後の 1 番地を
213 // 占める。したがって FIN が「順序どおり」なのは
214 // seg.seq + payload_len == rcv_nxt(ペイロード処理後)
215 // のときだけである。
216 //
217 // 以前の実装は `seq == rcv_nxt || payload_len > 0` という条件で
218 // FIN を受理していたため、
219 // * ギャップがあってペイロードを破棄したセグメント(payload_len>0)
220 // * 順序逆転した純粋 FIN
221 // でも `fin_received = true` にしてしまっていた。上位層はこれを
222 // EOF と解釈するので、**まだ届いていないデータがあるのに受信完了と
223 // 誤認**し、レスポンスが途中で切れる(TLS 復号エラー・CSS の途中
224 // 切断などの直接原因)。順序どおりの FIN のみを受理する。
225 if seg.flags & FLAG_FIN != 0 {
226 let fin_seq = seg.seq.wrapping_add(seg.payload_len);
227 if fin_seq == st.rcv_nxt {
228 st.rcv_nxt = st.rcv_nxt.wrapping_add(1);
229 st.fin_received = true;
230 out.send_ack = true;
231
232 match st.state {
233 TcpState::Established => st.state = TcpState::CloseWait,
234 TcpState::FinWait1 => {
235 // 自分の FIN も同時に ACK されていれば TimeWait へ。
236 if seg.flags & FLAG_ACK != 0 && seg.ack == st.snd_nxt {
237 st.state = TcpState::TimeWait;
238 }
239 }
240 TcpState::FinWait2 => st.state = TcpState::TimeWait,
241 _ => {}
242 }
243 } else {
244 // 順序外の FIN。受理せず、重複 ACK だけ返して相手に
245 // 欠落分の再送を促す。fin_received は立てない。
246 out.send_ack = true;
247 }
248 }
249
250 // --- 自分が送った FIN に対する ACK ---
251 if st.state == TcpState::FinWait1
252 && seg.flags & FLAG_ACK != 0
253 && seg.ack == st.snd_nxt
254 && seg.flags & FLAG_FIN == 0
255 {
256 st.state = TcpState::FinWait2;
257 }
258 }
259
260 TcpState::Closed | TcpState::TimeWait => {
261 // 何もしない(クローズ済みソケットへの遅延パケット等)。
262 }
263 }
264
265 out
266}