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}
79
80/// ソケットテーブルから空きスロットを 1 つ選ぶ(純粋ロジック)。
81///
82/// 実体の `TcpSocket` は `Vec` を含み `Copy` でないため配列ごと渡せない。
83/// 代わりに「長さ」と「その添字が使用中か」を返すクロージャだけを受け取ることで、
84/// グローバル状態から完全に切り離してテストできるようにしている。
85///
86/// 満杯時のエラー文字列は呼び出し側・テストの双方が依存するため定数化してある。
87pub const ERR_TABLE_FULL: &str = "tcp socket table full";
88
89pub fn find_free_slot(len: usize, is_occupied: impl Fn(usize) -> bool) -> Result<usize, &'static str> {
90 for i in 0..len {
91 if !is_occupied(i) {
92 return Ok(i);
93 }
94 }
95 Err(ERR_TABLE_FULL)
96}
97
98/// エフェメラルポートカウンタの次の値を、既存の生きたソケットと衝突しない
99/// ところまで進めて返す(純粋ロジック)。
100///
101/// 【2026-08-31】カウンタは 40000 起点で単調増加し、65535 到達で
102/// 40000 へラップアラウンドする。ラップアラウンド後、テーブルに残っている
103/// 生きたソケットと同じローカルポートを再び払い出すと、`tcp_find_socket`
104/// は `local_port + remote_ip + remote_port` の一致だけで判定するため、
105/// 別の生きた接続と誤って混線するリスクがある
106/// (`TCP_SOCKET_TABLE_SIZE` が小さくポート範囲が広いため実害の可能性は
107/// 低いが、理論上は起こりうる。`spec/TODO.md` 参照)。
108///
109/// `is_in_use` が真を返す間は候補を進め続ける。全ポートが使用中
110/// (テーブルが空いているのにこれが起きることは無いはずだが、
111/// 呼び出し側の不整合に備えて)なら `None` を返し、
112/// 呼び出し側は従来どおり衝突未確認のまま払い出すか、エラーにする。
113///
114/// 計算量: 最悪 O(ポート範囲) だが、通常は 0〜数回で衝突なしと判定できる。
115pub fn next_ephemeral_port(start: u16, is_in_use: impl Fn(u16) -> bool) -> Option<u16> {
116 const MIN: u16 = 40000;
117 let mut p = if start < MIN { MIN } else { start };
118 let first = p;
119 loop {
120 if !is_in_use(p) {
121 return Some(p);
122 }
123 p = if p == u16::MAX { MIN } else { p + 1 };
124 if p == first {
125 return None;
126 }
127 }
128}
129
130/// RFC 1982 準拠のシリアル番号比較: `a` が `b` より新しければ true。
131/// 単純な `a > b` では 2^32 の折り返し(wraparound)で誤判定するため必須。
132fn seq_gt(a: u32, b: u32) -> bool {
133 (a.wrapping_sub(b) as i32) > 0
134}
135
136/// 受信セグメント 1 個を状態機械へ適用する。
137pub fn on_segment(st: &mut RecvState, seg: &Segment) -> SegmentOutcome {
138 let mut out = SegmentOutcome {
139 accept_payload: false,
140 send_ack: false,
141 };
142
143 // --- RST: 状態を問わず即座に接続断 ---
144 // これを見落とすと、正当な接続拒否を受けても SYN 再送タイムアウトが
145 // 尽きるまで無駄に待ち続けることになる。
146 if seg.flags & FLAG_RST != 0 {
147 st.state = TcpState::Closed;
148 return out;
149 }
150
151 match st.state {
152 TcpState::SynSent => {
153 // SYN+ACK のみを受理する。SYN 単独(同時オープン)は本スタックでは非対応。
154 if seg.flags & (FLAG_SYN | FLAG_ACK) == (FLAG_SYN | FLAG_ACK) {
155 st.state = TcpState::Established;
156 st.rcv_nxt = seg.seq.wrapping_add(1);
157 st.snd_nxt = seg.ack;
158 out.send_ack = true;
159 }
160 }
161
162 TcpState::Established
163 | TcpState::FinWait1
164 | TcpState::FinWait2
165 | TcpState::CloseWait
166 | TcpState::LastAck => {
167 // --- ACK 処理 ---
168 // 【重要】以前は `snd_nxt = ack` と無条件に代入していたため、
169 // 遅延・重複した古い ACK が届くと snd_nxt が**巻き戻り**、
170 // 次の送信が既に送った番号を再利用して TCP ストリームを破壊して
171 // いた。snd_nxt は決して後退させない(前進のみ)。
172 if seg.flags & FLAG_ACK != 0 && seq_gt(seg.ack, st.snd_nxt) {
173 st.snd_nxt = seg.ack;
174 }
175
176 // --- ペイロード処理 ---
177 // 順序どおり(seq == rcv_nxt)のときのみ受理する。本スタックは
178 // 順序逆転セグメントの再組み立てバッファを持たないため、
179 // ギャップがある場合は破棄するしかない。
180 let in_order_data = seg.payload_len > 0 && seg.seq == st.rcv_nxt;
181 if in_order_data {
182 st.rcv_nxt = st.rcv_nxt.wrapping_add(seg.payload_len);
183 out.accept_payload = true;
184 out.send_ack = true;
185 } else if seg.payload_len > 0 {
186 // 【改善】順序逆転・重複データを黙って捨てるのではなく、
187 // 現在の rcv_nxt を載せた ACK(重複 ACK)を返す。相手はこれを
188 // 見て欠落セグメントを再送できる。以前は無反応だったため、
189 // 相手側の RTO が切れるまで回復の手がかりが無かった。
190 out.send_ack = true;
191 }
192
193 // --- FIN 処理 ---
194 // 【重要な修正】FIN はシーケンス空間上、ペイロードの直後の 1 番地を
195 // 占める。したがって FIN が「順序どおり」なのは
196 // seg.seq + payload_len == rcv_nxt(ペイロード処理後)
197 // のときだけである。
198 //
199 // 以前の実装は `seq == rcv_nxt || payload_len > 0` という条件で
200 // FIN を受理していたため、
201 // * ギャップがあってペイロードを破棄したセグメント(payload_len>0)
202 // * 順序逆転した純粋 FIN
203 // でも `fin_received = true` にしてしまっていた。上位層はこれを
204 // EOF と解釈するので、**まだ届いていないデータがあるのに受信完了と
205 // 誤認**し、レスポンスが途中で切れる(TLS 復号エラー・CSS の途中
206 // 切断などの直接原因)。順序どおりの FIN のみを受理する。
207 if seg.flags & FLAG_FIN != 0 {
208 let fin_seq = seg.seq.wrapping_add(seg.payload_len);
209 if fin_seq == st.rcv_nxt {
210 st.rcv_nxt = st.rcv_nxt.wrapping_add(1);
211 st.fin_received = true;
212 out.send_ack = true;
213
214 match st.state {
215 TcpState::Established => st.state = TcpState::CloseWait,
216 TcpState::FinWait1 => {
217 // 自分の FIN も同時に ACK されていれば TimeWait へ。
218 if seg.flags & FLAG_ACK != 0 && seg.ack == st.snd_nxt {
219 st.state = TcpState::TimeWait;
220 }
221 }
222 TcpState::FinWait2 => st.state = TcpState::TimeWait,
223 _ => {}
224 }
225 } else {
226 // 順序外の FIN。受理せず、重複 ACK だけ返して相手に
227 // 欠落分の再送を促す。fin_received は立てない。
228 out.send_ack = true;
229 }
230 }
231
232 // --- 自分が送った FIN に対する ACK ---
233 if st.state == TcpState::FinWait1
234 && seg.flags & FLAG_ACK != 0
235 && seg.ack == st.snd_nxt
236 && seg.flags & FLAG_FIN == 0
237 {
238 st.state = TcpState::FinWait2;
239 }
240 }
241
242 TcpState::Closed | TcpState::TimeWait => {
243 // 何もしない(クローズ済みソケットへの遅延パケット等)。
244 }
245 }
246
247 out
248}