Skip to main content

atmos/kernel/net/
mod.rs

1//! TCP/IPネットワークスタック。
2//!
3//! Link 層(イーサネット)から Application 層(DNS/HTTP)までを自前実装する。
4//! 各層の設計方針・API境界の事前条件検証は `spec/DESIGN.md` §2「ネットワークスタック
5//! アーキテクチャ」を参照。カーネル外部(アプリ)との境界は [`crate::kernel::net_stack`]
6//! の `TcpIpStack::web_get`/`web_post`/`web_get_binary` に一本化されている。
7//!
8//! - [`arp`]/[`arp_cache`]: IP→MAC 解決
9//! - [`dhcp`]: DHCP クライアント
10//! - [`dns`]: UDP 経由の名前解決(ホスト名自動解決フォールバック)
11//! - [`icmp`]: Ping 等の診断
12//! - [`tcp`]/[`tcp_state`]: コネクション指向・スライディングウィンドウ
13//! - [`udp`]/[`udp_queue`]: コネクションレス転送
14//! - [`http`]: HTTP クライアント(チャンク転送対応)
15//! - [`eth_frame`]/[`tx_queue`]/[`reassembly`]: フレーム送受信・再構成
16use alloc::string::String;
17use alloc::vec::Vec;
18
19use crate::kernel::reclock::RecoverableMutex;
20
21/// 【2026-07-25診断用】同一ブート内でのHTTPS接続の通し番号。「ブート後
22/// 最初の接続(メインHTML取得)は成功しやすいが、直後に連続する2件目
23/// 以降(CSS/フォント取得)は失敗しやすい」という今回のセッションで
24/// 繰り返し観測された現象が、接続の「順序」自体に起因するのか、単なる
25/// ホスト依存かを定量的に切り分けるための通し番号カウンタ。
26static HTTPS_ATTEMPT_ORDINAL: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
27
28pub fn next_https_attempt_ordinal() -> u32 {
29    HTTPS_ATTEMPT_ORDINAL.fetch_add(1, core::sync::atomic::Ordering::Relaxed)
30}
31
32// 【2026-07-25 ホストユニットテスト対応】各サブモジュールを private から
33// `pub(crate)` へ変更した。これは `atmos`(本体バイナリ)の外部からの見え方には
34// 一切影響しない(本体は `[[bin]]` で外部クレートから参照されることが無いため)。
35// 一方 `arm/v1/tests/`(別クレート `atmos_unit_tests`)は `#[path]` でこの
36// `mod.rs` を丸ごと自分のクレート内へ取り込んで実体を検証するため、
37// `arp`/`icmp`/`tcp`/`udp`/`dns` 等がそのクレート内から到達可能である必要がある
38// (`pub(crate)` は「取り込んだ側のクレート内」に対して有効なので、これで届く)。
39pub(crate) mod addr_util;
40pub(crate) mod arp;
41pub(crate) mod arp_cache;
42pub(crate) mod dhcp;
43pub(crate) mod dns;
44pub(crate) mod http;
45pub(crate) mod icmp;
46pub(crate) mod tcp;
47pub(crate) mod tcp_state;
48pub(crate) mod udp;
49pub(crate) mod eth_frame;
50pub(crate) mod tx_queue;
51pub(crate) mod reassembly;
52pub(crate) mod udp_queue;
53
54pub use addr_util::{
55    format_ipv4, format_mac, parse_host_port, parse_host_port_with_default, parse_ipv4, parse_mac,
56};
57pub(crate) use addr_util::{internet_checksum, ipv4_header_checksum};
58
59// ─── Re-exports: public API ──────────────────────────────────────────────────
60pub use arp::{
61    arp_cache_lines, build_arp_request, get_next_hop_ip, ingest_ethernet_frame,
62    ingest_nic_ethernet_frame, ingest_nic_ipv4_packet,
63    nic_pop_tx_ipv4_packet, nic_take_tx_ipv4_chunk, transmit_nic_ethernet_frame,
64    transmit_nic_ipv4_packet,
65};
66pub use dhcp::{
67    dhcp_acquire, dhcp_release, dhcp_start_async, dhcp_status_line, dhcp_tick,
68    set_dhcp_auto_pending,
69};
70pub use dns::{
71    build_dns_query, dns_query_a_via_udp, ntp_query, ntp_sync, parse_dns_a_response,
72};
73pub use http::{
74    extract_http_body_binary, http_get_binary, http_get_real, http_get_real_with_referer, http_post_real, http_request_real,
75    https_get_real, parse_http_response, parse_http_response_bytes, parse_http_response_text,
76};
77pub use icmp::{
78    build_icmp_echo_request, parse_icmp_echo_reply_ipv4, ping_once_real,
79};
80pub use tcp::{
81    tcp_close_real, tcp_connect_real, tcp_current_snd_seq, tcp_is_socket_closed,
82    tcp_acked_through, tcp_recv_peek_nonempty, tcp_recv_real, tcp_retransmit_real, tcp_send_real,
83    tcp_socket_lines,
84};
85pub use udp::{
86    send_udp_ipv4, udp_queue_lines, udp_recv_next_datagram,
87    udp_recv_next_line,
88};
89
90// ─── Lock ────────────────────────────────────────────────────────────────────
91pub(crate) static NET_LOCK: RecoverableMutex<()> = RecoverableMutex::new(());
92
93pub fn force_release_locks(cid: i16) {
94    unsafe {
95        NET_LOCK.force_release_if_core(cid);
96    }
97}
98
99// ─── Structs ─────────────────────────────────────────────────────────────────
100#[derive(Copy, Clone)]
101pub struct NetworkState {
102    pub link_up: bool,
103    pub nic_backend: NicBackend,
104    pub mac: [u8; 6],
105    pub ip: [u8; 4],
106    pub gateway: [u8; 4],
107    pub dns: [u8; 4],
108    pub dns2: [u8; 4],
109    pub usb_eth_present: bool,
110    pub usb_eth_addr: u8,
111    pub usb_eth_bulk_in: u8,
112    pub usb_eth_bulk_out: u8,
113    pub usb_eth_mps: u16,
114    pub usb_eth_is_rndis: bool,
115    pub tx_packets: u64,
116    pub rx_packets: u64,
117    /// USB から受け取った生の Ethernet フレーム数(型・宛先による選別より前、無条件でカウント)。
118    /// `rx_packets` は ARP/DNS/UDP 受理時など個別プロトコルの成功パスでのみ増えるため、
119    /// 「そもそも NIC に何かが届いているか」を切り分けるための診断用カウンタ。
120    pub raw_rx_frames: u64,
121    pub dhcp_active: bool,
122    pub dhcp_server: [u8; 4],
123    pub dhcp_lease_secs: u32,
124    pub dhcp_subnet: [u8; 4],
125    pub dhcp_t1_secs: u32,
126    pub dhcp_t2_secs: u32,
127}
128
129#[derive(Copy, Clone)]
130pub enum NicBackend {
131    SoftwareLoopback,
132    UsbEthernet,
133}
134
135// ARP キャッシュのエントリ型と表操作は、ホスト側ユニットテストで異常系
136// (満杯時の LRU 退避、否定キャッシュの期限切れ)まで検証できるよう純粋モジュール
137// `arp_cache.rs` へ移設した。ここでは再輸出とグローバル領域の確保のみ行う。
138pub use arp_cache::ArpEntry;
139
140#[derive(Copy, Clone)]
141pub struct PingReply {
142    pub src_ip: [u8; 4],
143    pub identifier: u16,
144    pub sequence: u16,
145    pub payload_len: usize,
146}
147
148const ARP_CACHE_SIZE: usize = 16;
149static mut ARP_CACHE: [ArpEntry; ARP_CACHE_SIZE] = [ArpEntry::empty(); ARP_CACHE_SIZE];
150static mut ARP_TICK: u32 = 1;
151/// ARP 解決の否定キャッシュ(直近に解決失敗した宛先1件のみ)。DNS の複数回リトライ
152/// (3回 × 複数サーバ) が同じ到達不能な next-hop に対して毎回 2秒のブロッキング
153/// ARP 再試行を払うのを防ぐ。ゲートウェイ/DNSサーバが応答しない環境では、これが
154/// 無いと「DNSがタイムアウトするまでに十数秒かかる」原因になる。
155static mut ARP_NEG_IP: [u8; 4] = [0; 4];
156static mut ARP_NEG_UNTIL_MS: u64 = 0;
157static mut PING_SEQ: u16 = 1;
158/// DNS トランザクション ID。複数スレッド(browser_loader / img_load)が同時に
159/// DNS を引くため、非アトミックだと採番が衝突して応答照合を誤る。アトミック化。
160static DNS_TXID: core::sync::atomic::AtomicU16 = core::sync::atomic::AtomicU16::new(0x1200);
161/// DNS クエリの送信元ポート。クエリ毎にローテーションして並行クエリ間の
162/// 応答取り違えを防ぐ(固定ポートだと複数スレッドの DNS が同一ポートを共有し、
163/// txid 不一致で応答を読み捨ててタイムアウトしていた)。アトミック化でレース解消。
164static DNS_SRC_PORT: core::sync::atomic::AtomicU16 =
165    core::sync::atomic::AtomicU16::new(DNS_SRC_PORT_BASE);
166pub(super) const DNS_SRC_PORT_BASE: u16 = 53000;
167pub(super) const DNS_SRC_PORT_RANGE: u16 = 512; // 53000..=53511
168
169/// DNS 解決結果キャッシュ(host→IP, TTL付き)。画像が多いページで同一ホストを
170/// 何度も引く無駄をなくし、UDP RX キューの競合を激減させる。
171const DNS_CACHE_SIZE: usize = 32;
172const DNS_CACHE_NAME_MAX: usize = 64;
173const DNS_CACHE_TTL_MS: u64 = 60_000; // 60 秒
174
175#[derive(Copy, Clone)]
176struct DnsCacheEntry {
177    valid: bool,
178    name: [u8; DNS_CACHE_NAME_MAX],
179    name_len: usize,
180    ip: [u8; 4],
181    expires_ms: u64,
182}
183
184impl DnsCacheEntry {
185    const fn empty() -> Self {
186        Self {
187            valid: false,
188            name: [0; DNS_CACHE_NAME_MAX],
189            name_len: 0,
190            ip: [0; 4],
191            expires_ms: 0,
192        }
193    }
194}
195
196static mut DNS_CACHE: [DnsCacheEntry; DNS_CACHE_SIZE] = [DnsCacheEntry::empty(); DNS_CACHE_SIZE];
197
198/// クエリ毎に DNS 送信元ポートを 1 つ進めて返す(53000..=53511 を循環)。アトミック。
199pub(crate) fn next_dns_src_port() -> u16 {
200    use core::sync::atomic::Ordering;
201    // fetch_add でアトミックに採番。範囲外になったら base へ巻き戻す。
202    let raw = DNS_SRC_PORT.fetch_add(1, Ordering::Relaxed);
203    if !(DNS_SRC_PORT_BASE..DNS_SRC_PORT_BASE + DNS_SRC_PORT_RANGE).contains(&raw) {
204        DNS_SRC_PORT.store(DNS_SRC_PORT_BASE + 1, Ordering::Relaxed);
205        return DNS_SRC_PORT_BASE;
206    }
207    raw
208}
209
210/// クエリ毎に DNS トランザクション ID を 1 つ進めて返す。アトミック。
211pub(crate) fn next_dns_txid() -> u16 {
212    DNS_TXID.fetch_add(1, core::sync::atomic::Ordering::Relaxed)
213}
214
215/// DNS キャッシュを検索。期限内の有効エントリがあれば IP を返す。
216pub(crate) fn dns_cache_lookup(name: &str) -> Option<[u8; 4]> {
217    let nb = name.as_bytes();
218    if nb.len() > DNS_CACHE_NAME_MAX {
219        return None;
220    }
221    let now = crate::kernel::timer::get_system_time_ms();
222    unsafe {
223        for e in DNS_CACHE.iter() {
224            if e.valid
225                && e.name_len == nb.len()
226                && &e.name[..e.name_len] == nb
227                && now < e.expires_ms
228            {
229                return Some(e.ip);
230            }
231        }
232    }
233    None
234}
235
236/// DNS キャッシュへ登録(既存の同名/期限切れ/最古エントリを上書き)。
237pub(crate) fn dns_cache_insert(name: &str, ip: [u8; 4]) {
238    let nb = name.as_bytes();
239    if nb.is_empty() || nb.len() > DNS_CACHE_NAME_MAX {
240        return;
241    }
242    let now = crate::kernel::timer::get_system_time_ms();
243    let expires_ms = now.saturating_add(DNS_CACHE_TTL_MS);
244    unsafe {
245        // 1) 同名があれば更新。2) 無効/期限切れスロットを再利用。3) 無ければ最古を退避。
246        let mut free_idx: Option<usize> = None;
247        let mut oldest_idx = 0usize;
248        let mut oldest_exp = u64::MAX;
249        for (i, e) in DNS_CACHE.iter().enumerate() {
250            if e.valid && e.name_len == nb.len() && &e.name[..e.name_len] == nb {
251                free_idx = Some(i);
252                break;
253            }
254            if free_idx.is_none() && (!e.valid || now >= e.expires_ms) {
255                free_idx = Some(i);
256            }
257            if e.expires_ms < oldest_exp {
258                oldest_exp = e.expires_ms;
259                oldest_idx = i;
260            }
261        }
262        let idx = free_idx.unwrap_or(oldest_idx);
263        let mut entry = DnsCacheEntry::empty();
264        entry.valid = true;
265        entry.name[..nb.len()].copy_from_slice(nb);
266        entry.name_len = nb.len();
267        entry.ip = ip;
268        entry.expires_ms = expires_ms;
269        DNS_CACHE[idx] = entry;
270    }
271}
272
273const UDP_RX_QUEUE_SIZE: usize = 32;
274// 【2026-07-24】8だと、実サイト1ページの読み込みで発生しうる連続接続数
275// (メインHTML1 + 外部CSS最大3 + `@font-face`のフォントファイル最大4〜8等)
276// に対してぎりぎり、または不足気味だったため32へ拡大。仮に何らかの経路で
277// ソケットスロットが一時的に解放し損ねても、より多くの接続が正常に処理
278// できる余裕を持たせる(`tcp_close_real`の永久リークバグは既に修正済みだが、
279// 念のための安全マージンとして拡大)。
280const TCP_SOCKET_TABLE_SIZE: usize = 32;
281/// 【2026-07-30】NIC 送信キューの長さ。
282///
283/// 8 では実測で **2192 パケットを取りこぼしていた**
284/// (`nic tx queue FULL (size=8) dropped_total=2192`)。
285/// 3.7MB の画像取得のように連続送信が続くと USB の送出が追いつかず溢れる。
286/// 捨てられた送信は上位から「送ったのに応答が来ない」に見え、
287/// DNS/接続タイムアウトとして現れる(上層では原因が分からない)。
288/// 判定ロジックは `net/tx_queue.rs`(単体試験 10 件)。
289const NIC_TX_QUEUE_SIZE: usize = 64;
290const NIC_TX_MAX_PAYLOAD: usize = 1536;
291
292// UDP 受信キューのエントリ型と表操作は、満杯時の LRU 退避などの異常系を
293// ホスト側ユニットテストで検証できるよう純粋モジュール `udp_queue.rs` へ
294// 移設した。ここでは再輸出とグローバル領域の確保のみ行う。
295pub(crate) use udp_queue::UdpRxEntry;
296
297static mut UDP_RX_QUEUE: [UdpRxEntry; UDP_RX_QUEUE_SIZE] = [UdpRxEntry::empty(); UDP_RX_QUEUE_SIZE];
298static mut UDP_RX_TICK: u32 = 1;
299
300#[derive(Copy, Clone)]
301struct NicTxEntry {
302    valid: bool,
303    len: usize,
304    offset: usize,
305    payload: [u8; NIC_TX_MAX_PAYLOAD],
306    age: u32,
307}
308
309impl NicTxEntry {
310    const fn empty() -> Self {
311        Self {
312            valid: false,
313            len: 0,
314            offset: 0,
315            payload: [0; NIC_TX_MAX_PAYLOAD],
316            age: 0,
317        }
318    }
319}
320
321static mut NIC_TX_QUEUE: [NicTxEntry; NIC_TX_QUEUE_SIZE] = [NicTxEntry::empty(); NIC_TX_QUEUE_SIZE];
322/// 【2026-07-26診断】送信キュー枯渇で捨てたパケットの累計。
323/// 送信失敗は呼び出し側の大半が `let _ =` で戻り値を捨てるため不可視だった。
324static mut NIC_TX_DROPS: u32 = 0;
325static mut NIC_TX_TICK: u32 = 1;
326
327// TCP の状態そのものと受信側状態機械は、ホスト側ユニットテストから検証できる
328// よう純粋モジュール `tcp_state.rs` へ移設した。ここでは再輸出のみ行う。
329use tcp_state::TcpState;
330
331#[derive(Clone)]
332struct TcpSocket {
333    valid: bool,
334    local_port: u16,
335    remote_ip: [u8; 4],
336    remote_port: u16,
337    state: TcpState,
338    snd_nxt: u32,
339    /// 相手が受け取り済み(ACK 済み)の位置。送信後の待ちに使う(2026-10-05)。
340    snd_una: u32,
341    rcv_nxt: u32,
342    rx_buffer: Vec<u8>,
343    fin_received: bool,
344}
345
346impl TcpSocket {
347    const fn empty() -> Self {
348        Self {
349            valid: false,
350            local_port: 0,
351            remote_ip: [0; 4],
352            remote_port: 0,
353            state: TcpState::Closed,
354            snd_nxt: 0,
355            snd_una: 0,
356            rcv_nxt: 0,
357            rx_buffer: Vec::new(),
358            fin_received: false,
359        }
360    }
361}
362
363static mut TCP_SOCKETS: [TcpSocket; TCP_SOCKET_TABLE_SIZE] = [
364    TcpSocket::empty(),
365    TcpSocket::empty(),
366    TcpSocket::empty(),
367    TcpSocket::empty(),
368    TcpSocket::empty(),
369    TcpSocket::empty(),
370    TcpSocket::empty(),
371    TcpSocket::empty(),
372    TcpSocket::empty(),
373    TcpSocket::empty(),
374    TcpSocket::empty(),
375    TcpSocket::empty(),
376    TcpSocket::empty(),
377    TcpSocket::empty(),
378    TcpSocket::empty(),
379    TcpSocket::empty(),
380    TcpSocket::empty(),
381    TcpSocket::empty(),
382    TcpSocket::empty(),
383    TcpSocket::empty(),
384    TcpSocket::empty(),
385    TcpSocket::empty(),
386    TcpSocket::empty(),
387    TcpSocket::empty(),
388    TcpSocket::empty(),
389    TcpSocket::empty(),
390    TcpSocket::empty(),
391    TcpSocket::empty(),
392    TcpSocket::empty(),
393    TcpSocket::empty(),
394    TcpSocket::empty(),
395    TcpSocket::empty(),
396];
397static mut TCP_EPHEMERAL_PORT: u16 = 40000;
398
399pub struct UdpDatagram {
400    pub src_ip: [u8; 4],
401    pub src_port: u16,
402    pub dst_port: u16,
403    pub payload: Vec<u8>,
404}
405
406pub struct HttpGetResult {
407    pub remote_ip: [u8; 4],
408    pub local_port: u16,
409    pub status_code: u16,
410    pub reason: String,
411    pub headers: alloc::collections::BTreeMap<alloc::string::String, alloc::string::String>,
412    pub body: String,
413    /// ボディの生バイト列(chunked 解除済み)。`body` は lossy な文字列。
414    pub body_bytes: alloc::vec::Vec<u8>,
415}
416
417pub struct HttpsGetResult {
418    pub remote_ip: [u8; 4],
419    pub local_port: u16,
420    pub tls_version: String,
421    pub cipher_suite: String,
422    pub cert_subject: String,
423    pub cert_issuer: String,
424    pub cert_not_after: String,
425    pub status_code: u16,
426    pub reason: String,
427    pub headers: alloc::collections::BTreeMap<alloc::string::String, alloc::string::String>,
428    pub body: String,
429    /// ボディの生バイト列(`body` は表示/JSON解析用の lossy String だが、
430    /// 画像/zip 等のバイナリダウンロードはここを使う)。
431    pub body_bytes: alloc::vec::Vec<u8>,
432}
433
434pub struct DhcpLease {
435    pub ip: [u8; 4],
436    pub gateway: [u8; 4],
437    pub dns: [u8; 4],
438    pub dns2: [u8; 4],
439    pub server: [u8; 4],
440    pub subnet: [u8; 4],
441    pub lease_secs: u32,
442    pub t1_secs: u32,
443    pub t2_secs: u32,
444}
445
446struct DhcpAckInfo {
447    msg_type: u8,
448    yiaddr: [u8; 4],
449    server: [u8; 4],
450    router: [u8; 4],
451    dns: [u8; 4],
452    dns2: [u8; 4],
453    subnet: [u8; 4],
454    lease_secs: u32,
455    t1_secs: u32,
456    t2_secs: u32,
457}
458
459static mut NET_STATE: NetworkState = NetworkState {
460    link_up: false,
461    nic_backend: NicBackend::SoftwareLoopback,
462    mac: [0x02, 0x00, 0xA1, 0xCE, 0x00, 0x01],
463    // ip/gateway/dns は「未設定」を表す [0;4] で初期化する。以前は QEMU SLIRP
464    // (-netdev user) のデフォルトアドレス (10.0.2.15/10.0.2.2/10.0.2.3) を
465    // プレースホルダとして埋めていたが、コードベース全体の規約(dhcp.rs:206,
466    // main.rs:672/680 等)は「[0;4] = まだ DHCP で実アドレスを取得していない」
467    // を前提にしている。SLIRP は DHCP がほぼ瞬時に完了するため問題が表面化
468    // しなかったが、vmnet-shared や実機のような実ネットワーク(DHCP に実際の
469    // 往復時間がかかる)では、この非ゼロのプレースホルダのせいで「DHCP完了待ち」
470    // ガードが起動直後から常に true 判定され、NTP同期やDNS解決が実IP確定前に
471    // 発火して 10.0.2.x という存在しないホストへの ARP/送信を試み続け、
472    // 常にタイムアウトする不具合の根本原因になっていた。
473    ip: [0, 0, 0, 0],
474    gateway: [0, 0, 0, 0],
475    dns: [0, 0, 0, 0],
476    dns2: [0, 0, 0, 0],
477    usb_eth_present: false,
478    usb_eth_addr: 0,
479    usb_eth_bulk_in: 0,
480    usb_eth_bulk_out: 0,
481    usb_eth_mps: 0,
482    usb_eth_is_rndis: false,
483    tx_packets: 0,
484    rx_packets: 0,
485    raw_rx_frames: 0,
486    dhcp_active: false,
487    dhcp_server: [0; 4],
488    dhcp_lease_secs: 0,
489    dhcp_subnet: [0; 4],
490    dhcp_t1_secs: 0,
491    dhcp_t2_secs: 0,
492};
493
494static mut DHCP_XID: u32 = 0x41C3_0001;
495static mut DHCP_SERVICE_TICK: u64 = 0;
496static mut DHCP_LEASE_START_TICK: u64 = 0;
497static mut DHCP_RENEW_STAGE: u8 = 0;
498static mut DHCP_NEXT_RENEW_TICK: u64 = 0;
499static mut DHCP_AUTO_PENDING: bool = true;
500static mut DHCP_LAST_AUTO_ATTEMPT_TICK: u64 = 0;
501static mut DHCP_ASYNC_STATE: u8 = 0; // DHCP_ASYNC_IDLE
502static mut DHCP_ASYNC_XID: u32 = 0;
503static mut DHCP_ASYNC_ATTEMPT: usize = 0;
504static mut DHCP_ASYNC_DEADLINE_TICK: u64 = 0;
505static mut DHCP_ASYNC_RENEWING: bool = false;
506static mut DHCP_ASYNC_REBINDING: bool = false;
507static mut LAST_PING_REPLY: Option<PingReply> = None;
508
509// ─── Public API in mod.rs ────────────────────────────────────────────────────
510
511pub fn init() {
512    let _lock = NET_LOCK.lock();
513    unsafe {
514        LAST_PING_REPLY = None;
515        NET_STATE.link_up = false;
516        NET_STATE.nic_backend = NicBackend::SoftwareLoopback;
517        ARP_CACHE = [ArpEntry::empty(); ARP_CACHE_SIZE];
518        ARP_TICK = 1;
519        PING_SEQ = 1;
520        DNS_TXID.store(0x1200, core::sync::atomic::Ordering::Relaxed);
521        DNS_SRC_PORT.store(DNS_SRC_PORT_BASE, core::sync::atomic::Ordering::Relaxed);
522        DNS_CACHE = [DnsCacheEntry::empty(); DNS_CACHE_SIZE];
523        UDP_RX_QUEUE = [UdpRxEntry::empty(); UDP_RX_QUEUE_SIZE];
524        UDP_RX_TICK = 1;
525        NIC_TX_QUEUE = [NicTxEntry::empty(); NIC_TX_QUEUE_SIZE];
526        NIC_TX_TICK = 1;
527        TCP_SOCKETS = [
528            TcpSocket::empty(),
529            TcpSocket::empty(),
530            TcpSocket::empty(),
531            TcpSocket::empty(),
532            TcpSocket::empty(),
533            TcpSocket::empty(),
534            TcpSocket::empty(),
535            TcpSocket::empty(),
536            TcpSocket::empty(),
537            TcpSocket::empty(),
538            TcpSocket::empty(),
539            TcpSocket::empty(),
540            TcpSocket::empty(),
541            TcpSocket::empty(),
542            TcpSocket::empty(),
543            TcpSocket::empty(),
544            TcpSocket::empty(),
545            TcpSocket::empty(),
546            TcpSocket::empty(),
547            TcpSocket::empty(),
548            TcpSocket::empty(),
549            TcpSocket::empty(),
550            TcpSocket::empty(),
551            TcpSocket::empty(),
552            TcpSocket::empty(),
553            TcpSocket::empty(),
554            TcpSocket::empty(),
555            TcpSocket::empty(),
556            TcpSocket::empty(),
557            TcpSocket::empty(),
558            TcpSocket::empty(),
559            TcpSocket::empty(),
560        ];
561        TCP_EPHEMERAL_PORT = 40000;
562        DHCP_XID = 0x41C3_0001;
563        DHCP_SERVICE_TICK = 0;
564        DHCP_LEASE_START_TICK = 0;
565        DHCP_RENEW_STAGE = 0;
566        DHCP_AUTO_PENDING = true;
567        DHCP_LAST_AUTO_ATTEMPT_TICK = 0;
568        DHCP_ASYNC_STATE = 0;
569        DHCP_ASYNC_XID = 0;
570        DHCP_ASYNC_ATTEMPT = 0;
571        DHCP_ASYNC_DEADLINE_TICK = 0;
572        DHCP_ASYNC_RENEWING = false;
573        NET_STATE.dhcp_active = false;
574        NET_STATE.dhcp_server = [0; 4];
575        NET_STATE.dhcp_lease_secs = 0;
576        NET_STATE.dhcp_subnet = [0; 4];
577        NET_STATE.dhcp_t1_secs = 0;
578        NET_STATE.dhcp_t2_secs = 0;
579    }
580
581    if self_test() {
582        crate::debug!("[NET] core initialized (IPv4/UDP/TCP/DNS/ARP/ICMP/HTTP/HTTPS parser ready)");
583    } else {
584        crate::debug!("[NET] core initialized with warnings (self-test failed)");
585    }
586}
587
588pub fn status() -> &'static mut NetworkState {
589    unsafe { &mut NET_STATE }
590}
591
592pub fn status_line() -> String {
593    let _lock = NET_LOCK.lock();
594    let st = status();
595
596    let nic = if st.usb_eth_present {
597        alloc::format!(
598            "usb-eth(addr={},in={},out={},mps={},type={})",
599            st.usb_eth_addr,
600            st.usb_eth_bulk_in,
601            st.usb_eth_bulk_out,
602            st.usb_eth_mps,
603            if st.usb_eth_is_rndis {
604                "rndis"
605            } else {
606                "cdc-ecm"
607            }
608        )
609    } else {
610        String::from("none")
611    };
612
613        let mut dns_list = alloc::vec::Vec::new();
614        if st.dns != [0, 0, 0, 0] {
615            dns_list.push(format_ipv4(st.dns));
616        }
617        if st.dns2 != [0, 0, 0, 0] && st.dns2 != st.dns {
618            dns_list.push(format_ipv4(st.dns2));
619        }
620        for fallback in &[[8, 8, 8, 8], [1, 1, 1, 1]] {
621            if st.dns != *fallback && st.dns2 != *fallback {
622                dns_list.push(format_ipv4(*fallback));
623            }
624        }
625        let dns_display = if dns_list.is_empty() {
626            String::from("none")
627        } else {
628            dns_list.join(", ")
629        };
630
631        alloc::format!(
632            "link={}\nbackend={}\nnic={}\nip={}\nmask={}\ngw={}\ndns={}\ntx={}\nrx={}",
633            if st.link_up { "up" } else { "down" },
634            nic_backend_name(st.nic_backend),
635            nic,
636            format_ipv4(st.ip),
637            format_ipv4(st.dhcp_subnet),
638            format_ipv4(st.gateway),
639            dns_display,
640            st.tx_packets,
641        st.rx_packets
642    )
643}
644
645pub fn nic_debug_info() -> String {
646    let _lock = NET_LOCK.lock();
647    let st = status();
648    let async_state = unsafe { DHCP_ASYNC_STATE };
649    let async_attempt = unsafe { DHCP_ASYNC_ATTEMPT };
650    let async_xid = unsafe { DHCP_ASYNC_XID };
651    let tick = unsafe { DHCP_SERVICE_TICK };
652
653    let mut tx_q_count = 0;
654    unsafe {
655        for e in &NIC_TX_QUEUE {
656            if e.valid {
657                tx_q_count += 1;
658            }
659        }
660    }
661
662    let state_str = match async_state {
663        0 => {
664            if st.dhcp_active {
665                "Bound"
666            } else {
667                "Idle"
668            }
669        }
670        1 => "WaitOffer",
671        2 => "WaitAck",
672        _ => "Unknown",
673    };
674
675    alloc::format!(
676        "NIC DEBUG INFO:\n\
677         Backend: {} (MAC: {})\n\
678         USB Eth: present={} type={} addr={} in={} out={} mps={}\n\
679         Packets: TX={} RX={} RAW_RX_FRAMES={}\n\
680         TX Queue: {}/{} used\n\
681         DHCP State: active={} auto_pending={}\n\
682         DHCP Async: state={} ({}) attempt={} xid={:#010X} tick={}",
683        nic_backend_name(st.nic_backend),
684        format_mac(st.mac),
685        st.usb_eth_present,
686        if st.usb_eth_is_rndis {
687            "rndis"
688        } else {
689            "cdc-ecm"
690        },
691        st.usb_eth_addr,
692        st.usb_eth_bulk_in,
693        st.usb_eth_bulk_out,
694        st.usb_eth_mps,
695        st.tx_packets,
696        st.rx_packets,
697        st.raw_rx_frames,
698        tx_q_count,
699        NIC_TX_QUEUE_SIZE,
700        st.dhcp_active,
701        unsafe { DHCP_AUTO_PENDING },
702        async_state,
703        state_str,
704        async_attempt,
705        async_xid,
706        tick
707    )
708}
709
710pub fn register_usb_ethernet(
711    address: u8,
712    bulk_in_ep: u8,
713    bulk_out_ep: u8,
714    max_packet: u16,
715    is_rndis: bool,
716    mac: [u8; 6],
717) {
718    let _lock = NET_LOCK.lock();
719    unsafe {
720        NET_STATE.usb_eth_present = true;
721        NET_STATE.nic_backend = NicBackend::UsbEthernet;
722        NET_STATE.usb_eth_addr = address;
723        NET_STATE.usb_eth_bulk_in = bulk_in_ep;
724        NET_STATE.usb_eth_bulk_out = bulk_out_ep;
725        NET_STATE.usb_eth_mps = max_packet;
726        NET_STATE.usb_eth_is_rndis = is_rndis;
727        NET_STATE.mac = mac;
728        NET_STATE.link_up = true;
729        DHCP_AUTO_PENDING = true;
730        DHCP_LAST_AUTO_ATTEMPT_TICK = 0;
731    }
732
733    crate::info!(
734        "NET: USB Ethernet registered at addr {} (in ep {}, out ep {}, mps {}, type={})",
735        address,
736        bulk_in_ep,
737        bulk_out_ep,
738        max_packet,
739        if is_rndis { "RNDIS" } else { "CDC-ECM" }
740    );
741}
742
743pub fn nic_backend_name(backend: NicBackend) -> &'static str {
744    match backend {
745        NicBackend::SoftwareLoopback => "software-loopback",
746        NicBackend::UsbEthernet => "usb-ethernet",
747    }
748}
749
750fn self_test() -> bool {
751    let mut query = [0u8; 128];
752    let qlen = match dns::build_dns_query("example.com", 0xCAFE, &mut query) {
753        Ok(n) => n,
754        Err(_) => return false,
755    };
756
757    if qlen < 20 {
758        return false;
759    }
760
761    let response: [u8; 45] = [
762        0xCA, 0xFE, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x07, b'e', b'x',
763        b'a', b'm', b'p', b'l', b'e', 0x03, b'c', b'o', b'm', 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0,
764        0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x04, 192, 168, 1, 100,
765    ];
766
767    if !matches!(
768        dns::parse_dns_a_response(&response, 0xCAFE, "example.com"),
769        Ok(Some([192, 168, 1, 100]))
770    ) {
771        return false;
772    }
773
774
775
776    let req = match http::build_http_get_request_pub("example.com", "/") {
777        Ok(v) => v,
778        Err(_) => return false,
779    };
780    if !req.starts_with("GET /") {
781        return false;
782    }
783
784    let sample = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nhey";
785    let parsed = match http::parse_http_response(sample) {
786        Ok(v) => v,
787        Err(_) => return false,
788    };
789    if parsed.0 != 200 || parsed.3 != "hey" {
790        return false;
791    }
792
793
794
795    let mut dhcp_discover = [0u8; 320];
796    let discover_len = match dhcp::build_dhcp_discover_payload_pub(
797        0x1122_3344,
798        [2, 0, 0xA1, 0xCE, 0, 1],
799        68,
800        67,
801        &mut dhcp_discover,
802    ) {
803        Ok(v) => v,
804        Err(_) => return false,
805    };
806    if discover_len < 244 {
807        return false;
808    }
809
810    let mut dhcp_ack = [0u8; 320];
811    let ack_len = match dhcp::build_dhcp_ack_payload_pub(
812        0x1122_3344,
813        [2, 0, 0xA1, 0xCE, 0, 1],
814        [192, 168, 2, 200],
815        [192, 168, 2, 1],
816        [192, 168, 2, 1],
817        3600,
818        &mut dhcp_ack,
819    ) {
820        Ok(v) => v,
821        Err(_) => return false,
822    };
823    let parsed_ack = match dhcp::parse_dhcp_ack_payload_pub(&dhcp_ack[..ack_len], 0x1122_3344) {
824        Ok(v) => v,
825        Err(_) => return false,
826    };
827    if parsed_ack.msg_type != 5 {
828        return false;
829    }
830    if parsed_ack.yiaddr != [192, 168, 2, 200] {
831        return false;
832    }
833    if parsed_ack.subnet != [255, 255, 255, 0] {
834        return false;
835    }
836    if parsed_ack.t1_secs == 0 || parsed_ack.t2_secs == 0 {
837        return false;
838    }
839
840    true
841}