Repository navigation
toyos-net-ip: 169.254/16 is on the link whatever address is held, and a silent next hop holds up no other - #779
Conversation
…e holds RFC 3927 §2.6.2 has a host resolve a 169.254/16 destination and send to it directly on the link, with a link-local or a routable address alike, and §7 forbids handing such a datagram to a router. The route lookup had only the connected prefixes and then the gateway, so a leased interface sent a link-local asker's mDNS answer to the router; the node's test `an_answer_to_a_link_local_asker_leaves_by_the_router` asserted that frame on purpose. `Interface::on_link` is the one statement of "reachable without a gateway" and now holds 169.254/16 on any interface with a usable address. The lookup sends there after the connected prefixes and before the gateway; ARP reception, which reads the same predicate, takes a link-local sender as a neighbour, without which the request [ip] now sends could never be answered; and a gateway there is on the link. 169.254.255.255 is resolved as a host on an interface whose own prefix is not 169.254/16: the RFC excludes it from the rule, no permission to broadcast was asked for it, and it still reaches no router. The node's test is renamed and finds the request for the asker itself and then the answer in a frame to the asker's own link address. A next hop that never answers ARP holds up no datagram to another (RFC 4861 §7.2.2): the queue was already INCOMPLETE's own, and `rfc_4861_7_2_2_a_silent_next_hop_holds_up_no_other` now holds it so. The track loses its line for the first, says what is left of RFC 3927 §2.6.2 (a link-local source, which no interface can hold today) and that the four new tests carry no scenario id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Negative controls at
#!/bin/sh
# Applies each mutation as a checked patch, runs the tests it must red, records the exit, restores.
S=<scratchpad>
cd <worktree> || exit 2
[ -z "$(git status --porcelain)" ] || { echo "tree not clean"; exit 2; }
echo "head $(git rev-parse HEAD)"
one() { # name
name=$1; patch="$S/mut/$name.patch"
git apply --recount --check "$patch" || { echo "$name: patch does not apply"; exit 2; }
git apply --recount "$patch"
cargo test --locked --no-fail-fast -p toyos-net-ip -p toyos-net-node > "$S/mut/$name.log" 2>&1
echo "$name EXIT=$?"
echo "EXIT recorded above" >> "$S/mut/$name.log"
git apply --recount -R "$patch" || { echo "$name: restore failed"; exit 2; }
[ -z "$(git status --porcelain)" ] || { echo "$name: tree not clean after restore"; exit 2; }
}
one m0-whole-source-change-reverted
one m1-route-arm-removed
one m2-neighbour-queue-global
one m3-arp-reads-prefixes-only
git status --porcelain; echo "clean=$?"Its output (exit 0; each mutation's
diff --git a/toyos-net-ip/src/iface.rs b/toyos-net-ip/src/iface.rs
index b3d126daf..148ac72e3 100644
--- a/toyos-net-ip/src/iface.rs
+++ b/toyos-net-ip/src/iface.rs
@@ -63,10 +63,9 @@ impl Interface {
self.addresses.iter().any(|a| a.cidr.addr() == addr)
}
- /// Reachable without a gateway: inside the prefix of a usable address, or in 169.254/16,
- /// which is this link's whatever address the interface holds (RFC 3927 §2.6.2).
+ /// Inside the prefix of a usable address: reachable without a gateway.
pub fn on_link(&self, addr: Ipv4Addr) -> bool {
- self.usable().any(|a| addr.is_link_local() || a.cidr.contains(addr))
+ self.usable().any(|a| a.cidr.contains(addr))
}
/// A usable address whose prefix holds `toward`, else the first usable one.
diff --git a/toyos-net-ip/src/route.rs b/toyos-net-ip/src/route.rs
index 1437347d7..79f1f91a0 100644
--- a/toyos-net-ip/src/route.rs
+++ b/toyos-net-ip/src/route.rs
@@ -1,8 +1,7 @@
//! The host routing table: the connected prefixes of usable addresses and an ordered list of
-//! on-link gateways per interface; longest prefix first, then 169.254/16 on the link itself (RFC
-//! 3927 §2.6.2), then the active gateway, and no forwarding. A lookup considers only interfaces
-//! that are up, and with a bound source only the interface holding it (RFC 1122 §3.3.4.2, the
-//! strong model).
+//! on-link gateways per interface; longest prefix first, then the active gateway, and no
+//! forwarding. A lookup considers only interfaces that are up, and with a bound source only the
+//! interface holding it (RFC 1122 §3.3.4.2, the strong model).
use core::net::Ipv4Addr;
@@ -93,11 +92,6 @@ pub(crate) fn lookup(ifaces: &[Interface], destination: Ipv4Addr, source: Source
let next_hop = if cidr.broadcast() == Some(destination) { NextHop::Broadcast } else { NextHop::Neighbour(destination) };
return Ok(Route { iface: IfIndex(index), next_hop, source: pick_source(i, destination)? });
}
- // On the link with no prefix holding it is 169.254/16: resolved and sent to directly, and
- // never handed to a router (RFC 3927 §2.6.2, §7).
- if let Some((index, i)) = candidates().find(|(_, i)| i.on_link(destination)) {
- return Ok(Route { iface: IfIndex(index), next_hop: NextHop::Neighbour(destination), source: pick_source(i, destination)? });
- }
let (index, i, gateway) = candidates().find_map(|(index, i)| i.active.map(|g| (index, i, g))).ok_or(Counter::RouteNone)?;
Ok(Route { iface: IfIndex(index), next_hop: NextHop::Neighbour(gateway), source: pick_source(i, gateway)? })
}Red under it:
--- a/toyos-net-ip/src/route.rs
+++ b/toyos-net-ip/src/route.rs
@@ -93,11 +93,6 @@ pub(crate) fn lookup(ifaces: &[Interface], destination: Ipv4Addr, source: Source
let next_hop = if cidr.broadcast() == Some(destination) { NextHop::Broadcast } else { NextHop::Neighbour(destination) };
return Ok(Route { iface: IfIndex(index), next_hop, source: pick_source(i, destination)? });
}
- // On the link with no prefix holding it is 169.254/16: resolved and sent to directly, and
- // never handed to a router (RFC 3927 §2.6.2, §7).
- if let Some((index, i)) = candidates().find(|(_, i)| i.on_link(destination)) {
- return Ok(Route { iface: IfIndex(index), next_hop: NextHop::Neighbour(destination), source: pick_source(i, destination)? });
- }
let (index, i, gateway) = candidates().find_map(|(index, i)| i.active.map(|g| (index, i, g))).ok_or(Counter::RouteNone)?;
Ok(Route { iface: IfIndex(index), next_hop: NextHop::Neighbour(gateway), source: pick_source(i, gateway)? })
}Red under it:
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -395,13 +395,22 @@ pub(crate) fn send(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, hint: Opt
}
}
-/// Holds a frame for an INCOMPLETE neighbour; the queue keeps the newest (RFC 4861 §7.2.2).
+/// MUTATION: one queue of PENDING_PER_NEIGHBOUR for the whole interface, whose oldest datagram
+/// goes whichever neighbour it waits for.
pub(crate) fn hold(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, held: Held) {
- let Some(Nud::Incomplete(s)) = i.neighbours.get_mut(&addr).map(|n| &mut n.state) else { return };
- if s.pending.push(held).is_some() {
- i.held = i.held.saturating_sub(1);
- cx.log.count(Counter::NbPendingOverflow);
+ if !matches!(i.neighbours.get(&addr).map(|n| &n.state), Some(Nud::Incomplete(_))) {
+ return;
+ }
+ if i.held >= PENDING_PER_NEIGHBOUR {
+ let oldest = i.neighbours.values_mut().find_map(|n| match &mut n.state {
+ Nud::Incomplete(s) => s.pending.0.pop_front(),
+ _ => None,
+ });
+ if oldest.is_some() {
+ i.held = i.held.saturating_sub(1);
+ cx.log.count(Counter::NbPendingOverflow);
+ }
}
+ let Some(Nud::Incomplete(s)) = i.neighbours.get_mut(&addr).map(|n| &mut n.state) else { return };
+ s.pending.0.push_back(held);
i.held = i.held.saturating_add(1);
}
Red under it:
--- a/toyos-net-ip/src/arp.rs
+++ b/toyos-net-ip/src/arp.rs
@@ -36,7 +36,7 @@ pub(crate) fn receive(i: &mut Interface, cx: &mut Cx<'_>, arp: &Arp) {
let for_us = arp.operation == Operation::Request && i.is_usable(target);
let mut noticed = for_us;
if !probe {
- if !i.on_link(sender) {
+ if !i.usable().any(|a| a.cidr.contains(sender)) {
cx.log.count(Counter::ArpSenderOffLink);
noticed = true;
} else if i.neighbours.contains_key(&sender) {Red under it: |
|
Review of Growth: BLOCKER
NOTE
Asked
SEND BACK |
…t, a gateway is in a prefix One predicate answered two questions. The route and ARP ask what is reached without a gateway; the gateways ask what may be a router. RFC 3927 widened only the first, so `Interface::on_link` is again the prefix of a usable address, read by `set_gateways` and `withdraw_off_link` as on main, and `Interface::is_neighbour` is that or a host of 169.254/16, read by the route lookup and by ARP reception. A router at 169.254.0.1 on a routable interface is refused `route.gateway-off-link` again. 169.254.0.0 and 169.254.255.255 are a host nowhere: as an ARP sender each is `arp.invalid-sender-address`, as our own prefixes' edges are, and as a destination on an interface whose prefix is another each is `route.none`, neither asked for on the link nor handed to the router. At the reviewed head a reply claiming 169.254.255.255 would have confirmed the entry our own request made. The asker's ARP reply in the tests comes to the link broadcast, as RFC 3927 §2.5 has a link-local sender send it. "Never to a router" cites §2.6.2's own sentence and §2.7, not §7. The track's line for a link-local source says both cases and is held by a host test; its line for the scenario ids says what the readers' routing, reachability and ARP scenarios hold and do not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Round 2 negative controls at The script ( #!/bin/sh
# Applies each mutation as a checked patch, runs the tests, records the exit, restores, and
# refuses a tree that is not clean before or after. $1: the worktree, $2: the patches, $3: the logs.
W=$1; M=$2; L=$3
cd "$W" || exit 2
[ -z "$(git status --porcelain)" ] || { echo "tree not clean"; exit 2; }
echo "head $(git rev-parse HEAD)"
n=10
for patch in "$M"/m*.patch; do
name=$(basename "$patch" .patch)
log="$L/$n-mutation-$name.log"
git apply --recount --check "$patch" || { echo "$name: patch does not apply"; exit 2; }
git apply --recount "$patch"
echo '$ cargo test --locked --no-fail-fast -p toyos-net-ip -p toyos-net-node' > "$log"
cargo test --locked --no-fail-fast -p toyos-net-ip -p toyos-net-node >> "$log" 2>&1
rc=$?
echo "EXIT=$rc" >> "$log"
echo "$name EXIT=$rc"
git apply --recount -R "$patch" || { echo "$name: restore failed"; exit 2; }
[ -z "$(git status --porcelain)" ] || { echo "$name: tree not clean after restore"; exit 2; }
n=$((n+1))
done
echo "tree clean"Its output, step 09 (exit 0): Step 10, diff --git a/toyos-net-ip/src/arp.rs b/toyos-net-ip/src/arp.rs
index 08a6093b8..4c7236053 100644
--- a/toyos-net-ip/src/arp.rs
+++ b/toyos-net-ip/src/arp.rs
@@ -1,6 +1,6 @@
//! ARP reception: the sender's MAC and address are checked, then the packet classified, in
//! `receive`'s order. Only our own
-//! need to send, and a request for one of our addresses from a neighbour, create an entry;
+//! need to send, and a request for one of our addresses from an on-link host, create an entry;
//! any other packet can confirm, assert a MAC into STALE, or be ignored, and a MAC change is
//! always logged.
@@ -9,7 +9,7 @@ use toyos_net_wire::arp::{Arp, Operation};
use toyos_net_wire::ethernet::MacClass;
use crate::counters::Counter;
-use crate::iface::{link_local_edge, Cx, Interface};
+use crate::iface::{Cx, Interface};
use crate::{acd, egress, nud};
pub(crate) fn receive(i: &mut Interface, cx: &mut Cx<'_>, arp: &Arp) {
@@ -24,7 +24,7 @@ pub(crate) fn receive(i: &mut Interface, cx: &mut Cx<'_>, arp: &Arp) {
if mac == i.mac.get() {
return cx.log.count(Counter::ArpOwnSender);
}
- if link_local_edge(sender) || i.usable().any(|a| a.cidr.is_edge(sender)) {
+ if i.usable().any(|a| a.cidr.is_edge(sender)) {
return cx.log.count(Counter::ArpInvalidSenderAddress);
}
if !probe && i.owns(sender) {
@@ -36,7 +36,7 @@ pub(crate) fn receive(i: &mut Interface, cx: &mut Cx<'_>, arp: &Arp) {
let for_us = arp.operation == Operation::Request && i.is_usable(target);
let mut noticed = for_us;
if !probe {
- if !i.is_neighbour(sender) {
+ if !i.on_link(sender) {
cx.log.count(Counter::ArpSenderOffLink);
noticed = true;
} else if i.neighbours.contains_key(&sender) {
diff --git a/toyos-net-ip/src/iface.rs b/toyos-net-ip/src/iface.rs
index 4a6f3d038..148ac72e3 100644
--- a/toyos-net-ip/src/iface.rs
+++ b/toyos-net-ip/src/iface.rs
@@ -50,11 +50,6 @@ impl Cx<'_> {
}
}
-/// The network or the broadcast address of 169.254/16: no host's, on any interface.
-pub(crate) const fn link_local_edge(addr: Ipv4Addr) -> bool {
- matches!(addr.octets(), [169, 254, 0, 0] | [169, 254, 255, 255])
-}
-
impl Interface {
pub fn usable(&self) -> impl Iterator<Item = &Address> + '_ {
self.addresses.iter().filter(|a| a.usable())
@@ -73,12 +68,6 @@ impl Interface {
self.usable().any(|a| a.cidr.contains(addr))
}
- /// Reached without a gateway: on the link, or a host of 169.254/16, which is this link's
- /// whatever address the interface holds (RFC 3927 §2.6.2).
- pub fn is_neighbour(&self, addr: Ipv4Addr) -> bool {
- self.on_link(addr) || (addr.is_link_local() && !link_local_edge(addr) && self.usable().next().is_some())
- }
-
/// A usable address whose prefix holds `toward`, else the first usable one.
pub fn source_for(&self, toward: Ipv4Addr) -> Option<Ipv4Addr> {
self.usable().find(|a| a.cidr.contains(toward)).or_else(|| self.usable().next()).map(|a| a.cidr.addr())
diff --git a/toyos-net-ip/src/route.rs b/toyos-net-ip/src/route.rs
index fc0565490..79f1f91a0 100644
--- a/toyos-net-ip/src/route.rs
+++ b/toyos-net-ip/src/route.rs
@@ -1,8 +1,7 @@
//! The host routing table: the connected prefixes of usable addresses and an ordered list of
-//! on-link gateways per interface; longest prefix first, then 169.254/16 on the link itself (RFC
-//! 3927 §2.6.2), then the active gateway, and no forwarding. A lookup considers only interfaces
-//! that are up, and with a bound source only the interface holding it (RFC 1122 §3.3.4.2, the
-//! strong model).
+//! on-link gateways per interface; longest prefix first, then the active gateway, and no
+//! forwarding. A lookup considers only interfaces that are up, and with a bound source only the
+//! interface holding it (RFC 1122 §3.3.4.2, the strong model).
use core::net::Ipv4Addr;
@@ -93,12 +92,6 @@ pub(crate) fn lookup(ifaces: &[Interface], destination: Ipv4Addr, source: Source
let next_hop = if cidr.broadcast() == Some(destination) { NextHop::Broadcast } else { NextHop::Neighbour(destination) };
return Ok(Route { iface: IfIndex(index), next_hop, source: pick_source(i, destination)? });
}
- // RFC 3927 §2.6.2, §2.7: a host of 169.254/16 is resolved and sent to on the link itself, and
- // nothing to that prefix is a router's: not its two edges either, which are no host.
- if destination.is_link_local() {
- let (index, i) = candidates().find(|(_, i)| i.is_neighbour(destination)).ok_or(Counter::RouteNone)?;
- return Ok(Route { iface: IfIndex(index), next_hop: NextHop::Neighbour(destination), source: pick_source(i, destination)? });
- }
let (index, i, gateway) = candidates().find_map(|(index, i)| i.active.map(|g| (index, i, g))).ok_or(Counter::RouteNone)?;
Ok(Route { iface: IfIndex(index), next_hop: NextHop::Neighbour(gateway), source: pick_source(i, gateway)? })
}Failed assertions, from its log (thread ids removed, long payloads cut): Step 11, diff --git a/toyos-net-ip/src/arp.rs b/toyos-net-ip/src/arp.rs
index 08a6093b8..4c7236053 100644
--- a/toyos-net-ip/src/arp.rs
+++ b/toyos-net-ip/src/arp.rs
@@ -1,6 +1,6 @@
//! ARP reception: the sender's MAC and address are checked, then the packet classified, in
//! `receive`'s order. Only our own
-//! need to send, and a request for one of our addresses from a neighbour, create an entry;
+//! need to send, and a request for one of our addresses from an on-link host, create an entry;
//! any other packet can confirm, assert a MAC into STALE, or be ignored, and a MAC change is
//! always logged.
@@ -9,7 +9,7 @@ use toyos_net_wire::arp::{Arp, Operation};
use toyos_net_wire::ethernet::MacClass;
use crate::counters::Counter;
-use crate::iface::{link_local_edge, Cx, Interface};
+use crate::iface::{Cx, Interface};
use crate::{acd, egress, nud};
pub(crate) fn receive(i: &mut Interface, cx: &mut Cx<'_>, arp: &Arp) {
@@ -24,7 +24,7 @@ pub(crate) fn receive(i: &mut Interface, cx: &mut Cx<'_>, arp: &Arp) {
if mac == i.mac.get() {
return cx.log.count(Counter::ArpOwnSender);
}
- if link_local_edge(sender) || i.usable().any(|a| a.cidr.is_edge(sender)) {
+ if i.usable().any(|a| a.cidr.is_edge(sender)) {
return cx.log.count(Counter::ArpInvalidSenderAddress);
}
if !probe && i.owns(sender) {
@@ -36,7 +36,7 @@ pub(crate) fn receive(i: &mut Interface, cx: &mut Cx<'_>, arp: &Arp) {
let for_us = arp.operation == Operation::Request && i.is_usable(target);
let mut noticed = for_us;
if !probe {
- if !i.is_neighbour(sender) {
+ if !i.on_link(sender) {
cx.log.count(Counter::ArpSenderOffLink);
noticed = true;
} else if i.neighbours.contains_key(&sender) {
diff --git a/toyos-net-ip/src/iface.rs b/toyos-net-ip/src/iface.rs
index 4a6f3d038..b3d126daf 100644
--- a/toyos-net-ip/src/iface.rs
+++ b/toyos-net-ip/src/iface.rs
@@ -50,11 +50,6 @@ impl Cx<'_> {
}
}
-/// The network or the broadcast address of 169.254/16: no host's, on any interface.
-pub(crate) const fn link_local_edge(addr: Ipv4Addr) -> bool {
- matches!(addr.octets(), [169, 254, 0, 0] | [169, 254, 255, 255])
-}
-
impl Interface {
pub fn usable(&self) -> impl Iterator<Item = &Address> + '_ {
self.addresses.iter().filter(|a| a.usable())
@@ -68,15 +63,10 @@ impl Interface {
self.addresses.iter().any(|a| a.cidr.addr() == addr)
}
- /// Inside the prefix of a usable address: reachable without a gateway.
+ /// Reachable without a gateway: inside the prefix of a usable address, or in 169.254/16,
+ /// which is this link's whatever address the interface holds (RFC 3927 §2.6.2).
pub fn on_link(&self, addr: Ipv4Addr) -> bool {
- self.usable().any(|a| a.cidr.contains(addr))
- }
-
- /// Reached without a gateway: on the link, or a host of 169.254/16, which is this link's
- /// whatever address the interface holds (RFC 3927 §2.6.2).
- pub fn is_neighbour(&self, addr: Ipv4Addr) -> bool {
- self.on_link(addr) || (addr.is_link_local() && !link_local_edge(addr) && self.usable().next().is_some())
+ self.usable().any(|a| addr.is_link_local() || a.cidr.contains(addr))
}
/// A usable address whose prefix holds `toward`, else the first usable one.
diff --git a/toyos-net-ip/src/route.rs b/toyos-net-ip/src/route.rs
index fc0565490..1437347d7 100644
--- a/toyos-net-ip/src/route.rs
+++ b/toyos-net-ip/src/route.rs
@@ -93,10 +93,9 @@ pub(crate) fn lookup(ifaces: &[Interface], destination: Ipv4Addr, source: Source
let next_hop = if cidr.broadcast() == Some(destination) { NextHop::Broadcast } else { NextHop::Neighbour(destination) };
return Ok(Route { iface: IfIndex(index), next_hop, source: pick_source(i, destination)? });
}
- // RFC 3927 §2.6.2, §2.7: a host of 169.254/16 is resolved and sent to on the link itself, and
- // nothing to that prefix is a router's: not its two edges either, which are no host.
- if destination.is_link_local() {
- let (index, i) = candidates().find(|(_, i)| i.is_neighbour(destination)).ok_or(Counter::RouteNone)?;
+ // On the link with no prefix holding it is 169.254/16: resolved and sent to directly, and
+ // never handed to a router (RFC 3927 §2.6.2, §7).
+ if let Some((index, i)) = candidates().find(|(_, i)| i.on_link(destination)) {
return Ok(Route { iface: IfIndex(index), next_hop: NextHop::Neighbour(destination), source: pick_source(i, destination)? });
}
let (index, i, gateway) = candidates().find_map(|(index, i)| i.active.map(|g| (index, i, g))).ok_or(Counter::RouteNone)?;Failed assertions, from its log (thread ids removed, long payloads cut): Step 12, --- a/toyos-net-ip/src/route.rs
+++ b/toyos-net-ip/src/route.rs
@@ -93,12 +93,6 @@ pub(crate) fn lookup(ifaces: &[Interface], destination: Ipv4Addr, source: Source
let next_hop = if cidr.broadcast() == Some(destination) { NextHop::Broadcast } else { NextHop::Neighbour(destination) };
return Ok(Route { iface: IfIndex(index), next_hop, source: pick_source(i, destination)? });
}
- // RFC 3927 §2.6.2, §2.7: a host of 169.254/16 is resolved and sent to on the link itself, and
- // nothing to that prefix is a router's: not its two edges either, which are no host.
- if destination.is_link_local() {
- let (index, i) = candidates().find(|(_, i)| i.is_neighbour(destination)).ok_or(Counter::RouteNone)?;
- return Ok(Route { iface: IfIndex(index), next_hop: NextHop::Neighbour(destination), source: pick_source(i, destination)? });
- }
let (index, i, gateway) = candidates().find_map(|(index, i)| i.active.map(|g| (index, i, g))).ok_or(Counter::RouteNone)?;
Ok(Route { iface: IfIndex(index), next_hop: NextHop::Neighbour(gateway), source: pick_source(i, gateway)? })
}Failed assertions, from its log (thread ids removed, long payloads cut): Step 13, --- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -395,13 +395,22 @@ pub(crate) fn send(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, hint: Opt
}
}
-/// Holds a frame for an INCOMPLETE neighbour; the queue keeps the newest (RFC 4861 §7.2.2).
+/// MUTATION: one queue of PENDING_PER_NEIGHBOUR for the whole interface, whose oldest datagram
+/// goes whichever neighbour it waits for.
pub(crate) fn hold(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, held: Held) {
- let Some(Nud::Incomplete(s)) = i.neighbours.get_mut(&addr).map(|n| &mut n.state) else { return };
- if s.pending.push(held).is_some() {
- i.held = i.held.saturating_sub(1);
- cx.log.count(Counter::NbPendingOverflow);
+ if !matches!(i.neighbours.get(&addr).map(|n| &n.state), Some(Nud::Incomplete(_))) {
+ return;
+ }
+ if i.held >= PENDING_PER_NEIGHBOUR {
+ let oldest = i.neighbours.values_mut().find_map(|n| match &mut n.state {
+ Nud::Incomplete(s) => s.pending.0.pop_front(),
+ _ => None,
+ });
+ if oldest.is_some() {
+ i.held = i.held.saturating_sub(1);
+ cx.log.count(Counter::NbPendingOverflow);
+ }
}
+ let Some(Nud::Incomplete(s)) = i.neighbours.get_mut(&addr).map(|n| &mut n.state) else { return };
+ s.pending.0.push_back(held);
i.held = i.held.saturating_add(1);
}
Failed assertions, from its log (thread ids removed, long payloads cut): Step 14, --- a/toyos-net-ip/src/arp.rs
+++ b/toyos-net-ip/src/arp.rs
@@ -36,7 +36,7 @@ pub(crate) fn receive(i: &mut Interface, cx: &mut Cx<'_>, arp: &Arp) {
let for_us = arp.operation == Operation::Request && i.is_usable(target);
let mut noticed = for_us;
if !probe {
- if !i.is_neighbour(sender) {
+ if !i.on_link(sender) {
cx.log.count(Counter::ArpSenderOffLink);
noticed = true;
} else if i.neighbours.contains_key(&sender) {Failed assertions, from its log (thread ids removed, long payloads cut): Step 15, --- a/toyos-net-ip/src/iface.rs
+++ b/toyos-net-ip/src/iface.rs
@@ -75,7 +75,7 @@ impl Interface {
/// Reached without a gateway: on the link, or a host of 169.254/16, which is this link's
/// whatever address the interface holds (RFC 3927 §2.6.2).
pub fn is_neighbour(&self, addr: Ipv4Addr) -> bool {
- self.on_link(addr) || (addr.is_link_local() && !link_local_edge(addr) && self.usable().next().is_some())
+ self.on_link(addr) || (addr.is_link_local() && self.usable().next().is_some())
}
/// A usable address whose prefix holds `toward`, else the first usable one.Failed assertions, from its log (thread ids removed, long payloads cut): Step 16, --- a/toyos-net-ip/src/arp.rs
+++ b/toyos-net-ip/src/arp.rs
@@ -24,7 +24,7 @@ pub(crate) fn receive(i: &mut Interface, cx: &mut Cx<'_>, arp: &Arp) {
if mac == i.mac.get() {
return cx.log.count(Counter::ArpOwnSender);
}
- if link_local_edge(sender) || i.usable().any(|a| a.cidr.is_edge(sender)) {
+ if i.usable().any(|a| a.cidr.is_edge(sender)) {
return cx.log.count(Counter::ArpInvalidSenderAddress);
}
if !probe && i.owns(sender) {Failed assertions, from its log (thread ids removed, long payloads cut): Step 17, --- a/toyos-net-ip/src/route.rs
+++ b/toyos-net-ip/src/route.rs
@@ -160,7 +160,7 @@ impl Ip {
Some(Counter::RouteGatewayInvalid)
} else if local.contains(&g) {
Some(Counter::RouteGatewayIsLocal)
- } else if !i.on_link(g) {
+ } else if !i.is_neighbour(g) {
Some(Counter::RouteGatewayOffLink)
} else {
NoneFailed assertions, from its log (thread ids removed, long payloads cut): Step 18, --- a/toyos-net-ip/src/route.rs
+++ b/toyos-net-ip/src/route.rs
@@ -95,8 +95,7 @@ pub(crate) fn lookup(ifaces: &[Interface], destination: Ipv4Addr, source: Source
}
// RFC 3927 §2.6.2, §2.7: a host of 169.254/16 is resolved and sent to on the link itself, and
// nothing to that prefix is a router's: not its two edges either, which are no host.
- if destination.is_link_local() {
- let (index, i) = candidates().find(|(_, i)| i.is_neighbour(destination)).ok_or(Counter::RouteNone)?;
+ if let Some((index, i)) = candidates().find(|(_, i)| i.is_neighbour(destination)) {
return Ok(Route { iface: IfIndex(index), next_hop: NextHop::Neighbour(destination), source: pick_source(i, destination)? });
}
let (index, i, gateway) = candidates().find_map(|(index, i)| i.active.map(|g| (index, i, g))).ok_or(Counter::RouteNone)?;Failed assertions, from its log (thread ids removed, long payloads cut): |
|
Review of Growth: Round 1, judged
BLOCKERNone new. NOTE
Asked
SEND BACK |
A datagram whose source is 169.254.255.255 or 169.254.0.0 was admitted on an interface whose own prefix is another, since the input policy read only the held prefixes' edges. RFC 1122 §3.2.1.3 has a host discard a datagram from a broadcast address, RFC 3927 §2.6.2 names 169.254.255.255 that for every host, and the network address goes by the tree's own rule for one. Both are now `ip.invalid-source` on every interface, by the predicate ARP reception and the route already read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Round 3 negative controls at The script's output, step 09 (exit 0):
diff --git a/toyos-net-ip/src/input.rs b/toyos-net-ip/src/input.rs
index bb5749451..d2769ea10 100644
--- a/toyos-net-ip/src/input.rs
+++ b/toyos-net-ip/src/input.rs
@@ -14,7 +14,7 @@ use toyos_net_wire::udp::UdpDatagram;
use toyos_net_wire::Instant;
use crate::counters::Counter;
-use crate::iface::{link_local_edge, Interface};
+use crate::iface::Interface;
use crate::{arp, igmp, Arrival, Cast, Delivery, IfIndex, Ip, Peer};
const IPV6: u16 = 0x86DD;
@@ -84,7 +84,7 @@ fn admit(ifaces: &[Interface], i: &Interface, link: MacClass, packet: &Ipv4Packe
return Err(Counter::IpNotForUs);
};
let unspecified_igmp = source == Ipv4Addr::UNSPECIFIED && packet.protocol() == Protocol::Igmp;
- if !unspecified_igmp && (!is_host(source) || link_local_edge(source) || i.usable().any(|a| a.cidr.is_edge(source))) {
+ if !unspecified_igmp && (!is_host(source) || i.usable().any(|a| a.cidr.is_edge(source))) {
return Err(Counter::IpInvalidSource);
}
if ifaces.iter().any(|j| j.owns(source)) {
--- a/toyos-net-ip/src/input.rs
+++ b/toyos-net-ip/src/input.rs
@@ -84,7 +84,7 @@ fn admit(ifaces: &[Interface], i: &Interface, link: MacClass, packet: &Ipv4Packe
return Err(Counter::IpNotForUs);
};
let unspecified_igmp = source == Ipv4Addr::UNSPECIFIED && packet.protocol() == Protocol::Igmp;
- if !unspecified_igmp && (!is_host(source) || link_local_edge(source) || i.usable().any(|a| a.cidr.is_edge(source))) {
+ if !unspecified_igmp && (!is_host(source) || i.usable().any(|a| a.cidr.is_edge(source))) {
return Err(Counter::IpInvalidSource);
}
if ifaces.iter().any(|j| j.owns(source)) {Failed assertions at this head, from each step's log (thread ids removed, long payloads cut): Step 10, Step 11, Step 12, Step 13, Step 14, Step 15, Step 16, Step 17, Step 18, Step 19, |
|
Review of Growth: Earlier findings, judged
BLOCKERNone new. NOTE
Asked
SEND BACK |
IN-01 to IN-09 name no 169.254 and no link-local source, by the round 3 review's reading of them; the refusal of a source at an edge of 169.254/16 is an addition to IN-05's list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
CI at |
…, the drivers' room and wake (#782) and the SMI_CMD call (#780), into the listeners stage No file stopped the merge. One file is both sides': the track, where main's two removed bullets and two added ones (#779) sit in the node's and stage 3's lists and this stage's lines in stage 5's, merged by git and read against both parents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…ers (#777), the drivers' room and wake (#782), the SMI_CMD call (#780) and the guest waits (#786), into the resolver rules One conflict, in issues/toyos-has-its-own-network-stack.md, resolved by hand with every line of both sides accounted for: - The stage 5 paragraph: main's "In the tree", which now names streams, listeners and the one bound, and this branch's "Still to build on it", less the streams and listeners that landed: datagram senders that wait on the hop, then the move. - "What the node does not yet meet": this branch's two lines stand in place of the two lines they rewrote, which main had left as they were; the line on an answer to a 169.254/16 asker, which #779 deleted with the defect, is gone, as is the line on a silent next hop, which #779 deleted with its test (no conflict). - "What stage 3 departs from its specifications": both sides added a line at the list's end; both stand, this branch's on NUD-04 and #779's on the scenarios the readers' specification lacks. Every other file merged without a conflict. toyos-dns/src/lib.rs and userland/netstack/node/src/resolve.rs are byte-identical to b2d9037. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Stage "ip" of the move off smoltcp: two rules of
toyos-net-ip, each with the test that is its exit.toyos-net-ipships in no image (userland/netstacklinks smoltcp and none of thetoyos-net-*crates), so no guest test reaches this and none is added.Head
95b889b15(b83237caband one line of the track after it), onorigin/mainata4416fe6c(#774 is merged in). Round 3: it answers the review of6b14a7f4d, whose one source change is the input path's refusal below.What changed, per decision
1. A host of 169.254/16 is on the link whatever address the interface holds (RFC 3927 §2.6.2, §2.7).
Interface::on_linkis what it is onmain, inside the prefix of a usable address, and its readersset_gatewaysandwithdraw_off_linkare byte for bytemain's: a gateway is in a prefix the interface holds (RFC 2132 §3.5), and 169.254.0.1 on a routable interface is refusedroute.gateway-off-link, astoyos-dhcprefuses it.Interface::is_neighbouris new: on the link, or a host of 169.254/16 on an interface with a usable address. It has two readers:route::lookup, one arm after the connected prefixes and before the gateway: a 169.254/16 destination goes toNextHop::Neighbour(destination)on the lowest eligible interface that has an address, or the one a bound source names, and never falls through to the gateway.arp.sender-off-linkand dropped.arp.invalid-sender-addresson every interface, beside our own prefixes' edges;ip.invalid-sourceon every interface (RFC 1122 §3.2.1.3 for the broadcast address, which RFC 3927 §2.6.2 names for every host; the network address by the tree's rule for one). This is round 3's change:input.rsread only the held prefixes' edges;route.none: §2.6.2 excludes the broadcast address from what is resolved, and §2.7 keeps it from the router;main: it is inside a connected prefix and not that prefix's edge. Nothing answers it, since the reply is refused as a sender, and the entry fails at 3 s.an_answer_to_a_link_local_asker_leaves_by_the_routeris renamedan_answer_to_a_link_local_asker_goes_to_its_own_link_address. It finds exactly one frame after the query, the ARP request for the asker itself, and after the asker's reply the answer in a frame to the asker's own link address. The reply comes to the link's broadcast address, as RFC 3927 §2.5 has a link-local sender send it. The test delivers that reply itself:Lan::pumpanswers only for the router and B, andtests/lan/mod.rsis outside the fence.2. A next hop that never answers ARP holds up no datagram to another (RFC 4861 §7.2.2). No source change: the queue was already INCOMPLETE's own.
rfc_4861_7_2_2_a_silent_next_hop_holds_up_no_otherholds it: nine datagrams to nine hosts behind a silent router fill and overflow the router's queue; eight to B wait in B's; B answers and its eight leave in order while the router's wait on; a ninth to B leaves at once; the router fails alone, and only the flows routed by it are told.3. The track. Its line for the first rule and #774's line for the second are removed: both exits are met. Two lines are added:
route.nonewhere there is none.rfc_3927_2_6_2_a_link_local_source_still_sends_by_the_router_or_nowhereholds both as they stand. The line says that no interface holds such an address today, that no stage is planned that gives one, that §2.5 would be owed with it, and an exit a test reads either way.What a host on the link can now do to the neighbour table
nb.pending-fulluntil they fail at 3 s. Both were already true of any lease of /16 or shorter, and the second of any prefix with 64 unused addresses.Checks (a trust boundary)
etherparse(lan::outside); the query and the asker's reply are written from the RFCs' layouts by the test's own helpers.unwrapandexpect, and the source diff adds amatches!on four octets, ananyand afind.cargo test --locked --no-fail-fast -p toyos-net-ip -p toyos-net-node, and reversed, by one script that refuses a dirty tree before and after. The patches, the script and each failed assertion of rounds 1 and 2 are in their comments; round 3's comment carries the two new patches, the script's output and every failed assertion at this head. Each run's whole log is round 3's step of that number in the orchestrator's scratchpad.toyos-net-ip/srcasorigin/main), tests as here…a_link_local_destination_is_on_the_link,…a_datagram_to_a_link_local_host_is_resolved_and_sent_to_it,…a_link_local_sender_is_a_neighbour,…the_edges_…_are_no_destination,…the_edges_…_are_no_senders,…the_edges_…_are_no_source6b14a7f4d), tests as here: the red-first run of round 3's testrfc_3927_the_edges_of_the_link_local_prefix_are_no_sourcerfc_3927_2_6_2tests;…the_edges_…_are_no_destinationrfc_4861_7_2_2_a_silent_next_hop_holds_up_no_other; alsos_ip_nud_005,s_ip_nud_029…a_datagram_to_a_link_local_host…,…a_link_local_sender_is_a_neighbour…the_edges_…_are_no_destination…the_edges_…_are_no_sendersrfc_3927_a_link_local_address_is_no_gateway_of_a_routable_interface…the_edges_…_are_no_destination…the_edges_…_are_no_sourceThe red-first run of the three tests round 1's review named, against
c9bbdb5f4's source, is round 2's step 11 (exit 101) and its comment.rfc_3927test but the present-state one, and the node's renamed test, are red on the base (step 10); round 3's test is red on6b14a7f4d's source (step 11): the datagram from 169.254.0.0 is delivered,Some(Unicast)whereNoneis wanted.rfc_4861_7_2_2_a_silent_next_hop_holds_up_no_otheris green on the base, as it must be: the property already held, and its control is m2, which also reds two older scenarios.rfc_3927_2_6_2_a_link_local_source_still_sends_by_the_router_or_nowhereis green on every arm: it holds a present state the change does not touch.Gates, round 3, at
b83237cabcargo test --locked --no-fail-fast -p toyos-net-ip -p toyos-net-udp -p toyos-dhcp -p toyos-dns -p toyos-net-shard -p toyos-net-testnet -p toyos-net-tcp -p toyos-net-node -p toyos-mdnscargo clippy --locked -p toyos-net-ip -p toyos-net-node -p toyos-net-udp -p toyos-net-shard -p toyos-dhcp --all-targets --keep-going -- <src/clippy.rs's ADOPTED as -W> -D warningscargo test --locked --lib -- hostws:: userlandhost:: sourcegate:: licence::from the root package (45 passed)Not run, and still owed before this lands:
cargo run -- --ci host. My brief leaves it to CI, and a draft runs no CI, so thehostcheck at this head is skipped and not green. Step 02 is the touched crates and their dependants under the local toolchain's lints, notsrc/clippy.rs's workspace shapes. No guest test and nothing on metal is owed or was run.Growth
git diff --shortstat origin/main...b83237cab: 9 files, +249 −21. Production +27 −9:arp.rs+4 −4,iface.rs+11 −0,input.rs+2 −2,route.rs+10 −3; the code is one predicate of one expression, one constant function with three readers, a five-line arm, and three changed conditions. Tests:toyos-net-ip/tests+203, the node'sname.rs+17 −10. The track +2 −2.Unsure of
route.nonefor the two edges on a routable interface.route.invalid-destinationwould also be a true name; I took the one that says there is no way there from this interface, since an interface that holds the prefix does send to its broadcast.tests/addr.rsbeside the otherrfc_3927tests, which the track's line names; I did not read the specification's input scenarios, so that 169.254 is in none of them rests on the round 2 review's reading of the whole of it.🤖 Generated with Claude Code
https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A