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