Skip to main content

atmos/kernel/net/
udp_queue.rs

1// L4(Transport) — UDP 受信キュー表操作の「純粋ロジック」。
2//
3// グローバル状態(`UDP_RX_QUEUE`)やロックに依存せず、**呼び出し側から渡された
4// スライス**に対してのみ動作する。DNS 応答の取りこぼしはここでの取り違え・
5// 退避ミスに直結するため、満杯時の LRU 退避やペイロード切り詰めといった
6// 異常系をホスト側ユニットテスト(`tests/src/test_udp_queue.rs`)で検証できる
7// ようにした(`arp_cache.rs`/`tcp_state.rs` と同じ分離方針)。
8
9pub const UDP_RX_MAX_PAYLOAD: usize = 1024;
10
11#[derive(Copy, Clone)]
12pub struct UdpRxEntry {
13    pub valid: bool,
14    pub src_ip: [u8; 4],
15    pub src_port: u16,
16    pub dst_port: u16,
17    pub payload_len: usize,
18    pub payload: [u8; UDP_RX_MAX_PAYLOAD],
19    /// 単調増加の受信時刻。小さいほど古い(LRU 退避の判断に使う)。
20    pub age: u32,
21}
22
23impl UdpRxEntry {
24    pub const fn empty() -> Self {
25        Self {
26            valid: false,
27            src_ip: [0; 4],
28            src_port: 0,
29            dst_port: 0,
30            payload_len: 0,
31            payload: [0; UDP_RX_MAX_PAYLOAD],
32            age: 0,
33        }
34    }
35
36    pub fn payload(&self) -> &[u8] {
37        &self.payload[..self.payload_len]
38    }
39}
40
41#[derive(Copy, Clone, PartialEq, Eq, Debug)]
42pub enum PushOutcome {
43    /// 空きスロットへ格納した。
44    Stored,
45    /// 満杯だったため最も古いエントリを追い出して格納した。
46    Evicted,
47    /// 表が空(サイズ 0)で格納できなかった。
48    Dropped,
49}
50
51/// 受信データグラムをキューへ積む。
52///
53/// `UDP_RX_MAX_PAYLOAD` を超えるペイロードは**切り詰めて**格納する
54/// (破棄はしない)。満杯の場合は age が最小のエントリを追い出す。
55pub fn push(
56    queue: &mut [UdpRxEntry],
57    src_ip: [u8; 4],
58    src_port: u16,
59    dst_port: u16,
60    payload: &[u8],
61    age: u32,
62) -> PushOutcome {
63    if queue.is_empty() {
64        return PushOutcome::Dropped;
65    }
66
67    let copy_len = payload.len().min(UDP_RX_MAX_PAYLOAD);
68    let mut entry = UdpRxEntry {
69        valid: true,
70        src_ip,
71        src_port,
72        dst_port,
73        payload_len: copy_len,
74        payload: [0; UDP_RX_MAX_PAYLOAD],
75        age,
76    };
77    entry.payload[..copy_len].copy_from_slice(&payload[..copy_len]);
78
79    for slot in queue.iter_mut() {
80        if !slot.valid {
81            *slot = entry;
82            return PushOutcome::Stored;
83        }
84    }
85
86    let mut oldest_idx = 0usize;
87    let mut oldest_age = queue[0].age;
88    for (i, e) in queue.iter().enumerate().skip(1) {
89        if e.age < oldest_age {
90            oldest_age = e.age;
91            oldest_idx = i;
92        }
93    }
94    queue[oldest_idx] = entry;
95    PushOutcome::Evicted
96}
97
98/// 宛先ポートが一致し、かつ述語を満たすエントリのうち**最も古いもの**の
99/// 添字を返す(取り出しは呼び出し側が行う)。
100///
101/// 最も古いものを選ぶのは、同一問い合わせに対して複数サーバから応答が
102/// 返る DNS のようなケースで、先に届いた応答から順に消費するため。
103pub fn find_matching(
104    queue: &[UdpRxEntry],
105    dst_port: u16,
106    pred: impl Fn(&UdpRxEntry) -> bool,
107) -> Option<usize> {
108    let mut best: Option<(usize, u32)> = None;
109    for (i, e) in queue.iter().enumerate() {
110        if !e.valid || e.dst_port != dst_port {
111            continue;
112        }
113        if !pred(e) {
114            continue;
115        }
116        match best {
117            None => best = Some((i, e.age)),
118            Some((_, best_age)) if e.age < best_age => best = Some((i, e.age)),
119            _ => {}
120        }
121    }
122    best.map(|(i, _)| i)
123}
124
125/// 指定した宛先ポート宛のエントリを全て破棄し、破棄した件数を返す。
126///
127/// 【2026-07-29】DNS クライアントは冗長化のため 1 クエリを複数サーバ
128/// (1.1.1.1 と 8.8.8.8)へ同時送出する。応答は先着 1 件だけ消費され、
129/// **もう一方の応答がキューに永久に残っていた**。クエリを重ねるほど
130/// 残骸が溜まり、6 件のキューが消費済みの古い応答で埋まって
131/// 新しい DNS 応答が入らなくなる(実測: `udp_queue_len=6` で全て古い応答)。
132///
133/// 送信元ポートはクエリごとに使い捨てなので、1 件消費した時点で
134/// 同じポート宛の残りは不要。呼び出し側は消費直後にこれを呼ぶこと。
135pub fn purge_port(queue: &mut [UdpRxEntry], dst_port: u16) -> usize {
136    let mut n = 0;
137    for e in queue.iter_mut() {
138        if e.valid && e.dst_port == dst_port {
139            *e = UdpRxEntry::empty();
140            n += 1;
141        }
142    }
143    n
144}