Repository navigation
toyos-net-udp: a datagram waits for its next hop in its own sender; [ip] takes none it cannot frame and holds only its own messages; a failed hop drops what waited; the node reads a refused query in the opportunity that refused it - #792
Conversation
…one of them [udp] handed every datagram to [ip] at its turn, and [ip] kept one whose next hop had no link address yet in that neighbour's queue: eight places for every sender, the oldest dropped for the newest. A burst of the node's sixteen lookups at a resolver not yet resolved lost eight queries there, which the resolver stage answered by raising the queue to 16 and asserting it against MAX_LOOKUPS; a client's datagram to the same next hop still took a query's place. Udp::serve now offers a sender's datagrams in the order it accepted them and the caller answers each: taken, waits for this next hop, or unreachable. One that waits stays in its sender's queue, under the bound that queue already has, and is passed over, so the sender's datagrams to other next hops leave (US-26) and those to one hop leave in order. The shard asks [ip] the question [tcp]'s flows are asked, by the one peek both share, before it hands [ip] a datagram; Udp::wake offers the datagrams of a hop that resolved again, Udp::wake_all those of every hop when the routes change, and Udp::fail drops what waited for a hop [ip] gave up (US-27): counted udp.tx-unreachable, a connected socket told once, no other told, nothing kept. The closed sender waits per datagram the same way. The broadcast permission stays on the queued datagram and the route is asked again when it leaves. PENDING_PER_NEIGHBOUR is 8 again, NUD-04's test is the scenario's ten again, and the resolver's compile-time assertion is gone. Node::transmit takes the draws and carries the lookups on: a query dropped at its turn, for a resolver [ip] holds failed, is read in that opportunity and the query asked in its place leaves in it. The report no longer waits on a socket for the node's next wake. toyos-dhcp's test harness calls Udp::serve and answers it Offer::Taken: one line outside the stage's files, which the signature forces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Mutation patches and the script that ran them, at run.sh#!/bin/sh
# Applies each mutation as a checked patch to a clean tree, builds, runs the five crates' suites,
# reports both exit codes and the tests that failed, and reverses the patch. Refuses a dirty tree
# before and after each.
# usage: run.sh <worktree> <mutations dir> <logs dir> [patch...]
set -u
tree=$1; dir=$2; logs=$3; shift 3
crates="-p toyos-net-udp -p toyos-net-ip -p toyos-net-shard -p toyos-net-node -p toyos-dhcp"
cd "$tree" || exit 2
[ $# -gt 0 ] || set -- "$dir"/m*.patch
for patch in "$@"; do
name=$(basename "$patch" .patch)
log="$logs/$name.log"
if [ -n "$(git status --porcelain --ignore-submodules=none)" ]; then echo "$name: the tree is dirty before" ; exit 2; fi
{ echo "head $(git rev-parse HEAD)"; echo "git apply --check $name.patch"; } > "$log"
if ! git apply --check "$patch" >> "$log" 2>&1; then echo "$name: the patch does not apply"; exit 2; fi
git apply "$patch"
echo "cargo test --locked --no-run $crates" >> "$log"
cargo test --locked --no-run $crates >> "$log" 2>&1; build=$?
echo "BUILD_EXIT=$build" >> "$log"
tests="not run"
if [ $build -eq 0 ]; then
echo "cargo test --locked --no-fail-fast $crates" >> "$log"
cargo test --locked --no-fail-fast $crates >> "$log" 2>&1; tests=$?
echo "TEST_EXIT=$tests" >> "$log"
fi
git apply -R "$patch"
if [ -n "$(git status --porcelain --ignore-submodules=none)" ]; then echo "$name: the tree is dirty after"; exit 2; fi
red=$(grep -c '^test .* \.\.\. FAILED$' "$log")
echo "$name build=$build tests=$tests failed=$red"
done
echo "all restored: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ') dirty paths"m00-the-whole-source-change-reverted-onto-the-base.patchdiff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 9cbee3f78..59be88ccf 100644
--- a/toyos-net-ip/src/lib.rs
+++ b/toyos-net-ip/src/lib.rs
@@ -108,7 +108,7 @@ pub mod limits {
pub const FAILED_HOLD: Duration = Duration::from_secs(20);
pub const LOCKTIME: Duration = Duration::from_secs(1);
pub const IDLE_LIFETIME: Duration = Duration::from_secs(600);
- pub const PENDING_PER_NEIGHBOUR: usize = 8;
+ pub const PENDING_PER_NEIGHBOUR: usize = 16;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index c47e74c1f..678f1f50a 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -13,12 +13,7 @@
//! next hop is unresolved or failed builds nothing, spends nothing, and is not asked again until
//! [ip] reports a change for that next hop, for the routes, or, to a flow a full neighbour table
//! refused, that the table has room: a waiting flow costs one question per such change. A UDP
-//! datagram is asked the same question before [ip] is handed it: one whose next hop is unresolved
-//! stays in its sender's queue in [udp], spending nothing and charged nothing, while the sender's
-//! datagrams to other next hops leave: it is asked again when [ip] reports that hop resolved or
-//! the routes changed, and dropped when [ip] reports the hop failed. One with no route, or whose
-//! next hop [ip] holds failed or has no room for, is dropped as its turn comes. [ip] holds no
-//! sender's datagram.
+//! datagram whose next hop is unresolved waits in [ip], spending nothing and charged nothing.
//!
//! **Refusals.** Each crate's refusals of legacy or insecure input pass through that crate's
//! `RefusalLog` here: at most one [`Event::Refused`] per rule in any 10 s, carrying how many
@@ -50,7 +45,7 @@ use core::net::Ipv4Addr;
use toyos_net_ip::{Advice, Delivery, ErrorKind, IfIndex, Ip, Limiter, NextHop, Nud, Resolution, Sent, Source, Transport, TransportError, FRAME};
use toyos_net_tcp::{ConnId, Endpoint, Hop, IcmpError, IcmpKind, ListenerId, Outgoing, Received, Seq, Served, Status, Tcp, Tuple};
-use toyos_net_udp::{Offer, Sender, SocketId, Udp, Verdict};
+use toyos_net_udp::{Sender, SocketId, Udp, Verdict};
use toyos_net_udp::Served as UdpServed;
use toyos_net_wire::ethernet::{FrameBuilder, IndividualMac, MacAddr};
use toyos_net_wire::ipv4::{Ipv4Builder, Ipv4Source, MulticastAddr, TrafficClass, Ttl};
@@ -149,8 +144,8 @@ struct Member {
/// What serving a flow did with a frame of credit.
struct Outcome {
- /// The length of the frame that left; none when the flow had nothing to offer, or when [ip]
- /// refused its datagram.
+ /// The length of the frame that left; none when the flow had nothing, or when [ip] holds its
+ /// datagram for its next hop or refused it.
frame: Option<i32>,
/// The flow has nothing left: it leaves the round, its deficit with it.
leaves: bool,
@@ -343,33 +338,18 @@ impl Shard {
(done, Some(len))
}
- /// One datagram of `sender`'s, framed by [ip]: the oldest whose next hop's link address is
- /// known, or that needs none. One [ip] refuses as it leaves makes no frame.
+ /// One datagram of `sender`'s, framed by [ip]; one [ip] holds for its next hop or refuses
+ /// leaves no frame.
fn udp_frame(&mut self, now: Instant, sender: Sender, sink: &mut impl FnMut(&[u8])) -> Outcome {
let Self { ip, udp, frame, .. } = self;
let mut sent = None;
let served = udp.serve(sender, |out| {
- let source = if out.source.is_unspecified() { Source::Unspecified } else { Source::Bound(out.source) };
- let Ok(route) = ip.route(out.destination, source, None) else { return Offer::Unreachable };
- if let NextHop::Neighbour(next_hop) = route.next_hop {
- match peek(ip, now, route.iface, next_hop, route.source) {
- (Resolution::Resolved(_), _) => {}
- (Resolution::Pending, _) => return Offer::Waits(next_hop),
- (Resolution::Failed, _) => return Offer::Unreachable,
- }
- }
- match ip.send_udp(now, out, frame) {
- Ok(Sent::Frame(len)) => {
- sent = frame.get(..len).map(|bytes| {
- sink(bytes);
- charge(bytes)
- });
- }
- Ok(Sent::Held) => unreachable!("[ip] is handed a datagram only once its next hop has a link address"),
- // Counted by [ip], which tells the datagram's flow.
- Err(_) => {}
+ if let Ok(Sent::Frame(len)) = ip.send_udp(now, out, frame) {
+ sent = frame.get(..len).map(|bytes| {
+ sink(bytes);
+ charge(bytes)
+ });
}
- Offer::Taken
});
Outcome { frame: sent, leaves: served != UdpServed::More }
}
@@ -409,16 +389,9 @@ impl Shard {
}
}
toyos_net_ip::Event::Unreachable(flow) => udp.unreachable(&flow),
- toyos_net_ip::Event::Resolved { next_hop, .. } => {
- wake(tcp, waiting, Wait::Hop(next_hop));
- udp.wake(next_hop);
- }
- toyos_net_ip::Event::Failed { next_hop, .. } => {
+ toyos_net_ip::Event::Resolved { next_hop, .. } | toyos_net_ip::Event::Failed { next_hop, .. } | toyos_net_ip::Event::Cleared { next_hop, .. } => {
wake(tcp, waiting, Wait::Hop(next_hop));
- udp.fail(next_hop);
}
- // No datagram waits on a failed next hop: the failure dropped them.
- toyos_net_ip::Event::Cleared { next_hop, .. } => wake(tcp, waiting, Wait::Hop(next_hop)),
toyos_net_ip::Event::Room { .. } => wake(tcp, waiting, Wait::Room),
toyos_net_ip::Event::Verified { addr, .. } => events.push(Event::Verified(addr)),
toyos_net_ip::Event::Conflict { addr, mac, .. } => events.push(Event::Conflict { addr, mac }),
@@ -435,7 +408,6 @@ impl Shard {
self.routes = ip.generation();
waiting.clear();
tcp.wake_all();
- udp.wake_all();
}
let head = self.round.front().map(|m| m.flow);
let gone: BTreeSet<Flow> = tcp.drain_gone().map(Flow::Tcp).chain(udp.drain_gone().map(Flow::Udp)).collect();
@@ -672,33 +644,22 @@ fn hop(ip: &mut Ip, now: Instant, iface: IfIndex, tuple: &Tuple, waiting: &mut B
let remote = tuple.remote.addr;
let Ok(route) = ip.route(remote, Source::Bound(tuple.local.addr), Some(iface)) else { return Hop::Unreachable };
let NextHop::Neighbour(next_hop) = route.next_hop else { return Hop::Unreachable };
- let (answer, wait) = match peek(ip, now, iface, next_hop, route.source) {
- (Resolution::Resolved(mac), _) => return Hop::Ready(Via { next_hop, mac }),
- (Resolution::Pending, wait) => (Hop::Pending, wait),
- (Resolution::Failed, wait) => (Hop::Unreachable, wait),
- };
- waiting.entry(wait).or_default().insert(remote);
- answer
-}
-
-/// Whether a frame for `next_hop` can be built now, and what a flow that cannot build one waits
-/// on. The entry is peeked and does not move: only a next hop with no entry is resolved, which
-/// queues its request, preferring `source`.
-fn peek(ip: &mut Ip, now: Instant, iface: IfIndex, next_hop: Ipv4Addr, source: Ipv4Addr) -> (Resolution, Wait) {
- match ip.neighbour(iface, next_hop) {
- Some(entry) => {
- let answer = match entry.mac() {
- Some(mac) => Resolution::Resolved(mac),
- None if matches!(entry, Nud::Failed) => Resolution::Failed,
- None => Resolution::Pending,
- };
- (answer, Wait::Hop(next_hop))
- }
- None => match ip.resolve(now, iface, next_hop, source) {
- Resolution::Failed => (Resolution::Failed, Wait::Room),
- answer => (answer, Wait::Hop(next_hop)),
+ let (answer, wait) = match ip.neighbour(iface, next_hop) {
+ Some(entry) => match entry.mac() {
+ Some(mac) => (Hop::Ready(Via { next_hop, mac }), None),
+ None if matches!(entry, Nud::Failed) => (Hop::Unreachable, Some(Wait::Hop(next_hop))),
+ None => (Hop::Pending, Some(Wait::Hop(next_hop))),
+ },
+ None => match ip.resolve(now, iface, next_hop, route.source) {
+ Resolution::Resolved(mac) => (Hop::Ready(Via { next_hop, mac }), None),
+ Resolution::Pending => (Hop::Pending, Some(Wait::Hop(next_hop))),
+ Resolution::Failed => (Hop::Unreachable, Some(Wait::Room)),
},
+ };
+ if let Some(wait) = wait {
+ waiting.entry(wait).or_default().insert(remote);
}
+ answer
}
/// What a frame costs its flow's deficit: its length, which `FRAME` bounds and `QUANTUM` fits.
@@ -787,32 +748,6 @@ mod tests {
}
}
- // No id: a sender all of whose datagrams wait for a next hop is out of the round, and a next
- // hop that resolves puts back the senders that waited for it and no other: a waiting datagram
- // costs one question per change of its own next hop.
- #[test]
- fn a_resolved_next_hop_wakes_the_senders_that_waited_for_it_and_no_other() {
- const C: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 3);
- const D: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 4);
- let (mut shard, now) = verified();
- shard.transmit(now, usize::MAX, |_| {});
- let port = |n| Port::new(n).unwrap();
- let (to_c, to_d) = (shard.bind(Ipv4Addr::UNSPECIFIED, Some(port(5000)), || 0).unwrap(), shard.bind(Ipv4Addr::UNSPECIFIED, Some(port(5001)), || 0).unwrap());
- shard.send_to(now, to_c, C, 9, b"c").unwrap();
- shard.send_to(now, to_d, D, 9, b"d").unwrap();
- assert_eq!(shard.round.len(), 2);
- assert_eq!(shard.transmit(now, usize::MAX, |_| {}), 2, "a request for each");
- assert!(shard.round.is_empty(), "what waits is not served");
-
- let mut reply = [[2, 0, 0, 0, 0, 0x0a], [2, 0, 0, 0, 0, 0x0c]].concat();
- reply.extend_from_slice(&[0x08, 0x06, 0, 1, 8, 0, 6, 4, 0, 2, 2, 0, 0, 0, 0, 0x0c]);
- reply.extend_from_slice(&C.octets());
- reply.extend_from_slice(&[2, 0, 0, 0, 0, 0x0a]);
- reply.extend_from_slice(&A.octets());
- shard.receive(now, &reply);
- assert_eq!(shard.round.iter().map(|m| m.flow).collect::<Vec<_>>(), [Flow::Udp(Sender::Socket(to_c))]);
- }
-
// No id: the head freed in its turn takes the turn with it, so the next flow's turn adds its
// quantum; a flow behind it leaves the turn as it was.
#[test]
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..edeb93d6f 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -8,16 +8,9 @@
//! datagrams: a send past it is refused and the caller keeps its datagram. What closed sockets
//! had accepted is held to `limits::CLOSED_DATAGRAMS` together and is one [`Sender`] beside the
//! sockets, so closing never starves another socket. The caller's round over senders decides
-//! whose datagram leaves next: a datagram leaves only when [`Udp::serve`] offers it to the caller
-//! and the caller takes it, and is built then. What [`Udp::drain_eligible`] and
-//! [`Udp::drain_gone`] report is held until the caller drains it, a socket closed while offered
-//! included.
-//!
-//! **A datagram waits for its next hop where it was accepted.** One the caller answers
-//! [`Offer::Waits`] stays in its sender's queue, under that queue's bound, and is passed over
-//! until [`Udp::wake`] names the hop: the sender's datagrams to other hops leave meanwhile, and
-//! those to one hop leave in the order they were accepted. One with no way out is dropped,
-//! counted, and reported to a connected socket; nothing is kept for another try.
+//! whose datagram leaves next: a datagram leaves only when [`Udp::serve`] hands it to the caller
+//! and is built then. What [`Udp::drain_eligible`] and [`Udp::drain_gone`] report is held until
+//! the caller drains it, a socket closed while offered included.
//!
//! **Refusals are values.** Every refusal is a named [`Counter`] and the [`Error`] the call
//! returns; one of legacy or insecure input is also a [`Refusal`] naming the socket and the peer.
@@ -82,7 +75,6 @@ toyos_net_wire::counters! {
RxDiscardedOnClose = "udp.rx-discarded-on-close";
TxDiscardedOnClose = "udp.tx-discarded-on-close";
Tx = "udp.tx";
- TxUnreachable = "udp.tx-unreachable";
IcmpErrorDelivered = "udp.icmp-error-delivered";
IcmpErrorSoft = "udp.icmp-error-soft";
IcmpErrorUnconnected = "udp.icmp-error-unconnected";
@@ -130,8 +122,7 @@ pub enum SocketError {
Refused,
/// An ICMP error said the way to the peer is administratively prohibited.
Prohibited,
- /// A datagram the socket had accepted found no way out, no route or no link address for its
- /// next hop, or [ip] refused it as it left: it is gone.
+ /// [ip] found no link address for the next hop of a datagram it held: nothing left.
NextHopFailed,
}
@@ -193,8 +184,6 @@ struct Stored {
#[derive(Debug)]
struct Queued {
source: Ipv4Addr,
- /// The port of the socket that accepted it.
- source_port: Port,
destination: Ipv4Addr,
port: Port,
ttl: Ttl,
@@ -202,24 +191,6 @@ struct Queued {
/// leaves is decided by this, not by what the socket holds then.
broadcast: bool,
payload: Vec<u8>,
- /// The next hop whose link address it waits for: not offered until [`Udp::wake`] names it.
- waits: Option<Ipv4Addr>,
-}
-
-impl Queued {
- fn out(&self) -> UdpOut<'_> {
- UdpOut {
- source: self.source,
- destination: self.destination,
- ttl: self.ttl,
- broadcast: self.broadcast,
- datagram: UdpBuilder { source: self.source_port, destination: self.port, data: &self.payload },
- }
- }
-
- fn flow(&self) -> Flow {
- Flow { source: self.source, source_port: self.source_port, destination: self.destination, destination_port: self.port }
- }
}
#[derive(Debug)]
@@ -232,6 +203,7 @@ struct Socket {
rx_bytes: usize,
rx_full: u64,
tx: VecDeque<Queued>,
+ tx_bytes: usize,
/// In `eligible` or the caller's round.
offered: bool,
pending: Option<SocketError>,
@@ -255,28 +227,14 @@ pub enum Sender {
Closed,
}
-/// What the caller did with a datagram [`Udp::serve`] offered it.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum Offer {
- /// It left [udp]: in a frame, or refused by [ip], which counted it and told its flow.
- Taken,
- /// The link address of this next hop is being asked for: the datagram stays with its sender
- /// until [`Udp::wake`] names the hop.
- Waits(Ipv4Addr),
- /// It has no way out: no route, or a next hop [ip] has given up on or has no room for.
- Unreachable,
-}
-
/// What [`Udp::serve`] handed the caller.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Served {
- /// A datagram, and another may be offered behind it.
+ /// A datagram, and another waits behind it.
More,
- /// The last datagram the sender may be offered: it leaves the round, and is offered again
- /// once it has another or one of its own is woken.
+ /// The sender's last datagram: it leaves the round, and is offered again once it has another.
Last,
- /// Nothing: the sender has no datagram that waits for no next hop, or names a socket since
- /// closed.
+ /// Nothing: the sender has no datagram, or names a socket since closed.
Nothing,
}
@@ -293,7 +251,7 @@ pub struct Udp {
closed_offered: bool,
/// Datagrams closed sockets had accepted: they still leave, at most
/// `limits::CLOSED_DATAGRAMS` of them.
- closed: VecDeque<Queued>,
+ closed: VecDeque<(Port, Queued)>,
counters: Counters,
refusals: Vec<Refusal>,
}
@@ -391,6 +349,7 @@ impl Udp {
rx_bytes: 0,
rx_full: 0,
tx: VecDeque::new(),
+ tx_bytes: 0,
offered: false,
pending: None,
ttl: Ttl::DEFAULT,
@@ -566,12 +525,13 @@ impl Udp {
Err(refusal) => return self.refuse(route_refusal(refusal), me, peer),
};
let socket = self.socket(id)?;
- let held: usize = socket.tx.iter().map(|queued| queued.payload.len()).sum();
- if socket.tx.len() >= limits::TX_DATAGRAMS || held.saturating_add(payload.len()) > limits::TX_BYTES {
+ let bytes = socket.tx_bytes.saturating_add(payload.len());
+ if socket.tx.len() >= limits::TX_DATAGRAMS || bytes > limits::TX_BYTES {
return self.refuse(Counter::TxQueueFull, me, peer);
}
let ttl = if destination.is_multicast() { multicast_ttl } else { ttl };
- socket.tx.push_back(Queued { source, source_port: local_port, destination, port, ttl, broadcast: broadcast_permitted, payload: payload.to_vec(), waits: None });
+ socket.tx.push_back(Queued { source, destination, port, ttl, broadcast: broadcast_permitted, payload: payload.to_vec() });
+ socket.tx_bytes = bytes;
if !core::mem::replace(&mut socket.offered, true) {
self.eligible.push(Sender::Socket(id));
}
@@ -621,7 +581,7 @@ impl Udp {
if socket.offered {
self.gone.push(Sender::Socket(id));
}
- self.closed.extend(socket.tx.into_iter().take(room));
+ self.closed.extend(socket.tx.into_iter().take(room).map(|q| (socket.port, q)));
if !self.closed.is_empty() && !core::mem::replace(&mut self.closed_offered, true) {
self.eligible.push(Sender::Closed);
}
@@ -716,8 +676,7 @@ impl Udp {
self.count(Counter::IcmpErrorDelivered);
}
- /// A datagram of `flow` this crate had accepted will not leave: [ip] refused it as it left,
- /// or it found no way out.
+ /// [ip] could not reach the next hop of a datagram this crate handed it.
pub fn unreachable(&mut self, flow: &Flow) {
if let Some(socket) = self.connected(flow) {
socket.pending = Some(SocketError::NextHopFailed);
@@ -741,94 +700,42 @@ impl Udp {
self.gone.drain(..)
}
- /// Offers `sender`'s datagrams in the order it accepted them, those waiting for a next hop
- /// aside, until `offer` takes one: each is built as `offer` reads it. One `offer` answers
- /// [`Offer::Waits`] for stays and waits; one it answers [`Offer::Unreachable`] for is dropped
- /// and counted, and the connected socket it left from is told.
- pub fn serve(&mut self, sender: Sender, mut offer: impl FnMut(&UdpOut<'_>) -> Offer) -> Served {
- let (queue, offered) = match sender {
- Sender::Closed => (&mut self.closed, &mut self.closed_offered),
+ /// The oldest datagram of `sender`, built as `sink` takes it; whether it leaves the stack then
+ /// or waits in [ip] for its next hop is the caller's.
+ pub fn serve(&mut self, sender: Sender, sink: impl FnOnce(&UdpOut<'_>)) -> Served {
+ let (port, queued, last) = match sender {
+ Sender::Closed => {
+ let Some((port, queued)) = self.closed.pop_front() else {
+ self.closed_offered = false;
+ return Served::Nothing;
+ };
+ let last = self.closed.is_empty();
+ self.closed_offered = !last;
+ (port, queued, last)
+ }
Sender::Socket(id) => {
let Ok(socket) = self.socket(id) else { return Served::Nothing };
- (&mut socket.tx, &mut socket.offered)
+ let Some(queued) = socket.tx.pop_front() else {
+ socket.offered = false;
+ return Served::Nothing;
+ };
+ socket.tx_bytes = socket.tx_bytes.saturating_sub(queued.payload.len());
+ socket.offered = !socket.tx.is_empty();
+ (socket.port, queued, !socket.offered)
}
};
- let mut taken = false;
- let mut lost = Vec::new();
- queue.retain_mut(|queued| {
- if taken || queued.waits.is_some() {
- return true;
- }
- match offer(&queued.out()) {
- Offer::Taken => taken = true,
- Offer::Waits(next_hop) => queued.waits = Some(next_hop),
- Offer::Unreachable => lost.push(queued.flow()),
- }
- queued.waits.is_some()
+ self.count(Counter::Tx);
+ sink(&UdpOut {
+ source: queued.source,
+ destination: queued.destination,
+ ttl: queued.ttl,
+ broadcast: queued.broadcast,
+ datagram: UdpBuilder { source: port, destination: queued.port, data: &queued.payload },
});
- let more = taken && queue.iter().any(|queued| queued.waits.is_none());
- *offered = more;
- self.counters.add(Counter::Tx, u64::from(taken));
- self.lose(&lost);
- match (taken, more) {
- (true, true) => Served::More,
- (true, false) => Served::Last,
- (false, _) => Served::Nothing,
- }
- }
-
- /// Datagrams this crate had accepted, each dropped for want of a way out.
- fn lose(&mut self, lost: &[Flow]) {
- self.counters.add(Counter::TxUnreachable, u64::try_from(lost.len()).unwrap_or(u64::MAX));
- for flow in lost {
- self.unreachable(flow);
- }
- }
-
- /// [ip] gave `next_hop` up: every datagram that waited for it is dropped and counted, and
- /// the connected socket it left from is told.
- pub fn fail(&mut self, next_hop: Ipv4Addr) {
- let mut lost = Vec::new();
- let queues = self.slots.iter_mut().filter_map(|slot| slot.socket.as_mut()).map(|socket| &mut socket.tx).chain([&mut self.closed]);
- for queue in queues {
- queue.retain(|queued| {
- let waited = queued.waits == Some(next_hop);
- if waited {
- lost.push(queued.flow());
- }
- !waited
- });
- }
- self.lose(&lost);
- }
-
- /// `next_hop`'s link address is known: the datagrams that wait for it are offered again.
- pub fn wake(&mut self, next_hop: Ipv4Addr) {
- self.rouse(|waits| waits == next_hop);
- }
-
- /// The routes changed: every waiting datagram's next hop is asked for again.
- pub fn wake_all(&mut self) {
- self.rouse(|_| true);
- }
-
- /// Ends the wait of every datagram whose next hop `woken` names, and offers its sender.
- fn rouse(&mut self, woken: impl Fn(Ipv4Addr) -> bool) {
- let Self { slots, closed, closed_offered, eligible, .. } = self;
- let mut rouse = |queue: &mut VecDeque<Queued>, offered: &mut bool, sender: Sender| {
- let mut any = false;
- for queued in queue.iter_mut().filter(|queued| queued.waits.is_some_and(&woken)) {
- queued.waits = None;
- any = true;
- }
- if any && !core::mem::replace(offered, true) {
- eligible.push(sender);
- }
- };
- for (index, slot) in slots.iter_mut().enumerate() {
- let (Some(socket), Ok(index)) = (&mut slot.socket, u32::try_from(index)) else { continue };
- rouse(&mut socket.tx, &mut socket.offered, Sender::Socket(SocketId { index, generation: slot.generation }));
+ if last {
+ Served::Last
+ } else {
+ Served::More
}
- rouse(closed, closed_offered, Sender::Closed);
}
}
diff --git a/userland/netstack/node/src/datagram.rs b/userland/netstack/node/src/datagram.rs
index 99a6e04b2..b40ad1f5c 100644
--- a/userland/netstack/node/src/datagram.rs
+++ b/userland/netstack/node/src/datagram.rs
@@ -17,13 +17,6 @@
//! accepted for a host leaves in no link broadcast, whatever prefix a server has renewed the
//! address with since, and is dropped, counted `ip.broadcast-not-permitted` and logged instead.
//!
-//! **A datagram waits for its next hop in its own socket**, among the `limits::TX_DATAGRAMS` it
-//! has accepted: while [ip] asks for a link address the socket's datagrams to other next hops
-//! leave, and a send past the queue is answered [`Refused::ResourceExhausted`] whatever its
-//! destination. One whose next hop [ip] gives up, or that has no way out when its turn comes, is
-//! dropped and counted `udp.tx-unreachable`, and the client is told nothing: its socket is not
-//! connected.
-//!
//! A refusal [udp] logs is in [`Node::drain_events`] when the send that met it returns, and one
//! [ip] logs as a datagram leaves when that [`Node::transmit`] returns; no other call of a
//! client's meets one.
@@ -107,7 +100,6 @@ fn refused(error: Error) -> Refused {
| Counter::RxDiscardedOnClose
| Counter::TxDiscardedOnClose
| Counter::Tx
- | Counter::TxUnreachable
| Counter::IcmpErrorDelivered
| Counter::IcmpErrorSoft
| Counter::IcmpErrorUnconnected
diff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs
index 4a68d7b9b..3a2ca566c 100644
--- a/userland/netstack/node/src/lib.rs
+++ b/userland/netstack/node/src/lib.rs
@@ -20,8 +20,8 @@
//! [`Node::drain_events`].
//!
//! **Draws.** Each `draw` is handed to the client, whose order is its own: a call that starts an
-//! exchange draws its transaction id first. The lookups in flight draw after it, and alone in
-//! [`Node::transmit`]: an id and then a port for each query they send, and nothing else.
+//! exchange draws its transaction id first. The lookups in flight draw after it: an id and then a
+//! port for each query they send, and nothing else.
#![no_std]
#![forbid(unsafe_code)]
@@ -150,19 +150,11 @@ impl Node {
}
/// A transmit opportunity with room for `credit` frames, each handed to `sink` as it is built.
- /// Returns how many left. A lookup's query that found no way out as its turn came is read
- /// here, and the query asked in its place leaves in this opportunity while credit lasts.
- pub fn transmit(&mut self, now: Instant, credit: usize, mut sink: impl FnMut(&[u8]), mut draw: impl FnMut() -> u32) -> usize {
- let mut sent = 0usize;
- loop {
- sent = sent.saturating_add(self.stack.transmit(now, credit.saturating_sub(sent), &mut sink));
- // A datagram [ip] refused as it left is a line of this opportunity.
- self.log();
- let asked = self.resolver.pass(now, &mut self.stack, &mut self.counters, &mut draw);
- if !asked || sent >= credit {
- break;
- }
- }
+ /// Returns how many left.
+ pub fn transmit(&mut self, now: Instant, credit: usize, sink: impl FnMut(&[u8])) -> usize {
+ let sent = self.stack.transmit(now, credit, sink);
+ // A datagram [ip] refused as it left is a line of this opportunity.
+ self.log();
self.pass(now, true);
sent
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 054fa6ac9..1dcef864a 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -13,17 +13,18 @@
//! - **A query the node knows did not reach its resolver is let go**
//! (`toyos_dns::Lookup::on_unreached`): the lookup asks its next resolver at once, or ends
//! where no query's answer is read any more. The node knows it of a query [udp] refuses in the
-//! call, which never left, is counted and holds no socket; and of one [udp] reports found no
-//! way out, no route or no link address for its next hop, which is counted and its socket
-//! closed. That report is read in the call that made it, a transmit opportunity included.
+//! call, which never left, is counted and holds no socket; and of one [ip] reports it found no
+//! next hop for, which is counted and its socket closed.
//! - **An ICMP error is a report and not knowledge** (`toyos_dns::Lookup::on_report`): [udp]
//! hands a query's socket one that quotes the socket's addresses and ports and says refused or
//! prohibited, which takes an off-path sender the query's port to forge and not its id (RFC
//! 8085 §5.2). It is counted, and the next resolver is asked at once; the query's socket stays
//! open and its answer is read, and no number of them ends a lookup.
-//! - **A query whose resolver's link address is not known yet waits in its own socket**, as
-//! [udp] keeps every datagram: no other lookup's query and no client's datagram to that next
-//! hop takes its place, and it leaves when the address is known.
+//! - **Every lookup's first query can wait in [ip] for one next hop at once**, where no link
+//! address is known yet: [ip] holds `PENDING_PER_NEIGHBOUR` datagrams for a next hop it is
+//! resolving and keeps the newest (RFC 4861 §7.2.2), which is no fewer than the lookups held
+//! here. A datagram another socket sends to that next hop meanwhile takes a place in the same
+//! queue.
//! - **A lookup asks the resolvers of the lease it started under, and ends with that lease's
//! word**: when the held lease names other resolvers, or none is held.
//! - **A wait is a deadline of the node's** ([`Resolver::next_deadline`]); a reply is a frame.
@@ -38,6 +39,7 @@ use alloc::vec::Vec;
use core::net::Ipv4Addr;
use toyos_dns::{Asked, Failure, Lookup, Name, Step, MAX_LOOKUPS, PORT};
+use toyos_net_ip::limits::nud::PENDING_PER_NEIGHBOUR;
use toyos_net_udp::{Error, SocketError, SocketId};
use toyos_net_wire::Instant;
@@ -45,6 +47,8 @@ use crate::lease::Stack;
use crate::name::millis;
use crate::{Counter, Counters, Node};
+const _: () = assert!(PENDING_PER_NEIGHBOUR >= MAX_LOOKUPS);
+
/// One lookup, from [`Node::resolve`] until its answer is taken or it is let go.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LookupId(u64);
@@ -120,7 +124,7 @@ fn close(stack: &mut Stack, now: Instant, socket: SocketId) {
enum Heard {
/// A datagram of this length, now in the reply buffer.
Reply(usize),
- /// The stack's own word that the query never left.
+ /// [ip]'s word that the query never left.
Unreached,
/// An ICMP error's word that the query was refused.
Reported,
@@ -142,8 +146,8 @@ fn waiting(queries: &[Query], stack: &mut Stack, reply: &mut [u8]) -> Option<(Qu
/// Carries out `step` for `asking`, and every step the lookup answers a query [udp] refused
/// with, then closes the socket of every query the lookup no longer reads an answer for. Returns
-/// how the lookup ended, if it did, and sets `queued` when [udp] accepted a query.
-fn act(asking: &mut Asking, mut step: Step, now: Instant, stack: &mut Stack, counters: &mut Counters, draw: &mut impl FnMut() -> u32, queued: &mut bool) -> Option<Result<Vec<[u8; 4]>, Ended>> {
+/// how the lookup ended, if it did.
+fn act(asking: &mut Asking, mut step: Step, now: Instant, stack: &mut Stack, counters: &mut Counters, draw: &mut impl FnMut() -> u32) -> Option<Result<Vec<[u8; 4]>, Ended>> {
let done = loop {
match step {
Step::Ask { asked, to, query } => {
@@ -152,7 +156,6 @@ fn act(asking: &mut Asking, mut step: Step, now: Instant, stack: &mut Stack, cou
let resolver = Ipv4Addr::from(to);
if stack.connect(now, socket, resolver, PORT).is_ok() && stack.send_to(now, socket, resolver, PORT, &query).is_ok() {
asking.queries.push(Query { asked, socket, to });
- *queued = true;
break None;
}
counters.add(Counter::QueryUnsent, 1);
@@ -195,7 +198,7 @@ impl Resolver {
let servers: Vec<[u8; 4]> = resolvers.iter().map(Ipv4Addr::octets).collect();
let Some((lookup, step)) = Lookup::start(name, &servers, millis(now), || id(&mut *draw)) else { unreachable!("the lease names a resolver") };
let mut asking = Asking { id: LookupId(self.next), lookup, resolvers, queries: Vec::new() };
- let done = act(&mut asking, step, now, stack, counters, &mut *draw, &mut false);
+ let done = act(&mut asking, step, now, stack, counters, &mut *draw);
if done == Some(Err(Ended::NoPort)) {
return Err(NotStarted::ResourceExhausted);
}
@@ -212,12 +215,10 @@ impl Resolver {
/// Ends every lookup whose lease went or names other resolvers, reads every reply that
/// reached a query's socket, and ends every wait that is over. A lookup that ended closes
- /// its sockets and waits to be taken. Returns whether a query was queued, which the next
- /// transmit opportunity carries.
- pub(crate) fn pass(&mut self, now: Instant, stack: &mut Stack, counters: &mut Counters, draw: &mut impl FnMut() -> u32) -> bool {
+ /// its sockets and waits to be taken.
+ pub(crate) fn pass(&mut self, now: Instant, stack: &mut Stack, counters: &mut Counters, draw: &mut impl FnMut() -> u32) {
let Self { asking: lookups, ended, reply, .. } = self;
let ms = millis(now);
- let mut queued = false;
lookups.retain_mut(|asking| {
let named = stack.lease().is_some_and(|lease| lease.dns == asking.resolvers);
let mut done = if named { None } else { Some(Err(Ended::LeaseChanged)) };
@@ -236,11 +237,11 @@ impl Resolver {
asking.lookup.on_report(query.asked, ms, || id(&mut *draw))
}
};
- done = act(asking, step, now, stack, counters, &mut *draw, &mut queued);
+ done = act(asking, step, now, stack, counters, &mut *draw);
}
if done.is_none() {
let step = asking.lookup.on_time(ms, || id(&mut *draw));
- done = act(asking, step, now, stack, counters, &mut *draw, &mut queued);
+ done = act(asking, step, now, stack, counters, &mut *draw);
}
let Some(result) = done else { return true };
for query in &asking.queries {
@@ -249,7 +250,6 @@ impl Resolver {
ended.push(Resolved { id: asking.id, result });
false
});
- queued
}
fn let_go(&mut self, now: Instant, id: LookupId, stack: &mut Stack) {m01-the-shard-asks-no-question-and-ip-holds-as-on-the-base.patchdiff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index c47e74c1f..95e8ef06d 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -354,7 +354,7 @@ impl Shard {
if let NextHop::Neighbour(next_hop) = route.next_hop {
match peek(ip, now, route.iface, next_hop, route.source) {
(Resolution::Resolved(_), _) => {}
- (Resolution::Pending, _) => return Offer::Waits(next_hop),
+ (Resolution::Pending, _) => {}
(Resolution::Failed, _) => return Offer::Unreachable,
}
}
@@ -365,7 +365,7 @@ impl Shard {
charge(bytes)
});
}
- Ok(Sent::Held) => unreachable!("[ip] is handed a datagram only once its next hop has a link address"),
+ Ok(Sent::Held) => {}
// Counted by [ip], which tells the datagram's flow.
Err(_) => {}
}m02-a-waiting-datagram-holds-back-the-senders-later-ones.patchdiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..11779a977 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -754,14 +754,19 @@ impl Udp {
}
};
let mut taken = false;
+ let mut blocked = false;
let mut lost = Vec::new();
queue.retain_mut(|queued| {
- if taken || queued.waits.is_some() {
+ blocked |= queued.waits.is_some();
+ if taken || blocked {
return true;
}
match offer(&queued.out()) {
Offer::Taken => taken = true,
- Offer::Waits(next_hop) => queued.waits = Some(next_hop),
+ Offer::Waits(next_hop) => {
+ queued.waits = Some(next_hop);
+ blocked = true;
+ }
Offer::Unreachable => lost.push(queued.flow()),
}
queued.waits.is_some()m03-a-resolved-hop-wakes-nothing.patchdiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..b66df2dec 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -804,7 +804,7 @@ impl Udp {
/// `next_hop`'s link address is known: the datagrams that wait for it are offered again.
pub fn wake(&mut self, next_hop: Ipv4Addr) {
- self.rouse(|waits| waits == next_hop);
+ let _ = next_hop;
}
/// The routes changed: every waiting datagram's next hop is asked for again.m04-a-failed-hop-drops-nothing.patchdiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..fc8dc3305 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -792,7 +792,7 @@ impl Udp {
let queues = self.slots.iter_mut().filter_map(|slot| slot.socket.as_mut()).map(|socket| &mut socket.tx).chain([&mut self.closed]);
for queue in queues {
queue.retain(|queued| {
- let waited = queued.waits == Some(next_hop);
+ let waited = queued.waits == Some(next_hop) && queued.waits.is_none();
if waited {
lost.push(queued.flow());
}m05-a-datagram-with-no-way-out-is-kept.patchdiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..18b755254 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -762,7 +762,7 @@ impl Udp {
match offer(&queued.out()) {
Offer::Taken => taken = true,
Offer::Waits(next_hop) => queued.waits = Some(next_hop),
- Offer::Unreachable => lost.push(queued.flow()),
+ Offer::Unreachable => queued.waits = Some(Ipv4Addr::UNSPECIFIED),
}
queued.waits.is_some()
});m06-a-dropped-datagrams-connected-socket-is-not-told.patchdiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..ce4a68035 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -780,9 +780,6 @@ impl Udp {
/// Datagrams this crate had accepted, each dropped for want of a way out.
fn lose(&mut self, lost: &[Flow]) {
self.counters.add(Counter::TxUnreachable, u64::try_from(lost.len()).unwrap_or(u64::MAX));
- for flow in lost {
- self.unreachable(flow);
- }
}
/// [ip] gave `next_hop` up: every datagram that waited for it is dropped and counted, andm07-a-socket-that-is-not-connected-is-told.patchdiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..ea966d9f1 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -719,7 +719,7 @@ impl Udp {
/// A datagram of `flow` this crate had accepted will not leave: [ip] refused it as it left,
/// or it found no way out.
pub fn unreachable(&mut self, flow: &Flow) {
- if let Some(socket) = self.connected(flow) {
+ if let Some(socket) = self.ports.get(&flow.source_port).copied().and_then(|index| self.at(index)) {
socket.pending = Some(SocketError::NextHopFailed);
}
}m08-the-shard-does-not-wake-udp-on-a-resolved-hop.patchdiff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index c47e74c1f..d65dd0eec 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -411,7 +411,6 @@ impl Shard {
toyos_net_ip::Event::Unreachable(flow) => udp.unreachable(&flow),
toyos_net_ip::Event::Resolved { next_hop, .. } => {
wake(tcp, waiting, Wait::Hop(next_hop));
- udp.wake(next_hop);
}
toyos_net_ip::Event::Failed { next_hop, .. } => {
wake(tcp, waiting, Wait::Hop(next_hop));m09-the-shard-does-not-tell-udp-of-a-failed-hop.patchdiff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index c47e74c1f..2062d2ec0 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -415,7 +415,6 @@ impl Shard {
}
toyos_net_ip::Event::Failed { next_hop, .. } => {
wake(tcp, waiting, Wait::Hop(next_hop));
- udp.fail(next_hop);
}
// No datagram waits on a failed next hop: the failure dropped them.
toyos_net_ip::Event::Cleared { next_hop, .. } => wake(tcp, waiting, Wait::Hop(next_hop)),m10-a-change-of-routes-wakes-no-datagram.patchdiff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index c47e74c1f..1f81371e6 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -435,7 +435,6 @@ impl Shard {
self.routes = ip.generation();
waiting.clear();
tcp.wake_all();
- udp.wake_all();
}
let head = self.round.front().map(|m| m.flow);
let gone: BTreeSet<Flow> = tcp.drain_gone().map(Flow::Tcp).chain(udp.drain_gone().map(Flow::Udp)).collect();m11-a-resolved-hop-wakes-every-waiting-datagram.patchdiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..4498e8d57 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -804,7 +804,8 @@ impl Udp {
/// `next_hop`'s link address is known: the datagrams that wait for it are offered again.
pub fn wake(&mut self, next_hop: Ipv4Addr) {
- self.rouse(|waits| waits == next_hop);
+ let _ = next_hop;
+ self.rouse(|_| true);
}
/// The routes changed: every waiting datagram's next hop is asked for again.m12-a-queued-datagram-carries-no-permission-of-its-call.patchdiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..ddb1c50da 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -212,7 +212,7 @@ impl Queued {
source: self.source,
destination: self.destination,
ttl: self.ttl,
- broadcast: self.broadcast,
+ broadcast: true,
datagram: UdpBuilder { source: self.source_port, destination: self.port, data: &self.payload },
}
}m13-a-failed-hop-is-handed-to-ip-at-the-datagrams-turn.patchdiff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index c47e74c1f..cfe844f73 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -355,7 +355,7 @@ impl Shard {
match peek(ip, now, route.iface, next_hop, route.source) {
(Resolution::Resolved(_), _) => {}
(Resolution::Pending, _) => return Offer::Waits(next_hop),
- (Resolution::Failed, _) => return Offer::Unreachable,
+ (Resolution::Failed, _) => {}
}
}
match ip.send_udp(now, out, frame) {m14-a-datagram-with-no-route-is-taken-and-not-counted.patchdiff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index c47e74c1f..05297fb08 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -350,7 +350,7 @@ impl Shard {
let mut sent = None;
let served = udp.serve(sender, |out| {
let source = if out.source.is_unspecified() { Source::Unspecified } else { Source::Bound(out.source) };
- let Ok(route) = ip.route(out.destination, source, None) else { return Offer::Unreachable };
+ let Ok(route) = ip.route(out.destination, source, None) else { return Offer::Taken };
if let NextHop::Neighbour(next_hop) = route.next_hop {
match peek(ip, now, route.iface, next_hop, route.source) {
(Resolution::Resolved(_), _) => {}m15-the-node-carries-no-lookup-on-in-transmit.patchdiff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs
index 4a68d7b9b..8d8c2c0d4 100644
--- a/userland/netstack/node/src/lib.rs
+++ b/userland/netstack/node/src/lib.rs
@@ -158,7 +158,8 @@ impl Node {
sent = sent.saturating_add(self.stack.transmit(now, credit.saturating_sub(sent), &mut sink));
// A datagram [ip] refused as it left is a line of this opportunity.
self.log();
- let asked = self.resolver.pass(now, &mut self.stack, &mut self.counters, &mut draw);
+ let asked = false;
+ let _ = &mut draw;
if !asked || sent >= credit {
break;
}m16-the-query-asked-in-a-dropped-ones-place-waits-for-the-next-opportunity.patchdiff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs
index 4a68d7b9b..7faa8821e 100644
--- a/userland/netstack/node/src/lib.rs
+++ b/userland/netstack/node/src/lib.rs
@@ -159,7 +159,7 @@ impl Node {
// A datagram [ip] refused as it left is a line of this opportunity.
self.log();
let asked = self.resolver.pass(now, &mut self.stack, &mut self.counters, &mut draw);
- if !asked || sent >= credit {
+ if asked || !asked {
break;
}
}m17-ips-queue-for-a-next-hop-is-16.patchdiff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 9cbee3f78..59be88ccf 100644
--- a/toyos-net-ip/src/lib.rs
+++ b/toyos-net-ip/src/lib.rs
@@ -108,7 +108,7 @@ pub mod limits {
pub const FAILED_HOLD: Duration = Duration::from_secs(20);
pub const LOCKTIME: Duration = Duration::from_secs(1);
pub const IDLE_LIFETIME: Duration = Duration::from_secs(600);
- pub const PENDING_PER_NEIGHBOUR: usize = 8;
+ pub const PENDING_PER_NEIGHBOUR: usize = 16;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}m18-what-waits-is-not-counted-against-the-sockets-queue.patchdiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..94926a9e2 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -567,7 +567,7 @@ impl Udp {
};
let socket = self.socket(id)?;
let held: usize = socket.tx.iter().map(|queued| queued.payload.len()).sum();
- if socket.tx.len() >= limits::TX_DATAGRAMS || held.saturating_add(payload.len()) > limits::TX_BYTES {
+ if socket.tx.iter().filter(|queued| queued.waits.is_none()).count() >= limits::TX_DATAGRAMS || held.saturating_add(payload.len()) > limits::TX_BYTES {
return self.refuse(Counter::TxQueueFull, me, peer);
}
let ttl = if destination.is_multicast() { multicast_ttl } else { ttl }; |
|
Review, round 1, of Net lines: 19 files, +780 -150. Production +247 -99 ( BLOCKER
NOTE
Checked, no finding
At the head that landsThe fixes change source, so SEND BACK |
…atagram-senders stage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…s own messages Round 1 left two answers to one question. The shard asked [ip] for a datagram's route and next hop, dropped in [udp] what had no way out, and then handed [ip] the rest, whose own refusal of the same cases stayed behind it; and [ip] still carried the code that holds a transport's datagram for a next hop, reached by its own tests and by no caller. Ip::send_udp now answers in one call: the frame, a refusal it counts and reports to the flow as before, or Sent::Pending(next_hop) for a datagram it did not take, whose next hop it has asked for. The shard maps that to Offer::Waits and nothing else; Offer::Unreachable, the shard's route and neighbour lookups for a datagram, and its unreachable! are gone, and a datagram's route is looked up once as it leaves. udp.tx-unreachable counts what Udp::fail drops and nothing else. [ip]'s neighbour queue holds [ip]'s own ICMP messages alone, and the types say so: Held has no flow, FrameKind no Datagram, and a dropped held message tells nobody. The scenarios that filled that queue with UDP datagrams fill it with echo replies; the track records them by id. limits::TX_BYTES is a constant no send can reach, asserted so at compile time, and the per-send sum that compared against it is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Answer to the round-1 review, at BLOCKER 1: the second path to an outcome [ip] already produces. Fixed, by deleting it.
The full neighbour table, measured: it needs no variant. The shard makes one call for a datagram, The [udp] harness's BLOCKER 2: the residuals without an owner, and the dead transport half of [ip]'s queue. Fixed, by deleting the half.By the orchestrator's decision the fence widened to
Two residuals remain on the track, each with an owner and a due stage: the one queue a socket (the worker of the move, by its first run on the T14 or in a guest), and a link that returns before a transmit opportunity (the worker of the move, before the move). The second is narrower than round 1's line and measured at this head: the datagram waits anew and leaves when its hop answers ( NOTEs
Measured at
|
| exit | |
|---|---|
cargo test --locked -p toyos-net-udp: 66 passed |
0 |
cargo test --locked -p toyos-net-ip: 273 passed |
0 |
cargo test --locked -p toyos-net-shard: 56 passed |
0 |
cargo test --locked -p toyos-net-node: 144 passed |
0 |
cargo test --locked -p toyos-dhcp: 76 passed |
0 |
| clippy on the six crates, the adopted lints, warnings denied | 0 |
| the source gates, 45 passed | 0 |
cargo run -- --ci host: 78 steps, all green |
0 |
cargo run -- --build-only |
0 |
| m01 to m30 less m05, twenty-nine: each built | 0 |
| the same twenty-nine: each run | 101 |
The patches and the script are in the comment below.
|
Mutation patches of round 2 and the script that ran them, at the runrun.sh#!/bin/sh
# Applies each mutation as a checked patch to a clean tree, builds, runs the five crates' suites,
# reports both exit codes and the tests that failed, and reverses the patch. Refuses a dirty tree
# before and after each.
# usage: run.sh <worktree> <mutations dir> <logs dir> [patch...]
set -u
tree=$1; dir=$2; logs=$3; shift 3
crates="-p toyos-net-udp -p toyos-net-ip -p toyos-net-shard -p toyos-net-node -p toyos-dhcp"
cd "$tree" || exit 2
[ $# -gt 0 ] || set -- "$dir"/m*.patch
for patch in "$@"; do
name=$(basename "$patch" .patch)
log="$logs/$name.log"
if [ -n "$(git status --porcelain --ignore-submodules=none)" ]; then echo "$name: the tree is dirty before" ; exit 2; fi
{ echo "head $(git rev-parse HEAD)"; echo "git apply --check $name.patch"; } > "$log"
if ! git apply --check "$patch" >> "$log" 2>&1; then echo "$name: the patch does not apply"; exit 2; fi
git apply "$patch"
echo "cargo test --locked --no-run $crates" >> "$log"
cargo test --locked --no-run $crates >> "$log" 2>&1; build=$?
echo "BUILD_EXIT=$build" >> "$log"
tests="not run"
if [ $build -eq 0 ]; then
echo "cargo test --locked --no-fail-fast $crates" >> "$log"
cargo test --locked --no-fail-fast $crates >> "$log" 2>&1; tests=$?
echo "TEST_EXIT=$tests" >> "$log"
fi
git apply -R "$patch"
if [ -n "$(git status --porcelain --ignore-submodules=none)" ]; then echo "$name: the tree is dirty after"; exit 2; fi
red=$(grep -c '^test .* \.\.\. FAILED$' "$log")
echo "$name build=$build tests=$tests failed=$red"
done
echo "all restored: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ') dirty paths"m00-the-whole-source-change-reverted-onto-the-base
m01-the-shard-takes-a-datagram-ip-did-notdiff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index 9a3a7d90c..a58644f1f 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -355,7 +355,7 @@ impl Shard {
});
Offer::Taken
}
- Ok(Sent::Pending(next_hop)) => Offer::Waits(next_hop),
+ Ok(Sent::Pending(_)) => Offer::Taken,
// Counted by [ip], which tells the datagram's flow.
Err(_) => Offer::Taken,
});m02-a-waiting-datagram-holds-back-the-senders-later-onesdiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 8a3901b85..a6fe52ead 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -753,13 +753,19 @@ impl Udp {
}
};
let mut taken = None;
- for (at, queued) in queue.iter_mut().enumerate().filter(|(_, queued)| queued.waits.is_none()) {
+ for (at, queued) in queue.iter_mut().enumerate() {
+ if queued.waits.is_some() {
+ break;
+ }
match offer(&queued.out()) {
Offer::Taken => {
taken = Some(at);
break;
}
- Offer::Waits(next_hop) => queued.waits = Some(next_hop),
+ Offer::Waits(next_hop) => {
+ queued.waits = Some(next_hop);
+ break;
+ }
}
}
let taken = taken.and_then(|at| queue.remove(at)).is_some();m03-a-resolved-hop-wakes-nothingdiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..b66df2dec 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -804,7 +804,7 @@ impl Udp {
/// `next_hop`'s link address is known: the datagrams that wait for it are offered again.
pub fn wake(&mut self, next_hop: Ipv4Addr) {
- self.rouse(|waits| waits == next_hop);
+ let _ = next_hop;
}
/// The routes changed: every waiting datagram's next hop is asked for again.m04-a-failed-hop-drops-nothingdiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..fc8dc3305 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -792,7 +792,7 @@ impl Udp {
let queues = self.slots.iter_mut().filter_map(|slot| slot.socket.as_mut()).map(|socket| &mut socket.tx).chain([&mut self.closed]);
for queue in queues {
queue.retain(|queued| {
- let waited = queued.waits == Some(next_hop);
+ let waited = queued.waits == Some(next_hop) && queued.waits.is_none();
if waited {
lost.push(queued.flow());
}m06-a-dropped-datagrams-connected-socket-is-not-tolddiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 8a3901b85..dc6e8e1ed 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -788,9 +788,6 @@ impl Udp {
});
}
self.counters.add(Counter::TxUnreachable, u64::try_from(lost.len()).unwrap_or(u64::MAX));
- for flow in &lost {
- self.unreachable(flow);
- }
}
/// `next_hop`'s link address is known: the datagrams that wait for it are offered again.m07-a-socket-that-is-not-connected-is-tolddiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 8a3901b85..39fcd5de1 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -719,7 +719,7 @@ impl Udp {
/// A datagram of `flow` this crate had accepted will not leave: [ip] refused it as it left,
/// or gave up the next hop it waited for.
pub fn unreachable(&mut self, flow: &Flow) {
- if let Some(socket) = self.connected(flow) {
+ if let Some(socket) = self.ports.get(&flow.source_port).copied().and_then(|index| self.at(index)) {
socket.pending = Some(SocketError::NextHopFailed);
}
}m08-the-shard-does-not-wake-udp-on-a-resolved-hopdiff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index c47e74c1f..d65dd0eec 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -411,7 +411,6 @@ impl Shard {
toyos_net_ip::Event::Unreachable(flow) => udp.unreachable(&flow),
toyos_net_ip::Event::Resolved { next_hop, .. } => {
wake(tcp, waiting, Wait::Hop(next_hop));
- udp.wake(next_hop);
}
toyos_net_ip::Event::Failed { next_hop, .. } => {
wake(tcp, waiting, Wait::Hop(next_hop));m09-the-shard-does-not-tell-udp-of-a-failed-hopdiff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index c47e74c1f..2062d2ec0 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -415,7 +415,6 @@ impl Shard {
}
toyos_net_ip::Event::Failed { next_hop, .. } => {
wake(tcp, waiting, Wait::Hop(next_hop));
- udp.fail(next_hop);
}
// No datagram waits on a failed next hop: the failure dropped them.
toyos_net_ip::Event::Cleared { next_hop, .. } => wake(tcp, waiting, Wait::Hop(next_hop)),m10-a-change-of-routes-wakes-no-datagramdiff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index c47e74c1f..1f81371e6 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -435,7 +435,6 @@ impl Shard {
self.routes = ip.generation();
waiting.clear();
tcp.wake_all();
- udp.wake_all();
}
let head = self.round.front().map(|m| m.flow);
let gone: BTreeSet<Flow> = tcp.drain_gone().map(Flow::Tcp).chain(udp.drain_gone().map(Flow::Udp)).collect();m11-a-resolved-hop-wakes-every-waiting-datagramdiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..4498e8d57 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -804,7 +804,8 @@ impl Udp {
/// `next_hop`'s link address is known: the datagrams that wait for it are offered again.
pub fn wake(&mut self, next_hop: Ipv4Addr) {
- self.rouse(|waits| waits == next_hop);
+ let _ = next_hop;
+ self.rouse(|_| true);
}
/// The routes changed: every waiting datagram's next hop is asked for again.m12-a-queued-datagram-carries-no-permission-of-its-calldiff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 0454418c8..ddb1c50da 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -212,7 +212,7 @@ impl Queued {
source: self.source,
destination: self.destination,
ttl: self.ttl,
- broadcast: self.broadcast,
+ broadcast: true,
datagram: UdpBuilder { source: self.source_port, destination: self.port, data: &self.payload },
}
}m13-ip-has-a-datagram-wait-for-a-hop-it-has-given-updiff --git a/toyos-net-ip/src/egress.rs b/toyos-net-ip/src/egress.rs
index dd91fe40f..74a93851f 100644
--- a/toyos-net-ip/src/egress.rs
+++ b/toyos-net-ip/src/egress.rs
@@ -382,7 +382,7 @@ fn hop(i: &mut Interface, cx: &mut Cx<'_>, route: &Route, destination: Ipv4Addr,
NextHop::Neighbour(addr) => match nud::send(i, cx, addr, Some(route.source)) {
Link::Resolved(mac) => Ok(Hop::To(mac)),
Link::Pending => Ok(Hop::Asked(addr)),
- Link::Failed(refusal) => Err(refusal),
+ Link::Failed(_) => Ok(Hop::Asked(addr)),
},
}
}m14-ip-tells-no-flow-of-its-refusaldiff --git a/toyos-net-ip/src/egress.rs b/toyos-net-ip/src/egress.rs
index dd91fe40f..579684d64 100644
--- a/toyos-net-ip/src/egress.rs
+++ b/toyos-net-ip/src/egress.rs
@@ -326,9 +326,7 @@ impl Ip {
Hop::Asked(next_hop) => Ok(Sent::Pending(next_hop)),
}
});
- if sent.is_err() {
- self.log.event(Event::Unreachable(flow));
- }
+ let _ = flow;
sent
}
m15-the-node-carries-no-lookup-on-in-transmitdiff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs
index 4a68d7b9b..8d8c2c0d4 100644
--- a/userland/netstack/node/src/lib.rs
+++ b/userland/netstack/node/src/lib.rs
@@ -158,7 +158,8 @@ impl Node {
sent = sent.saturating_add(self.stack.transmit(now, credit.saturating_sub(sent), &mut sink));
// A datagram [ip] refused as it left is a line of this opportunity.
self.log();
- let asked = self.resolver.pass(now, &mut self.stack, &mut self.counters, &mut draw);
+ let asked = false;
+ let _ = &mut draw;
if !asked || sent >= credit {
break;
}m16-the-query-asked-in-a-dropped-ones-place-waits-for-the-next-opportunitydiff --git a/userland/netstack/node/src/lib.rs b/userland/netstack/node/src/lib.rs
index 4a68d7b9b..7faa8821e 100644
--- a/userland/netstack/node/src/lib.rs
+++ b/userland/netstack/node/src/lib.rs
@@ -159,7 +159,7 @@ impl Node {
// A datagram [ip] refused as it left is a line of this opportunity.
self.log();
let asked = self.resolver.pass(now, &mut self.stack, &mut self.counters, &mut draw);
- if !asked || sent >= credit {
+ if asked || !asked {
break;
}
}m17-ips-queue-for-a-next-hop-is-16diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 9cbee3f78..59be88ccf 100644
--- a/toyos-net-ip/src/lib.rs
+++ b/toyos-net-ip/src/lib.rs
@@ -108,7 +108,7 @@ pub mod limits {
pub const FAILED_HOLD: Duration = Duration::from_secs(20);
pub const LOCKTIME: Duration = Duration::from_secs(1);
pub const IDLE_LIFETIME: Duration = Duration::from_secs(600);
- pub const PENDING_PER_NEIGHBOUR: usize = 8;
+ pub const PENDING_PER_NEIGHBOUR: usize = 16;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}m18-what-waits-is-not-counted-against-the-sockets-queuediff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index 8a3901b85..1731ca232 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -567,7 +567,7 @@ impl Udp {
Err(refusal) => return self.refuse(route_refusal(refusal), me, peer),
};
let socket = self.socket(id)?;
- if socket.tx.len() >= limits::TX_DATAGRAMS {
+ if socket.tx.iter().filter(|queued| queued.waits.is_none()).count() >= limits::TX_DATAGRAMS {
return self.refuse(Counter::TxQueueFull, me, peer);
}
let ttl = if destination.is_multicast() { multicast_ttl } else { ttl };m19-ip-tells-the-flow-of-a-datagram-it-did-not-takediff --git a/toyos-net-ip/src/egress.rs b/toyos-net-ip/src/egress.rs
index dd91fe40f..b91903fec 100644
--- a/toyos-net-ip/src/egress.rs
+++ b/toyos-net-ip/src/egress.rs
@@ -326,7 +326,7 @@ impl Ip {
Hop::Asked(next_hop) => Ok(Sent::Pending(next_hop)),
}
});
- if sent.is_err() {
+ if sent.is_err() || matches!(sent, Ok(Sent::Pending(_))) {
self.log.event(Event::Unreachable(flow));
}
sentm20-ip-holds-its-own-messages-without-the-interfaces-bounddiff --git a/toyos-net-ip/src/egress.rs b/toyos-net-ip/src/egress.rs
index dd91fe40f..3a959718d 100644
--- a/toyos-net-ip/src/egress.rs
+++ b/toyos-net-ip/src/egress.rs
@@ -341,9 +341,7 @@ impl Ip {
let Some((i, mut cx)) = self.split(now, route.iface) else { return };
if let NextHop::Neighbour(addr) = route.next_hop {
let holds = matches!(i.neighbours.get(&addr).map(|n| &n.state), None | Some(Nud::Incomplete(_)));
- if holds && i.held >= PENDING_TOTAL {
- return cx.log.count(Counter::NbPendingFull);
- }
+ let _ = (holds, PENDING_TOTAL);
}
let Ok(hop) = hop(i, &mut cx, route, builder.destination, false) else { return };
let (to, held_for) = match hop {m21-ip-holds-none-of-its-own-messagesdiff --git a/toyos-net-ip/src/egress.rs b/toyos-net-ip/src/egress.rs
index dd91fe40f..658ffa03f 100644
--- a/toyos-net-ip/src/egress.rs
+++ b/toyos-net-ip/src/egress.rs
@@ -352,7 +352,7 @@ impl Ip {
};
let Some(frame) = build_vec(i.mac, to, builder) else { return };
match held_for {
- Some(next_hop) => nud::hold(i, &mut cx, next_hop, Held { frame, kind }),
+ Some(next_hop) => drop((next_hop, Held { frame, kind })),
None => {
cx.control.push(Item::Frame { iface: route.iface, frame, kind }, cx.log);
}m22-a-failed-hop-counts-no-message-it-droppeddiff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs
index dc11c25bc..7b2eb0aad 100644
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -498,7 +498,6 @@ fn dropped(cx: &mut Cx<'_>, counter: Counter, held: usize) {
fn fail(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, pending: Pending) {
let now = cx.now;
i.held = i.held.saturating_sub(pending.0.len());
- dropped(cx, Counter::NbPendingDropped, pending.0.len());
cx.log.count(Counter::NbFailed);
cx.log.event(Event::Failed { iface: cx.iface, next_hop: addr });
cx.timers.arm(timer(cx, addr), now.after(FAILED_HOLD));m23-a-link-that-goes-down-counts-no-message-it-droppeddiff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs
index dc11c25bc..5d0112324 100644
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -619,7 +619,7 @@ pub(crate) fn flush(i: &mut Interface, cx: &mut Cx<'_>) {
Nud::Incomplete(s) => s.pending.0,
mut state => state.take_released().0,
};
- dropped(cx, Counter::NbPendingDropped, held.len());
+ drop(held);
}
cx.control.purge(cx.iface);
i.held = 0;m24-eviction-takes-an-entry-with-released-messages-as-any-otherdiff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs
index dc11c25bc..f9bb8976c 100644
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -322,7 +322,7 @@ fn evictable(n: &Neighbour) -> Option<u8> {
Nud::Stale(_) => 2,
_ => return None,
};
- Some(if n.state.releasing() { 3 } else { class })
+ Some(class)
}
/// An entry became one eviction takes, or the table emptied: a send refused since room lastm25-an-idle-lifetime-deletes-an-entry-with-released-messagesdiff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs
index dc11c25bc..419fc9d95 100644
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -446,9 +446,6 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) {
// A request is pending, so it is not idle: its next deadline starts as the request leaves.
Nud::Unreachable(Unreachable { solicit: Solicit::Queued, .. }) => {}
// Not idle while released datagrams are its: `leave` deletes it once they have left.
- Nud::Stale(Stale { lifetime, .. }) | Nud::Unreachable(Unreachable { solicit: Solicit::Quiescent(lifetime), .. }) if releasing => {
- *lifetime = Lifetime::Passed;
- }
Nud::Stale(_) | Nud::Unreachable(_) | Nud::Failed => {
remove(i, cx, addr);
route::refresh_active(i, cx);m26-a-full-queue-drops-the-newestdiff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs
index dc11c25bc..08bb98800 100644
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -63,9 +63,11 @@ impl Pending {
/// Takes `held`, handing back the oldest when PENDING_PER_NEIGHBOUR wait already (RFC 4861
/// §7.2.2).
pub fn push(&mut self, held: Held) -> Option<Held> {
- let oldest = if self.0.len() >= PENDING_PER_NEIGHBOUR { self.0.pop_front() } else { None };
+ if self.0.len() >= PENDING_PER_NEIGHBOUR {
+ return Some(held);
+ }
self.0.push_back(held);
- oldest
+ None
}
}
m27-released-messages-leave-newest-firstdiff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs
index dc11c25bc..98afa7d57 100644
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -80,7 +80,7 @@ impl Released {
}
pub fn pop(&mut self) -> Option<Held> {
- self.0.pop_front()
+ self.0.pop_back()
}
}
m28-an-unreachable-entry-whose-request-waits-is-idlediff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs
index dc11c25bc..8f37ce50b 100644
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -443,8 +443,6 @@ pub(crate) fn fire(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) {
*solicit = Solicit::Quiescent(Lifetime::Runs);
cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME));
}
- // A request is pending, so it is not idle: its next deadline starts as the request leaves.
- Nud::Unreachable(Unreachable { solicit: Solicit::Queued, .. }) => {}
// Not idle while released datagrams are its: `leave` deletes it once they have left.
Nud::Stale(Stale { lifetime, .. }) | Nud::Unreachable(Unreachable { solicit: Solicit::Quiescent(lifetime), .. }) if releasing => {
*lifetime = Lifetime::Passed;m29-a-released-message-leaves-to-the-link-address-it-was-built-fordiff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs
index dc11c25bc..76087d61e 100644
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -466,9 +466,7 @@ pub(crate) fn leave(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) -> Optio
let n = i.neighbours.get_mut(&addr)?;
let mac = n.state.mac()?;
let mut held = n.state.released_mut()?.pop()?;
- if let Some((destination, _)) = held.frame.split_first_chunk_mut::<6>() {
- *destination = mac.0;
- }
+ let _ = mac;
let idle = matches!(
n.state,
Nud::Stale(Stale { lifetime: Lifetime::Passed, .. }) | Nud::Unreachable(Unreachable { solicit: Solicit::Quiescent(Lifetime::Passed), .. })m30-a-failed-hop-is-not-reporteddiff --git a/toyos-net-ip/src/nud.rs b/toyos-net-ip/src/nud.rs
index dc11c25bc..28a317e84 100644
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -500,7 +500,6 @@ fn fail(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr, pending: Pending) {
i.held = i.held.saturating_sub(pending.0.len());
dropped(cx, Counter::NbPendingDropped, pending.0.len());
cx.log.count(Counter::NbFailed);
- cx.log.event(Event::Failed { iface: cx.iface, next_hop: addr });
cx.timers.arm(timer(cx, addr), now.after(FAILED_HOLD));
route::refresh_active(i, cx);
} |
|
Review, round 2, of Net lines, Round 1's BLOCKERs
Round 1's NOTEsAll four closed: the per-send sum is gone and the product is a BLOCKER
NOTE
Checked, no finding
#791 and #793
At the head that landsThe fix adds a test, so the shard's suite and SEND BACK |
…the test that holds it as built The track's stage-5 line said a datagram that waited, whose link goes down and returns with no opportunity between, waits for its next hop anew and leaves, and rested that on a probe that was on no record. The probe is now a_waiting_datagram_whose_link_returns_before_an_opportunity_waits_anew_and_leaves in toyos-net-shard/tests/udp.rs, beside US-24's shard test, which it is with the link's return moved ahead of the opportunity. It carries no scenario id: it holds a departure from NUD-23. The track's line names it, and both arms of its exit are stated by it: the first inverts its last assertion, the second keeps the test as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Round 3, head The BLOCKER. The probe is committed as The merge of #791 is still owed.
Each log opens with the head and its command and ends with its exit. Mutation m31, a change of routes wakes no datagram, by round 2's script: diff --git a/toyos-net-shard/src/lib.rs b/toyos-net-shard/src/lib.rs
index 9a3a7d90c..0960885c9 100644
--- a/toyos-net-shard/src/lib.rs
+++ b/toyos-net-shard/src/lib.rs
@@ -423,7 +423,6 @@ impl Shard {
self.routes = ip.generation();
waiting.clear();
tcp.wake_all();
- udp.wake_all();
}
let head = self.round.front().map(|m| m.flow);
let gone: BTreeSet<Flow> = tcp.drain_gone().map(Flow::Tcp).chain(udp.drain_gone().map(Flow::Udp)).collect(); |
…gram senders Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Review, round 3, of Net lines, Round 2's BLOCKER
The merge
The rows the merged head owedEach log opens with
No mutation re-run at the merged headAccepted. The thirty-one posted patches change six files: BLOCKER
NOTE
Guest suiteUnchanged: no shipped program links a crate this branch touches, and what the merge brought to netstack is At the head that lands
LAND |
|
CI at |
… name's probing Two conflicts. The node's module header, **Draws**: both orders stand, the name after the client and before the lookups in `settle`, the lookups alone in `Node::transmit`, which takes a draw and serves no name. The track's list of what the node does not yet meet: this branch's line on the name, and #792's two lines on a socket's waiting datagrams in place of the two it replaced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Stage "datagram senders" of the move to ToyOS's own stack: the two lines
issues/toyos-has-its-own-network-stack.mdkept from #781, the resolver's burst and the report read late, are closed here.toyos-net-udp,toyos-net-ip,toyos-net-shardandtoyos-net-nodeship in nothing, so every test is a host test and no guest test reaches the change: what ships linkstoyos-dnsandtoyos-mdns, neither of which this touches.Head
554bac968, the merge oforigin/mainat975d5702a(#791) into9afecafb4, the head round 3 reviewed. The merge was clean: three files both sides changed merged without a conflict,issues/toyos-has-its-own-network-stack.md,userland/netstack/node/tests/lease.rsanduserland/netstack/node/tests/listeners.rs. Of the stack's sources it moved one, the node'slisteners.rs, which is #791's alone; nothing oftoyos-net-udp,toyos-net-ip,toyos-net-shardortoyos-dhcpmoved. #791's three listener tests run on this branch's shard here for the first time and are green, each... okinr4-suite-toyos-net-node.log:a_handshake_reset_before_it_ends_leaves_the_next_connection_its_listeners_option,a_listener_holds_the_option_its_listen_named_from_its_first_connectionandan_accept_answers_the_option_of_a_stream_its_own_pass_let_go.What changed, per decision
1. A datagram waits for its next hop in its own sender, and [ip] takes none it cannot frame.
Ip::send_udpanswers in one call:Sent::Frame, a refusal it counts and reports to the flow as it always did, orSent::Pending(next_hop)for a datagram it did not take, whose next hop it has asked for. It holds nothing of a transport's.Udp::serve(sender, offer)offers a sender's datagrams in the order it accepted them, andofferanswers eachOffer::TakenorOffer::Waits(next_hop). One that waits stays where it is, marked with its hop, and is passed over until that hop is named again.PendingisWaits, everything else isTaken. It asks no route and peeks no neighbour for a datagram, and a datagram's route is looked up once as it leaves. [tcp]'shopreads as it did onmain.2. A failed hop drops what waited for it and counts it (the second decision; US-27).
Udp::fail(next_hop), called on [ip]'sEvent::Failed, drops every datagram that waited for that hop, in a socket or in the closed sender, in the call that reports the failure. That is the one drop [ip] cannot make, and the only thingudp.tx-unreachablecounts. A connected socket whose datagram it was hearsSocketError::NextHopFailedonce; no other socket is told. Nothing is kept: a host that answers after the hold-down brings out no datagram of before.main: [ip]'s counter (route.*,nb.failed-refused,nb.table-full),Event::Unreachable,Udp::unreachable. There is one path to it. Round 1'sOffer::Unreachable,lostinserveand the shard's two arms are deleted.send_udp, which is the calls_ip_nud_022_a_full_table_evicts_in_ordermakes, and that test readsnb.table-full= 1 for one datagram refused. No variant is kept for it.3. [ip]'s queue for a next hop holds [ip]'s own messages alone, and the types say so.
Sent::Held,Held.flow,FrameKind::Datagram, theEvent::Unreachablea dropped held datagram raised, and the interface's bound refusing a transport's datagram. What is left holds an echo reply or an ICMP error. The shard'sunreachable!went with the state it guarded.Event::Failedin [ip]'s tests, ands_ip_icd_012_shard_a_resolution_failure_reaches_the_connected_socketthrough the shard. The bound (NUD-04, NUD-05): the socket's own queue, US-25's two tests. The link going down (NUD-23): US-24's shard test. The notices NUD-05, NUD-23 and NUD-29 expect for a dropped datagram are gone, since nobody is told of a message of [ip]'s own; the track records the departure by scenario id.4. The DHCP client's socket has no rule of its own (the third decision). Its broadcasts are never asked to wait. Its unicast renewal waits in the client's socket like any datagram and is dropped when [ip] gives the server up; the socket is not connected, so the client hears nothing, keeps its lease and asks again on its own clock.
5. The broadcast permission stays on the queued datagram, and [ip] reads the route of the moment it leaves: a datagram that waited for a host which a renewal has since made a prefix's broadcast address leaves in a link broadcast only if its socket held the permission at the call.
6.
PENDING_PER_NEIGHBOURis 8 again,s_ip_nud_004_overflow_drops_the_oldestis the scenario's ten again, now of [ip]'s own messages, and the resolver's compile-time assertion is deleted.7.
Node::transmittakes the draws and carries the lookups on. A query for a resolver [ip] holds failed is accepted and refused by [ip] at its turn, inside a transmit opportunity. The node reads that in the same opportunity, and sends the query asked in its place in it while credit lasts. This is the first of the two exits the track offered for the late report; the second, a refusal in the call, would have answered a client's unconnected socket with an error, which the second decision rules out.8.
limits::TX_BYTESis a constant no send reaches. 16 datagrams of at most 1,472 bytes are 23,552, under its 65,536. The count kept beside the queue, and the per-send sum round 1 put in its place, are deleted; aconstassertion says the product fits, so a larger MTU or queue that made the bound bind fails the build.Bounds
limits::TX_DATAGRAMS, 16 a socket, refused in the call past it asudp.tx-queue-full; andlimits::CLOSED_DATAGRAMS, 16 for every closed socket together. Neither is new and nothing was added beside them:Queuedgains oneOption<Ipv4Addr>and a port.Udp::wakeandUdp::failwalk every socket's queue once per event; [ip] makes such an event only for a next hop the machine itself asked for.s_ip_nud_005_the_interface_holds_at_most_64, m20), and a transport's datagram no longer counts against either.What the move must still do with this
Node::transmitits draws. The track's stage 5 paragraph says so.udp.tx-unreachablewherever it shows [udp]'s counters.Refused::ResourceExhausted, which the shell already maps.Outside the first brief's files
toyos-dhcp/tests/common/mod.rs, one line: its harness callsUdp::serve, whose closure now answers anOffer.toyos-net-ip/src/egress.rs,nud.rsand [ip]'s scenario tests, by the round-2 brief, for the third decision. Beside themiface.rsandlib.rs, comments the deletion made false.What is left, on the track
s_udp_us_025_what_waits_for_a_next_hop_is_bounded_by_its_sockets_queueholds it as it stands. Owner: the worker of the move; due by the move's first run on the T14 or in a guest.a_waiting_datagram_whose_link_returns_before_an_opportunity_waits_anew_and_leaves(toyos-net-shard/tests/udp.rs, beside US-24's shard test, under no scenario id since it holds a departure from NUD-23) holds it as it stands: after link down and up, the opportunity carries the address's announcement and an ARP request for the hop,route.no-source-addressandudp.tx-unreachablestay 0, and the datagram leaves after the reply. The track's line names the test. Owner: the worker of the move; closed before the move, by that test with its last assertion inverted, finding the datagram gone, or by the specifications having it wait and the test standing as it is.Checks (devices and a trust boundary)
toyos-net-wire. This round adds nounreachable!and removes one. The crates forbid indexing, arithmetic side effects,unwrap,expectandpanicoutside tests, and clippy is green.Sent::Pending, which [ip] answers only whennud::sendanswered that the entry is INCOMPLETE, new or already there, with its request queued. INCOMPLETE ends inEvent::Resolved,Event::Failed, or a link going down, which bumps the routing generation:Udp::wake,Udp::fail,Udp::wake_all. These are the three ends the round-1 review checked; what changed is that one call of [ip]'s makes the answer where two adjacent ones did.etherparse. The scenarios are the readers': US-21, US-24 to US-28, US-53, and the NUD, OUT, NBR and ICD ids above.86e685857and m31 at9afecafb4, whose sources are the same. None was run again at the merged head: the merge moved one source file of these crates, the node'slisteners.rs, which no mutation patches. Each is a checked patch applied to the clean head, built, run withcargo test --locked --no-fail-fast -p toyos-net-udp -p toyos-net-ip -p toyos-net-shard -p toyos-net-node -p toyos-dhcp, and reversed by one script that refuses a dirty tree before and after. Patches, script and the run's summary are in the round-2 mutations comment, m31's in the round-3 comment. Every one built (exit 0) and turned the suites red (exit 101). None stayed green.Offer,Sent::Pending,udp.tx-unreachableand the draws. It claims no behaviour. m01 is the control for the central decision.a_resolved_next_hop_wakes_the_senders_that_waited_for_it_and_no_others_ip_nud_004_overflow_drops_the_oldesta_resolved_next_hop_wakes_the_senders_that_waited_for_it_and_no_otherreads the round, which is private: that a waiting sender costs nothing until its own hop changes shows on no wire. No other test is red on m11.The track's exits, as met
a_burst_at_a_resolver_not_yet_resolved_is_on_the_wire_once_the_resolver_answers_arp, which also finds [ip] holding none. Its neighbour isa_clients_datagrams_behind_the_burst_wait_in_their_own_socket_and_take_no_querys_place: sixteen datagrams of a client's behind the sixteen queries, all thirty-two on the wire.a_query_for_a_resolver_ip_has_given_up_ends_its_lookup_in_the_opportunity_that_would_have_carried_it, whose premise is now [ip]'snb.failed-refused.Gates
At
554bac968, the head:cargo run -- --ci host(Host: 78 step(s), all green;clippy, warnings denied: clean)r4-ci-host.logcargo run -- --build-onlyr4-build-only.logcargo test --locked -p toyos-net-shard(57 passed, 0 failed)r4-suite-toyos-net-shard.logcargo test --locked -p toyos-net-node(147 passed, 0 failed: round 3's 144 and #791's three)r4-suite-toyos-net-node.logAt
9afecafb4, which the head's merge follows:cargo run -- --ci host(Host: 78 step(s), all green;clippy, warnings denied: clean)r3-ci-host.logcargo test --locked -p toyos-net-shard(57 passed, 0 failed;a_waiting_datagram_whose_link_returns_before_an_opportunity_waits_anew_and_leaves ... ok)r3-suite-toyos-net-shard.logcargo test --locked -p toyos-net-node(144 passed, 0 failed)r3-suite-toyos-net-node.logAt
86e685857, from which the head differs by one test file and the track:cargo run -- --ci host(Host: 78 step(s), all green;clippy, warnings denied: clean)r2-ci-host.logcargo run -- --build-onlyr2-build-only.logcargo test --locked -p toyos-net-udp(66 passed, 0 failed)r2-suite-toyos-net-udp.logcargo test --locked -p toyos-net-ip(273 passed, 0 failed)r2-suite-toyos-net-ip.logcargo test --locked -p toyos-net-shard(56 passed, 0 failed)r2-suite-toyos-net-shard.logcargo test --locked -p toyos-net-node(144 passed, 0 failed)r2-suite-toyos-net-node.logcargo test --locked -p toyos-dhcp(76 passed, 0 failed)r2-suite-toyos-dhcp.logcargo clippy --locked -p toyos-net-udp -p toyos-net-ip -p toyos-net-shard -p toyos-net-node -p toyos-dhcp -p toyos-dns --all-targets --keep-going -- <src/clippy.rs's ADOPTED as -W> -D warningsr2-clippy.log, and the same lint insider2-ci-host.logcargo test --locked --lib -- hostws:: userlandhost:: sourcegate:: licence::from the root package (45 passed)r2-sourcegates.logEach log opens with the head and its command and ends with its exit code, in the orchestrator's scratchpad under the stage's
logs/; each mutation's is underlogs/r2-mutations/andlogs/r3-mutations/. Each round's were written by one script run after its commit.Not run: any guest test, as nothing that ships changed; anything on metal.
Growth
git diff --shortstat origin/main...554bac968: 28 files, +891 -272. Sources: +308 -167, of which the shard's unit test is +26:toyos-net-udp/src/lib.rs+132 -48,toyos-net-ip/src+84 -85,toyos-net-shard/src/lib.rs+51 -9, the node's sources +41 -25. Tests: +578 -101. The track: +5 -4 lines.Round 2 by itself,
git diff --shortstat 05e3dbaad 86e685857over the stack's sources, its tests and the track, which leaves out the merge's six files: 18 files, +241 -270. Sources +135 -168; tests +101 -97; the track +5 -5.Round 3 by itself,
git diff --shortstat 86e685857 9afecafb4: 2 files, +19 -1: the test +18, the track's line +1 -1.[udp] grows by the offer, the mark,
wake,failand their documentation. [ip] is a line smaller than onmain.Unsure of
recv_froma query. That is what reading the report where it is made costs; with no lookup it costs nothing.main, where [tcp]'s flow waits forEvent::Room. A datagram could wait for room the same way; nothing here does, and no scenario asks it.udp.txstill counts a datagram [ip] refused as it left, as onmain:servecounts what it handed over.🤖 Generated with Claude Code
https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A