Skip to main content

atmos/kernel/net/
arp_cache.rs

1// L2(Data Link) — ARP キャッシュ表操作の「純粋ロジック」。
2//
3// グローバル状態(`ARP_CACHE`)やハードウェアに依存せず、**呼び出し側から渡された
4// スライス**に対してのみ動作する。これによりホスト側ユニットテスト
5// (`tests/src/test_arp_cache.rs`) から、満杯時の LRU 退避・否定キャッシュの
6// 期限切れといった**異常系**を決定論的に検証できる。
7//
8// `arp.rs` 側はグローバル配列とロックの管理だけを担当し、表の操作判断は
9// すべてここへ委譲する(`tcp_state.rs`/`addr_util.rs` と同じ分離方針)。
10
11#[derive(Copy, Clone, PartialEq, Eq, Debug)]
12pub struct ArpEntry {
13    pub valid: bool,
14    pub ip: [u8; 4],
15    pub mac: [u8; 6],
16    /// 単調増加のアクセス時刻。小さいほど古い(LRU 退避の判断に使う)。
17    pub age: u32,
18}
19
20impl ArpEntry {
21    pub const fn empty() -> Self {
22        Self {
23            valid: false,
24            ip: [0; 4],
25            mac: [0; 6],
26            age: 0,
27        }
28    }
29}
30
31/// `upsert` が実際に行った操作。呼び出し側のログ出力の出し分けに使う。
32#[derive(Copy, Clone, PartialEq, Eq, Debug)]
33pub enum UpsertOutcome {
34    /// 既存エントリの MAC/age を更新した。
35    Updated,
36    /// 空きスロットへ新規登録した。
37    Inserted,
38    /// 満杯だったため最も古いエントリを追い出して登録した。
39    Evicted,
40    /// 登録対象外のアドレス(0.0.0.0 / 255.255.255.255)だったため何もしなかった。
41    Ignored,
42}
43
44pub fn lookup(table: &[ArpEntry], ip: [u8; 4]) -> Option<[u8; 6]> {
45    for e in table.iter() {
46        if e.valid && e.ip == ip {
47            return Some(e.mac);
48        }
49    }
50    None
51}
52
53/// IP→MAC 対応を登録・更新する。
54///
55/// 表が満杯の場合は **age が最小(最も古い)** のエントリを追い出す。
56/// `age` は呼び出し側が単調増加で採番して渡す。
57pub fn upsert(table: &mut [ArpEntry], ip: [u8; 4], mac: [u8; 6], age: u32) -> UpsertOutcome {
58    // 未指定アドレスとブロードキャストは ARP 解決の対象ではないので保持しない
59    // (保持すると有限のスロットを無駄に消費する)。
60    if ip == [0, 0, 0, 0] || ip == [255, 255, 255, 255] {
61        return UpsertOutcome::Ignored;
62    }
63    if table.is_empty() {
64        return UpsertOutcome::Ignored;
65    }
66
67    // 1) 同じ IP が既にあれば更新(重複エントリを作らない)
68    for e in table.iter_mut() {
69        if e.valid && e.ip == ip {
70            e.mac = mac;
71            e.age = age;
72            return UpsertOutcome::Updated;
73        }
74    }
75
76    // 2) 空きスロットがあればそこへ
77    for e in table.iter_mut() {
78        if !e.valid {
79            *e = ArpEntry {
80                valid: true,
81                ip,
82                mac,
83                age,
84            };
85            return UpsertOutcome::Inserted;
86        }
87    }
88
89    // 3) 満杯なので最古を追い出す
90    let mut oldest_idx = 0usize;
91    let mut oldest_age = table[0].age;
92    for (i, e) in table.iter().enumerate().skip(1) {
93        if e.age < oldest_age {
94            oldest_age = e.age;
95            oldest_idx = i;
96        }
97    }
98    table[oldest_idx] = ArpEntry {
99        valid: true,
100        ip,
101        mac,
102        age,
103    };
104    UpsertOutcome::Evicted
105}
106
107/// ARP 解決に失敗した宛先を一定時間おぼえておくための否定キャッシュ。
108///
109/// 同じ到達不能な宛先へ DNS のリトライ等が連続する場合、毎回 2 秒のブロッキング
110/// 解決を払うのを避けるためにある。**期限切れの判定を誤ると、一時的な失敗が
111/// 恒久的な通信不能として残り続ける**ため単体で検証できるようにした。
112#[derive(Copy, Clone, PartialEq, Eq, Debug)]
113pub struct NegativeCache {
114    pub ip: [u8; 4],
115    pub until_ms: u64,
116}
117
118impl NegativeCache {
119    pub const fn empty() -> Self {
120        Self {
121            ip: [0; 4],
122            until_ms: 0,
123        }
124    }
125
126    /// `ip` への解決を今スキップすべきか。
127    pub fn should_skip(&self, ip: [u8; 4], now_ms: u64) -> bool {
128        self.ip == ip && now_ms < self.until_ms
129    }
130
131    pub fn record_failure(&mut self, ip: [u8; 4], now_ms: u64, ttl_ms: u64) {
132        self.ip = ip;
133        self.until_ms = now_ms.saturating_add(ttl_ms);
134    }
135
136    /// 解決に成功したら否定キャッシュを解除する。
137    /// これを忘れると、一度失敗した宛先が TTL の間ずっとスキップされ続ける。
138    pub fn clear_if_matches(&mut self, ip: [u8; 4]) {
139        if self.ip == ip {
140            self.ip = [0; 4];
141            self.until_ms = 0;
142        }
143    }
144}