Repository navigation
toyos-net-node: a query known not to have reached its resolver is not waited out, an ICMP error ends nothing, and a burst of lookups reaches the wire - #781
Conversation
…ookups waits whole for its resolver's link address Two lines of the network stack's track, each "before the move". A query that did not reach its resolver was waited out for WAIT_MS like one nobody answered. toyos_dns::Lookup has a word for it now, on_unreached: the query is let go and the next server asked at once, and once every server has been asked in turn and none reached the lookup ends Failure::Unreachable rather than repeat a query with no interval (RFC 1035 §4.2.1). The node's resolver says it for a query [udp] refuses in the call (no route) and for one the network reports to the query's connected socket ([ip] gave its next hop up, or ICMP refused or prohibited it). A lookup none of whose resolvers could be sent a query has started and ended in Node::resolve. A lookup behind one silent resolver ends at 3 s, [ip]'s report, where it took ROUNDS x WAIT_MS; behind an unrouted first resolver it is answered at the millisecond it started, where it took a WAIT_MS. The shipped netstack never says the word (smoltcp reports nothing to it), so its match on Failure gains an arm that cannot be reached and says so. Lookups started together at a resolver not yet resolved lost all but the newest eight queries to [ip]'s queue for that next hop. The queue holds 16, and the node's resolver asserts at compile time that this is no fewer than MAX_LOOKUPS; the interface's bound of 64 held datagrams is unchanged. Holding the queries back in the resolver until the neighbour resolves needs the next-hop question [tcp] asks, which the node's stack does not hand on: that is lease.rs and the shard, outside this stage. NUD-04's test reads the constant. What is left is on the track: a query [ip] refuses at its transmit opportunity is read at the node's next wake, and a client's datagram to the same next hop shares the queue. 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
#!/bin/sh
# Each mutation: a checked patch applied to the clean committed tree, shown to
# build (or not), run, and reversed. Logs: $S/logs/<mutation>-builds.log and
# $S/logs/<mutation>.log, the command first and the exit code last.
W=<worktree>
S=<scratchpad>/orch/ownstack/resolver
P="-p toyos-dns -p toyos-net-node -p toyos-net-ip"
cd "$W" || exit 2
echo "HEAD=$(git rev-parse HEAD)"
clean() { [ -z "$(git status --porcelain --ignore-submodules=none)" ]; }
clean || { echo "the worktree is not clean; nothing run"; exit 2; }
for patch in "$S"/mutations/${1:-m}*.patch; do
name=$(basename "$patch" .patch)
git apply --check "$patch" || { echo "$name does not apply"; exit 2; }
git apply "$patch"
# shellcheck disable=SC2086
{ echo "\$ cargo test --locked --no-run $P"; echo "HEAD=$(git rev-parse HEAD) + $name"; cargo test --locked --no-run $P; echo "EXIT=$?"; } > "$S/logs/$name-builds.log" 2>&1
built=$(tail -1 "$S/logs/$name-builds.log")
if [ "$built" = "EXIT=0" ]; then
# shellcheck disable=SC2086
{ echo "\$ cargo test --locked --no-fail-fast $P"; echo "HEAD=$(git rev-parse HEAD) + $name"; cargo test --locked --no-fail-fast $P; echo "EXIT=$?"; } > "$S/logs/$name.log" 2>&1
ran=$(tail -1 "$S/logs/$name.log")
else
ran="not run"
fi
git apply -R "$patch"
clean || { echo "$name left the tree dirty"; exit 2; }
echo "$name builds $built tests $ran"
done
echo "HEAD=$(git rev-parse HEAD) clean=$(clean && echo yes || echo no)"
echo "MUTATIONS DONE"
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 2ed5ceb41..1f46cdaa9 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -431,12 +431,8 @@ pub enum Failure {
NoSuchName,
/// The name exists and has no `A` record (RFC 2308 §2.2).
NoAddress,
- /// No server answered, and a query that may have reached one was given
- /// its [`WAIT_MS`].
+ /// Every query went unanswered for [`WAIT_MS`].
TimedOut,
- /// Every server was asked in turn and no query reached one
- /// ([`Lookup::on_unreached`]): there is no answer to wait for.
- Unreachable,
/// The answer did not fit a UDP reply (TC).
Truncated,
/// Every server that answered could not answer, the last with this RCODE.
@@ -482,17 +478,6 @@ struct Sent {
/// any query this lookup sent for the name now asked is read, so an answer
/// late past its query's wait still ends the lookup; it is read only on the
/// query's own port, with the query's ID, from the server the query went to.
-///
-/// **A wait is for an answer that can come.** A query the caller reports did
-/// not reach its server ([`Lookup::on_unreached`]) is let go, and the next
-/// server is asked at once: RFC 1035 §4.2.1's interval of two to five seconds
-/// is between repetitions of a query that may have been lost on its way, and
-/// the same section has the other servers tried first. Once every server has
-/// been asked in turn and none was reached, the lookup ends with
-/// [`Failure::Unreachable`] and repeats nothing: asking a server again at
-/// once would be the retransmission without an interval the RFC warns of, and
-/// a wait would be for no answer. Whether such a report is true is the
-/// caller's to judge; this type takes its word.
#[derive(Debug)]
pub struct Lookup {
/// The name now asked: the one given, or the last alias followed.
@@ -501,16 +486,12 @@ pub struct Lookup {
aliases: usize,
servers: Vec<[u8; 4]>,
/// Queries sent for `name`, oldest first; one answered with a server
- /// failure, or reported not to have reached its server, is taken out.
- /// [`Lookup::waiting`] is this list.
+ /// failure is taken out. [`Lookup::waiting`] is this list.
sent: Vec<Sent>,
/// The [`Asked`] the next query gets: none is given twice in a lookup.
next: u32,
/// Queries sent for `name`, answered or not.
asked: usize,
- /// Queries in a row, the newest last, that did not reach their server.
- /// Once they are as many as the servers, nobody is left to ask at once.
- unreached: usize,
/// When the newest query is given up on.
due: u64,
/// The RCODE of the last server failure, which is what a lookup that runs
@@ -532,11 +513,10 @@ impl Lookup {
sent: Vec::new(),
next: 0,
asked: 0,
- unreached: 0,
due: now,
failed: None,
};
- let step = lookup.ask(0, now, id);
+ let step = lookup.ask(now, id);
Some((lookup, step))
}
@@ -551,16 +531,11 @@ impl Lookup {
self.sent.iter().map(|s| s.asked)
}
- /// The next query for `name`, the `unreached` before it in a row having
- /// reached no server; or the end, where every query has been sent or
- /// every server was just asked and none reached.
- fn ask(&mut self, unreached: usize, now: u64, id: impl FnOnce() -> u16) -> Step {
- self.unreached = unreached;
- let nobody = unreached == self.servers.len();
- if nobody || self.asked == ROUNDS * self.servers.len() {
+ /// The next query for `name`, or the end where every one has been sent.
+ fn ask(&mut self, now: u64, id: impl FnOnce() -> u16) -> Step {
+ if self.asked == ROUNDS * self.servers.len() {
return Step::Done(Err(match self.failed {
Some(rcode) => Failure::ServerFailed(rcode),
- None if nobody => Failure::Unreachable,
None => Failure::TimedOut,
}));
}
@@ -580,24 +555,7 @@ impl Lookup {
if now < self.due {
return Step::Wait;
}
- self.ask(0, now, id)
- }
-
- /// The query `asked` did not reach its server, the caller learnt at
- /// `now`: it was never sent, or the network reported it back. Its answer
- /// is read no more. `id` draws the ID of the query this sends.
- pub fn on_unreached(&mut self, asked: Asked, now: u64, id: impl FnOnce() -> u16) -> Step {
- let Some(which) = self.sent.iter().position(|s| s.asked == asked) else {
- return Step::Wait;
- };
- self.sent.remove(which);
- // The next server is asked now, unless a newer query is still
- // waiting for its own answer.
- if which == self.sent.len() {
- self.ask(self.unreached + 1, now, id)
- } else {
- Step::Wait
- }
+ self.ask(now, id)
}
/// `msg` arrived at `now` from `port` of `from`, on the port `asked` was
@@ -630,7 +588,7 @@ impl Lookup {
// The next server is asked now, unless a newer query is still
// waiting for its own answer.
if which == self.sent.len() {
- self.ask(0, now, id)
+ self.ask(now, id)
} else {
Step::Wait
}
@@ -647,7 +605,7 @@ impl Lookup {
self.sent.clear();
self.asked = 0;
self.failed = None;
- self.ask(0, now, id)
+ self.ask(now, id)
}
},
}
diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 59be88ccf..9cbee3f78 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 = 16;
+ pub const PENDING_PER_NEIGHBOUR: usize = 8;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 38ffa8148..d8a5cfc23 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -10,18 +10,9 @@
//! query other than the one whose socket it reached.
//! - **A socket lives as long as its query's answer is read**: it is closed when the lookup lets
//! the query go, ends, or is let go itself, and its port is free from then.
-//! - **A query that did not reach its resolver is not waited for**: the lookup is told, and asks
-//! its next resolver at once or ends (`toyos_dns::Lookup::on_unreached`). One [udp] refuses
-//! never left: it is counted and holds no socket. One the network reports back is counted and
-//! its socket closed: [ip] found no next hop for it, or a host or router refused it by ICMP.
-//! [udp] hands a connected socket only an error that quotes the socket's own addresses and
-//! ports, and of ICMP's only one that says refused or prohibited, so forging a report takes the
-//! query's port, and buys the lookup's early end and never an answer.
-//! - **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 query [udp] refuses never left**: it is counted, holds no socket, and the lookup waits
+//! its wait out as for any query nobody answered. So does one the network reports back, its
+//! resolver unreachable or its port refused: counted, and waited out.
//! - **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.
@@ -36,7 +27,6 @@ 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, SocketId};
use toyos_net_wire::Instant;
@@ -44,8 +34,6 @@ 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);
@@ -117,48 +105,42 @@ fn close(stack: &mut Stack, now: Instant, socket: SocketId) {
}
}
-/// What waits at a query's socket.
-enum Heard {
- /// A datagram of this length, now in the reply buffer.
- Reply(usize),
- /// The network's report that the query did not reach its resolver.
- Unreached,
-}
-
-/// The first of `queries` with something at its socket, and what: a reply is now in `reply`.
-fn waiting(queries: &[Query], stack: &mut Stack, reply: &mut [u8]) -> Option<(Query, Heard)> {
+/// The length of the next reply waiting at one of `queries`' sockets, now in `reply`, and the
+/// query it is for. An error the network reported against a query is counted and read past.
+fn waiting(queries: &[Query], stack: &mut Stack, reply: &mut [u8], counters: &mut Counters) -> Option<(Query, usize)> {
for query in queries {
- match stack.recv_from(query.socket, reply) {
- Ok(Some(received)) => return Some((*query, Heard::Reply(received.len))),
- Ok(None) => {}
- Err(Error::Failed(_)) => return Some((*query, Heard::Unreached)),
- Err(Error::NoSuchSocket | Error::Refused(_)) => unreachable!("a query's socket is open while it is listed, and no rule refuses a receive"),
+ loop {
+ match stack.recv_from(query.socket, reply) {
+ Ok(Some(received)) => return Some((*query, received.len)),
+ Ok(None) => break,
+ Err(Error::Failed(_)) => counters.add(Counter::QueryFailed, 1),
+ Err(Error::NoSuchSocket | Error::Refused(_)) => unreachable!("a query's socket is open while it is listed, and no rule refuses a receive"),
+ }
}
}
None
}
-/// 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.
-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 } => {
- // An any-address bind that names no port is refused only for want of a free one.
- let Ok((socket, _)) = stack.bind(Ipv4Addr::UNSPECIFIED, None, &mut *draw) else { break Some(Err(Ended::NoPort)) };
+/// Carries out `step` for `asking`, then closes the socket of every query the lookup no longer
+/// reads an answer for. Returns how the lookup ended, if it did.
+fn act(asking: &mut Asking, step: Step, now: Instant, stack: &mut Stack, counters: &mut Counters, draw: &mut impl FnMut() -> u32) -> Option<Result<Vec<[u8; 4]>, Ended>> {
+ let done = match step {
+ // An any-address bind that names no port is refused only for want of a free one.
+ Step::Ask { asked, to, query } => match stack.bind(Ipv4Addr::UNSPECIFIED, None, &mut *draw) {
+ Ok((socket, _)) => {
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 });
- break None;
+ } else {
+ counters.add(Counter::QueryUnsent, 1);
+ close(stack, now, socket);
}
- counters.add(Counter::QueryUnsent, 1);
- close(stack, now, socket);
- step = asking.lookup.on_unreached(asked, millis(now), || id(&mut *draw));
+ None
}
- Step::Wait => break None,
- Step::Done(result) => break Some(result.map_err(Ended::Failed)),
- }
+ Err(_) => Some(Err(Ended::NoPort)),
+ },
+ Step::Wait => None,
+ Step::Done(result) => Some(result.map_err(Ended::Failed)),
};
let lookup = &asking.lookup;
asking.queries.retain(|query| {
@@ -192,19 +174,16 @@ 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);
- if done == Some(Err(Ended::NoPort)) {
- return Err(NotStarted::ResourceExhausted);
- }
- self.next = self.next.wrapping_add(1);
- let started = asking.id;
- match done {
- None => self.asking.push(asking),
- // No resolver could be sent a query: the lookup has its id and its end at once, and
- // holds no socket.
- Some(result) => self.ended.push(Resolved { id: started, result }),
+ match act(&mut asking, step, now, stack, counters, &mut *draw) {
+ None => {
+ self.next = self.next.wrapping_add(1);
+ let started = asking.id;
+ self.asking.push(asking);
+ Ok(started)
+ }
+ Some(Err(Ended::NoPort)) => Err(NotStarted::ResourceExhausted),
+ Some(other) => unreachable!("a lookup's first step is a query, not {other:?}"),
}
- Ok(started)
}
/// Ends every lookup whose lease went or names other resolvers, reads every reply that
@@ -217,16 +196,11 @@ impl Resolver {
let named = stack.lease().is_some_and(|lease| lease.dns == asking.resolvers);
let mut done = if named { None } else { Some(Err(Ended::LeaseChanged)) };
while done.is_none() {
- let Some((query, heard)) = waiting(&asking.queries, stack, reply.as_mut_slice()) else { break };
- let step = match heard {
- // The socket is connected: [udp] delivered this from port 53 of the resolver
- // the query went to, or not at all.
- Heard::Reply(len) => asking.lookup.on_datagram(query.asked, query.to, PORT, reply.get(..len).unwrap_or_default(), ms, || id(&mut *draw)),
- Heard::Unreached => {
- counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_unreached(query.asked, ms, || id(&mut *draw))
- }
- };
+ let Some((query, len)) = waiting(&asking.queries, stack, reply.as_mut_slice(), counters) else { break };
+ let message = reply.get(..len).unwrap_or_default();
+ // The socket is connected: [udp] delivered this from port 53 of the resolver the
+ // query went to, or not at all.
+ let step = asking.lookup.on_datagram(query.asked, query.to, PORT, message, ms, || id(&mut *draw));
done = act(asking, step, now, stack, counters, &mut *draw);
}
if done.is_none() {
@@ -257,11 +231,9 @@ impl Resolver {
}
impl Node {
- /// Starts looking `name` up at the resolvers the held lease names, which spends two draws a
- /// query, its id and then its port: for the first query, and for each asked in its place
- /// because [udp] refused the one before. A call refused for want of a port has spent them;
- /// any other refused call spends none. A lookup no resolver could be sent a query of has
- /// started and ended: its end is in the next [`Self::take_resolved`].
+ /// Starts looking `name` up at the resolvers the held lease names, which spends two draws:
+ /// the first query's id, then its port. A call refused for want of a port has spent both;
+ /// any other refused call spends none.
pub fn resolve(&mut self, now: Instant, name: Name, mut draw: impl FnMut() -> u32) -> Result<LookupId, NotStarted> {
self.resolver.start(now, name, &mut self.stack, &mut self.counters, &mut draw)
}
diff --git a/userland/netstack/src/main.rs b/userland/netstack/src/main.rs
index 21c30f688..0bd5fcc9f 100644
--- a/userland/netstack/src/main.rs
+++ b/userland/netstack/src/main.rs
@@ -1550,9 +1550,6 @@ impl Netstack {
// whether the name or only its address is missing.
Err(Ended::Failed(Failure::NoSuchName | Failure::NoAddress)) => answer_lookup(&client, &[]),
Err(Ended::Failed(Failure::TimedOut)) => client.error(ERR_TIMED_OUT),
- Err(Ended::Failed(Failure::Unreachable)) => {
- unreachable!("netstack: no lookup here is told a query did not reach its server, and {name}'s ended so")
- }
Err(Ended::Failed(why @ (Failure::Truncated | Failure::ServerFailed(_) | Failure::TooManyAliases))) => {
say!("netstack: a lookup of {name} ended without an answer: {why:?}");
client.error(ERR_OTHER);
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 38ffa8148..11f2e156f 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -154,7 +154,7 @@ fn act(asking: &mut Asking, mut step: Step, now: Instant, stack: &mut Stack, cou
}
counters.add(Counter::QueryUnsent, 1);
close(stack, now, socket);
- step = asking.lookup.on_unreached(asked, millis(now), || id(&mut *draw));
+ break None;
}
Step::Wait => break None,
Step::Done(result) => break Some(result.map_err(Ended::Failed)),
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 38ffa8148..a5cb9ade8 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -224,7 +224,7 @@ impl Resolver {
Heard::Reply(len) => asking.lookup.on_datagram(query.asked, query.to, PORT, reply.get(..len).unwrap_or_default(), ms, || id(&mut *draw)),
Heard::Unreached => {
counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_unreached(query.asked, ms, || id(&mut *draw))
+ Step::Wait
}
};
done = act(asking, step, now, stack, counters, &mut *draw);
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 2ed5ceb41..39f3136cf 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -593,11 +593,8 @@ impl Lookup {
self.sent.remove(which);
// The next server is asked now, unless a newer query is still
// waiting for its own answer.
- if which == self.sent.len() {
- self.ask(self.unreached + 1, now, id)
- } else {
- Step::Wait
- }
+ let _ = (now, id);
+ Step::Wait
}
/// `msg` arrived at `now` from `port` of `from`, on the port `asked` was
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 2ed5ceb41..7e09b1292 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -556,7 +556,7 @@ impl Lookup {
/// every server was just asked and none reached.
fn ask(&mut self, unreached: usize, now: u64, id: impl FnOnce() -> u16) -> Step {
self.unreached = unreached;
- let nobody = unreached == self.servers.len();
+ let nobody = false;
if nobody || self.asked == ROUNDS * self.servers.len() {
return Step::Done(Err(match self.failed {
Some(rcode) => Failure::ServerFailed(rcode),
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 2ed5ceb41..7b0568397 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -593,11 +593,7 @@ impl Lookup {
self.sent.remove(which);
// The next server is asked now, unless a newer query is still
// waiting for its own answer.
- if which == self.sent.len() {
- self.ask(self.unreached + 1, now, id)
- } else {
- Step::Wait
- }
+ self.ask(self.unreached + 1, now, id)
}
/// `msg` arrived at `now` from `port` of `from`, on the port `asked` was
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 2ed5ceb41..266591b8e 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -580,7 +580,7 @@ impl Lookup {
if now < self.due {
return Step::Wait;
}
- self.ask(0, now, id)
+ self.ask(self.unreached, now, id)
}
/// The query `asked` did not reach its server, the caller learnt at
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 2ed5ceb41..060be307d 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -590,10 +590,7 @@ impl Lookup {
let Some(which) = self.sent.iter().position(|s| s.asked == asked) else {
return Step::Wait;
};
- self.sent.remove(which);
- // The next server is asked now, unless a newer query is still
- // waiting for its own answer.
- if which == self.sent.len() {
+ if which + 1 == self.sent.len() {
self.ask(self.unreached + 1, now, id)
} else {
Step::Wait
diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 59be88ccf..9cbee3f78 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 = 16;
+ pub const PENDING_PER_NEIGHBOUR: usize = 8;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 59be88ccf..9cbee3f78 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 = 16;
+ pub const PENDING_PER_NEIGHBOUR: usize = 8;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 38ffa8148..cda74f8c2 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -44,7 +44,7 @@ use crate::lease::Stack;
use crate::name::millis;
use crate::{Counter, Counters, Node};
-const _: () = assert!(PENDING_PER_NEIGHBOUR >= MAX_LOOKUPS);
+const _: usize = PENDING_PER_NEIGHBOUR;
/// One lookup, from [`Node::resolve`] until its answer is taken or it is let go.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index a5fa0f73a..d0dcdc734 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -660,7 +660,8 @@ impl Udp {
let pending = match class {
ErrorClass::Refused => SocketError::Refused,
ErrorClass::Prohibited => SocketError::Unreachable,
- ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
+ ErrorClass::PathMtu => return self.count(Counter::IcmpErrorSoft),
+ ErrorClass::Soft => SocketError::Unreachable,
};
socket.pending = Some(pending);
self.count(Counter::IcmpErrorDelivered);
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 2ed5ceb41..226a71635 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -559,8 +559,8 @@ impl Lookup {
let nobody = unreached == self.servers.len();
if nobody || self.asked == ROUNDS * self.servers.len() {
return Step::Done(Err(match self.failed {
+ _ if nobody => Failure::Unreachable,
Some(rcode) => Failure::ServerFailed(rcode),
- None if nobody => Failure::Unreachable,
None => Failure::TimedOut,
}));
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 38ffa8148..ed5e70dfb 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -202,7 +202,7 @@ impl Resolver {
None => self.asking.push(asking),
// No resolver could be sent a query: the lookup has its id and its end at once, and
// holds no socket.
- Some(result) => self.ended.push(Resolved { id: started, result }),
+ Some(_) => {}
}
Ok(started)
}
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index a5fa0f73a..1ec1ef8d8 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -637,7 +637,8 @@ impl Udp {
fn connected(&mut self, flow: &Flow) -> Option<&mut Socket> {
let index = *self.ports.get(&flow.source_port)?;
self.at(index).filter(|s| {
- s.binding == Binding::Connected { local: flow.source, peer: flow.destination, peer_port: flow.destination_port }
+ let _ = flow;
+ matches!(s.binding, Binding::Connected { .. })
})
}
What it printed: |
|
Review, round 1, of Growth: BLOCKER
NOTE
SEND BACK |
…d only when no query is still read Round 1 of the review of #781. A report from the wire is anyone's word: an off-path sender who guesses a query's source port can quote its addresses and ports, and needs no id. toyos-net-udp's SocketError::Unreachable was both an ICMP prohibition and [ip]'s own report that a next hop failed; it is two words now, Prohibited and NextHopFailed. The node's resolver lets a query go only on what it knows: [udp]'s refusal in the call, and NextHopFailed. An ICMP error (Refused, Prohibited) goes to the new toyos_dns::Lookup::on_report, which asks the next server at once where one is left to ask, lets no query go and ends nothing: the query's socket stays open and its answer is read. Lookup counts the queries sent in a row with no wait between them, and sends at once only while that run is shorter than the servers, so no server is asked twice inside WAIT_MS whoever reports what. When nobody is left to ask at once it ends Unreachable only if no query is still read, and otherwise waits the newest query's wait out: before, an unreached newest query ended the lookup under older queries whose answers were a millisecond away, and an unreached last query ended it TimedOut early. an_alias_answered_late_restarts_the_lookup_and_lets_the_old_names_queries_go is its base's again, two resolvers and 10.5 s: on 20b2143 that body was red (TimedOut at the silent resolver's last query, reported unreached while three queries to the answering resolver were read), which was the defect above and not a property of the test. The track's two residuals name their owner, the move's node stage after #777, the design it takes for [ip]'s queue, and what it deletes with it; the departure line names NUD-04's limit and why NUD-05 still passes. Readers of the old word: toyos-net-udp's US-049 and US-027, the shard's ICD-012 test, and the node's resolver. The node's datagram.rs matches Error::Failed(_) and is untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Round 2: mutation patches and the script that ran them, at
#!/bin/sh
# Each mutation: a checked patch applied to the clean committed tree, shown to
# build (or not), run, and reversed. Logs: $S/logs2/<mutation>-builds.log and
# $S/logs2/<mutation>.log, the command first and the exit code last.
W=<worktree>
S=<scratchpad>/orch/ownstack/resolver
P="-p toyos-dns -p toyos-net-node -p toyos-net-ip -p toyos-net-udp -p toyos-net-shard"
cd "$W" || exit 2
echo "HEAD=$(git rev-parse HEAD)"
clean() { [ -z "$(git status --porcelain --ignore-submodules=none)" ]; }
clean || { echo "the worktree is not clean; nothing run"; exit 2; }
for patch in "$S"/mutations2/${1:-m}*.patch; do
name=$(basename "$patch" .patch)
git apply --check "$patch" || { echo "$name does not apply"; exit 2; }
git apply "$patch"
# shellcheck disable=SC2086
{ echo "\$ cargo test --locked --no-run $P"; echo "HEAD=$(git rev-parse HEAD) + $name"; cargo test --locked --no-run $P; echo "EXIT=$?"; } > "$S/logs2/$name-builds.log" 2>&1
built=$(tail -1 "$S/logs2/$name-builds.log")
if [ "$built" = "EXIT=0" ]; then
# shellcheck disable=SC2086
{ echo "\$ cargo test --locked --no-fail-fast $P"; echo "HEAD=$(git rev-parse HEAD) + $name"; cargo test --locked --no-fail-fast $P; echo "EXIT=$?"; } > "$S/logs2/$name.log" 2>&1
ran=$(tail -1 "$S/logs2/$name.log")
else
ran="not run"
fi
git apply -R "$patch"
clean || { echo "$name left the tree dirty"; exit 2; }
echo "$name builds $built tests $ran"
done
echo "HEAD=$(git rev-parse HEAD) clean=$(clean && echo yes || echo no)"
echo "MUTATIONS DONE"
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 0fe0260d1..1f46cdaa9 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -431,12 +431,8 @@ pub enum Failure {
NoSuchName,
/// The name exists and has no `A` record (RFC 2308 §2.2).
NoAddress,
- /// No server answered, and a query that may have reached one was given
- /// its [`WAIT_MS`].
+ /// Every query went unanswered for [`WAIT_MS`].
TimedOut,
- /// No query reached a server ([`Lookup::on_unreached`]) and none is left
- /// to send at once: there is no answer to wait for.
- Unreachable,
/// The answer did not fit a UDP reply (TC).
Truncated,
/// Every server that answered could not answer, the last with this RCODE.
@@ -482,21 +478,6 @@ struct Sent {
/// any query this lookup sent for the name now asked is read, so an answer
/// late past its query's wait still ends the lookup; it is read only on the
/// query's own port, with the query's ID, from the server the query went to.
-///
-/// **A wait is for an answer that can come.** When the newest query is known
-/// or said not to have reached its server, the next server is asked at once:
-/// RFC 1035 §4.2.1's interval of two to five seconds is between repetitions
-/// of a query that may have been lost on its way, and the same section has
-/// the other servers tried first. No server is asked twice without that
-/// interval: once each has been asked in turn, the lookup waits out the
-/// newest query like any other.
-///
-/// **What the caller knows and what it was told are two calls.** A query the
-/// caller itself knows never reached its server ([`Lookup::on_unreached`]) is
-/// let go, and a lookup with no query left whose answer is read ends with
-/// [`Failure::Unreachable`]. A report from the network
-/// ([`Lookup::on_report`]) is anyone's word (RFC 8085 §5.2): it lets no query
-/// go and ends nothing, so the answer to a query reported so is still read.
#[derive(Debug)]
pub struct Lookup {
/// The name now asked: the one given, or the last alias followed.
@@ -505,17 +486,12 @@ pub struct Lookup {
aliases: usize,
servers: Vec<[u8; 4]>,
/// Queries sent for `name`, oldest first; one answered with a server
- /// failure, or known not to have reached its server, is taken out.
- /// [`Lookup::waiting`] is this list, which holds a query while the
- /// lookup lasts.
+ /// failure is taken out. [`Lookup::waiting`] is this list.
sent: Vec<Sent>,
/// The [`Asked`] the next query gets: none is given twice in a lookup.
next: u32,
/// Queries sent for `name`, answered or not.
asked: usize,
- /// Queries in a row, the newest last, with no wait between them. Once
- /// they are as many as the servers, nobody is left to ask at once.
- run: usize,
/// When the newest query is given up on.
due: u64,
/// The RCODE of the last server failure, which is what a lookup that runs
@@ -537,7 +513,6 @@ impl Lookup {
sent: Vec::new(),
next: 0,
asked: 0,
- run: 0,
due: now,
failed: None,
};
@@ -556,20 +531,16 @@ impl Lookup {
self.sent.iter().map(|s| s.asked)
}
- /// The next query for `name`, the first of a run; or the end, where
- /// every query has been sent.
+ /// The next query for `name`, or the end where every one has been sent.
fn ask(&mut self, now: u64, id: impl FnOnce() -> u16) -> Step {
if self.asked == ROUNDS * self.servers.len() {
- return Step::Done(Err(self.failed.map_or(Failure::TimedOut, Failure::ServerFailed)));
+ return Step::Done(Err(match self.failed {
+ Some(rcode) => Failure::ServerFailed(rcode),
+ None => Failure::TimedOut,
+ }));
}
- self.run = 0;
- self.send(now, id)
- }
-
- fn send(&mut self, now: u64, id: impl FnOnce() -> u16) -> Step {
let to = self.servers[self.asked % self.servers.len()];
self.asked += 1;
- self.run += 1;
let asked = Asked(self.next);
self.next += 1;
let id = id();
@@ -578,20 +549,6 @@ impl Lookup {
Step::Ask { asked, to, query: query(id, &self.name) }
}
- /// The newest query is not waited for: the next server's query now,
- /// where one is left to ask at once; or the wait for the queries still
- /// read; or, with none, the end.
- fn at_once(&mut self, now: u64, id: impl FnOnce() -> u16) -> Step {
- if self.run < self.servers.len() && self.asked < ROUNDS * self.servers.len() {
- return self.send(now, id);
- }
- if !self.sent.is_empty() {
- return Step::Wait;
- }
- // Every query left the list answered with a failure or unreached.
- Step::Done(Err(self.failed.map_or(Failure::Unreachable, Failure::ServerFailed)))
- }
-
/// The time is `now`: the newest query has had its [`WAIT_MS`] where it
/// is due. `id` draws the ID of the query this sends.
pub fn on_time(&mut self, now: u64, id: impl FnOnce() -> u16) -> Step {
@@ -601,32 +558,6 @@ impl Lookup {
self.ask(now, id)
}
- /// The caller knows at `now`, of its own knowledge, that the query
- /// `asked` did not reach its server: it was never sent. Its answer is
- /// read no more. `id` draws the ID of the query this sends.
- pub fn on_unreached(&mut self, asked: Asked, now: u64, id: impl FnOnce() -> u16) -> Step {
- let Some(which) = self.sent.iter().position(|s| s.asked == asked) else {
- return Step::Wait;
- };
- self.sent.remove(which);
- // A newer query still waits for its own answer.
- if which < self.sent.len() {
- return Step::Wait;
- }
- self.at_once(now, id)
- }
-
- /// The network said at `now` that the query `asked` did not reach its
- /// server. Its answer is still read. `id` draws the ID of the query this
- /// sends.
- pub fn on_report(&mut self, asked: Asked, now: u64, id: impl FnOnce() -> u16) -> Step {
- // A newer query still waits for its own answer.
- if self.sent.last().is_none_or(|s| s.asked != asked) {
- return Step::Wait;
- }
- self.at_once(now, id)
- }
-
/// `msg` arrived at `now` from `port` of `from`, on the port `asked` was
/// sent from. `id` draws the ID of the query this sends.
pub fn on_datagram(
diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 59be88ccf..9cbee3f78 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 = 16;
+ pub const PENDING_PER_NEIGHBOUR: usize = 8;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..a5fa0f73a 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -113,17 +113,11 @@ pub enum Binding {
Connected { local: Ipv4Addr, peer: Ipv4Addr, peer_port: Port },
}
-/// An error against a connected socket's datagrams: its next call returns it once. Two are what
-/// an ICMP message said, which anyone who knows the socket's addresses and ports can send; one
-/// is [ip]'s own.
+/// An error the network reported against a connected socket: its next call returns it once.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SocketError {
- /// An ICMP error said the peer refuses the protocol or the port.
Refused,
- /// An ICMP error said the way to the peer is administratively prohibited.
- Prohibited,
- /// [ip] found no link address for the next hop of a datagram it held: nothing left.
- NextHopFailed,
+ Unreachable,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -665,7 +659,7 @@ impl Udp {
};
let pending = match class {
ErrorClass::Refused => SocketError::Refused,
- ErrorClass::Prohibited => SocketError::Prohibited,
+ ErrorClass::Prohibited => SocketError::Unreachable,
ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
};
socket.pending = Some(pending);
@@ -675,7 +669,7 @@ impl Udp {
/// [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);
+ socket.pending = Some(SocketError::Unreachable);
}
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..d8a5cfc23 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -10,21 +10,9 @@
//! query other than the one whose socket it reached.
//! - **A socket lives as long as its query's answer is read**: it is closed when the lookup lets
//! the query go, ends, or is let go itself, and its port is free from then.
-//! - **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 [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.
-//! - **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 query [udp] refuses never left**: it is counted, holds no socket, and the lookup waits
+//! its wait out as for any query nobody answered. So does one the network reports back, its
+//! resolver unreachable or its port refused: counted, and waited out.
//! - **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.
@@ -39,16 +27,13 @@ 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_udp::{Error, SocketId};
use toyos_net_wire::Instant;
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,51 +105,42 @@ fn close(stack: &mut Stack, now: Instant, socket: SocketId) {
}
}
-/// What waits at a query's socket.
-enum Heard {
- /// A datagram of this length, now in the reply buffer.
- Reply(usize),
- /// [ip]'s word that the query never left.
- Unreached,
- /// An ICMP error's word that the query was refused.
- Reported,
-}
-
-/// The first of `queries` with something at its socket, and what: a reply is now in `reply`.
-fn waiting(queries: &[Query], stack: &mut Stack, reply: &mut [u8]) -> Option<(Query, Heard)> {
+/// The length of the next reply waiting at one of `queries`' sockets, now in `reply`, and the
+/// query it is for. An error the network reported against a query is counted and read past.
+fn waiting(queries: &[Query], stack: &mut Stack, reply: &mut [u8], counters: &mut Counters) -> Option<(Query, usize)> {
for query in queries {
- match stack.recv_from(query.socket, reply) {
- Ok(Some(received)) => return Some((*query, Heard::Reply(received.len))),
- Ok(None) => {}
- Err(Error::Failed(SocketError::NextHopFailed)) => return Some((*query, Heard::Unreached)),
- Err(Error::Failed(SocketError::Refused | SocketError::Prohibited)) => return Some((*query, Heard::Reported)),
- Err(Error::NoSuchSocket | Error::Refused(_)) => unreachable!("a query's socket is open while it is listed, and no rule refuses a receive"),
+ loop {
+ match stack.recv_from(query.socket, reply) {
+ Ok(Some(received)) => return Some((*query, received.len)),
+ Ok(None) => break,
+ Err(Error::Failed(_)) => counters.add(Counter::QueryFailed, 1),
+ Err(Error::NoSuchSocket | Error::Refused(_)) => unreachable!("a query's socket is open while it is listed, and no rule refuses a receive"),
+ }
}
}
None
}
-/// 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.
-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 } => {
- // An any-address bind that names no port is refused only for want of a free one.
- let Ok((socket, _)) = stack.bind(Ipv4Addr::UNSPECIFIED, None, &mut *draw) else { break Some(Err(Ended::NoPort)) };
+/// Carries out `step` for `asking`, then closes the socket of every query the lookup no longer
+/// reads an answer for. Returns how the lookup ended, if it did.
+fn act(asking: &mut Asking, step: Step, now: Instant, stack: &mut Stack, counters: &mut Counters, draw: &mut impl FnMut() -> u32) -> Option<Result<Vec<[u8; 4]>, Ended>> {
+ let done = match step {
+ // An any-address bind that names no port is refused only for want of a free one.
+ Step::Ask { asked, to, query } => match stack.bind(Ipv4Addr::UNSPECIFIED, None, &mut *draw) {
+ Ok((socket, _)) => {
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 });
- break None;
+ } else {
+ counters.add(Counter::QueryUnsent, 1);
+ close(stack, now, socket);
}
- counters.add(Counter::QueryUnsent, 1);
- close(stack, now, socket);
- step = asking.lookup.on_unreached(asked, millis(now), || id(&mut *draw));
+ None
}
- Step::Wait => break None,
- Step::Done(result) => break Some(result.map_err(Ended::Failed)),
- }
+ Err(_) => Some(Err(Ended::NoPort)),
+ },
+ Step::Wait => None,
+ Step::Done(result) => Some(result.map_err(Ended::Failed)),
};
let lookup = &asking.lookup;
asking.queries.retain(|query| {
@@ -198,19 +174,16 @@ 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);
- if done == Some(Err(Ended::NoPort)) {
- return Err(NotStarted::ResourceExhausted);
- }
- self.next = self.next.wrapping_add(1);
- let started = asking.id;
- match done {
- None => self.asking.push(asking),
- // No resolver could be sent a query: the lookup has its id and its end at once, and
- // holds no socket.
- Some(result) => self.ended.push(Resolved { id: started, result }),
+ match act(&mut asking, step, now, stack, counters, &mut *draw) {
+ None => {
+ self.next = self.next.wrapping_add(1);
+ let started = asking.id;
+ self.asking.push(asking);
+ Ok(started)
+ }
+ Some(Err(Ended::NoPort)) => Err(NotStarted::ResourceExhausted),
+ Some(other) => unreachable!("a lookup's first step is a query, not {other:?}"),
}
- Ok(started)
}
/// Ends every lookup whose lease went or names other resolvers, reads every reply that
@@ -223,20 +196,11 @@ impl Resolver {
let named = stack.lease().is_some_and(|lease| lease.dns == asking.resolvers);
let mut done = if named { None } else { Some(Err(Ended::LeaseChanged)) };
while done.is_none() {
- let Some((query, heard)) = waiting(&asking.queries, stack, reply.as_mut_slice()) else { break };
- let step = match heard {
- // The socket is connected: [udp] delivered this from port 53 of the resolver
- // the query went to, or not at all.
- Heard::Reply(len) => asking.lookup.on_datagram(query.asked, query.to, PORT, reply.get(..len).unwrap_or_default(), ms, || id(&mut *draw)),
- Heard::Unreached => {
- counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_unreached(query.asked, ms, || id(&mut *draw))
- }
- Heard::Reported => {
- counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_report(query.asked, ms, || id(&mut *draw))
- }
- };
+ let Some((query, len)) = waiting(&asking.queries, stack, reply.as_mut_slice(), counters) else { break };
+ let message = reply.get(..len).unwrap_or_default();
+ // The socket is connected: [udp] delivered this from port 53 of the resolver the
+ // query went to, or not at all.
+ let step = asking.lookup.on_datagram(query.asked, query.to, PORT, message, ms, || id(&mut *draw));
done = act(asking, step, now, stack, counters, &mut *draw);
}
if done.is_none() {
@@ -267,11 +231,9 @@ impl Resolver {
}
impl Node {
- /// Starts looking `name` up at the resolvers the held lease names, which spends two draws a
- /// query, its id and then its port: for the first query, and for each asked in its place
- /// because [udp] refused the one before. A call refused for want of a port has spent them;
- /// any other refused call spends none. A lookup no resolver could be sent a query of has
- /// started and ended: its end is in the next [`Self::take_resolved`].
+ /// Starts looking `name` up at the resolvers the held lease names, which spends two draws:
+ /// the first query's id, then its port. A call refused for want of a port has spent both;
+ /// any other refused call spends none.
pub fn resolve(&mut self, now: Instant, name: Name, mut draw: impl FnMut() -> u32) -> Result<LookupId, NotStarted> {
self.resolver.start(now, name, &mut self.stack, &mut self.counters, &mut draw)
}
diff --git a/userland/netstack/src/main.rs b/userland/netstack/src/main.rs
index 21c30f688..0bd5fcc9f 100644
--- a/userland/netstack/src/main.rs
+++ b/userland/netstack/src/main.rs
@@ -1550,9 +1550,6 @@ impl Netstack {
// whether the name or only its address is missing.
Err(Ended::Failed(Failure::NoSuchName | Failure::NoAddress)) => answer_lookup(&client, &[]),
Err(Ended::Failed(Failure::TimedOut)) => client.error(ERR_TIMED_OUT),
- Err(Ended::Failed(Failure::Unreachable)) => {
- unreachable!("netstack: no lookup here is told a query did not reach its server, and {name}'s ended so")
- }
Err(Ended::Failed(why @ (Failure::Truncated | Failure::ServerFailed(_) | Failure::TooManyAliases))) => {
say!("netstack: a lookup of {name} ended without an answer: {why:?}");
client.error(ERR_OTHER);
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..be7c09da3 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -160,7 +160,7 @@ fn act(asking: &mut Asking, mut step: Step, now: Instant, stack: &mut Stack, cou
}
counters.add(Counter::QueryUnsent, 1);
close(stack, now, socket);
- step = asking.lookup.on_unreached(asked, millis(now), || id(&mut *draw));
+ break None;
}
Step::Wait => break None,
Step::Done(result) => break Some(result.map_err(Ended::Failed)),
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..d4b2e7efe 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -230,7 +230,7 @@ impl Resolver {
Heard::Reply(len) => asking.lookup.on_datagram(query.asked, query.to, PORT, reply.get(..len).unwrap_or_default(), ms, || id(&mut *draw)),
Heard::Unreached => {
counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_unreached(query.asked, ms, || id(&mut *draw))
+ Step::Wait
}
Heard::Reported => {
counters.add(Counter::QueryFailed, 1);
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 0fe0260d1..4cfae4ed0 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -610,10 +610,8 @@ impl Lookup {
};
self.sent.remove(which);
// A newer query still waits for its own answer.
- if which < self.sent.len() {
- return Step::Wait;
- }
- self.at_once(now, id)
+ let _ = (now, id);
+ Step::Wait
}
/// The network said at `now` that the query `asked` did not reach its
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 0fe0260d1..fa8d4b366 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -582,7 +582,7 @@ impl Lookup {
/// where one is left to ask at once; or the wait for the queries still
/// read; or, with none, the end.
fn at_once(&mut self, now: u64, id: impl FnOnce() -> u16) -> Step {
- if self.run < self.servers.len() && self.asked < ROUNDS * self.servers.len() {
+ if self.asked < ROUNDS * self.servers.len() {
return self.send(now, id);
}
if !self.sent.is_empty() {
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 0fe0260d1..ce96251be 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -609,10 +609,6 @@ impl Lookup {
return Step::Wait;
};
self.sent.remove(which);
- // A newer query still waits for its own answer.
- if which < self.sent.len() {
- return Step::Wait;
- }
self.at_once(now, id)
}
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 0fe0260d1..1d8cdff83 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -562,7 +562,6 @@ impl Lookup {
if self.asked == ROUNDS * self.servers.len() {
return Step::Done(Err(self.failed.map_or(Failure::TimedOut, Failure::ServerFailed)));
}
- self.run = 0;
self.send(now, id)
}
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 0fe0260d1..301511ed8 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -608,9 +608,7 @@ impl Lookup {
let Some(which) = self.sent.iter().position(|s| s.asked == asked) else {
return Step::Wait;
};
- self.sent.remove(which);
- // A newer query still waits for its own answer.
- if which < self.sent.len() {
+ if which + 1 < self.sent.len() {
return Step::Wait;
}
self.at_once(now, id)
diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 59be88ccf..9cbee3f78 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 = 16;
+ pub const PENDING_PER_NEIGHBOUR: usize = 8;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 59be88ccf..9cbee3f78 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 = 16;
+ pub const PENDING_PER_NEIGHBOUR: usize = 8;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..990bcdc37 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -47,7 +47,7 @@ use crate::lease::Stack;
use crate::name::millis;
use crate::{Counter, Counters, Node};
-const _: () = assert!(PENDING_PER_NEIGHBOUR >= MAX_LOOKUPS);
+const _: usize = PENDING_PER_NEIGHBOUR;
/// One lookup, from [`Node::resolve`] until its answer is taken or it is let go.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..8d8a26938 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -666,7 +666,8 @@ impl Udp {
let pending = match class {
ErrorClass::Refused => SocketError::Refused,
ErrorClass::Prohibited => SocketError::Prohibited,
- ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
+ ErrorClass::PathMtu => return self.count(Counter::IcmpErrorSoft),
+ ErrorClass::Soft => SocketError::Prohibited,
};
socket.pending = Some(pending);
self.count(Counter::IcmpErrorDelivered);
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 0fe0260d1..ce3c58a18 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -589,7 +589,7 @@ impl Lookup {
return Step::Wait;
}
// Every query left the list answered with a failure or unreached.
- Step::Done(Err(self.failed.map_or(Failure::Unreachable, Failure::ServerFailed)))
+ Step::Done(Err(Failure::Unreachable))
}
/// The time is `now`: the newest query has had its [`WAIT_MS`] where it
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..0fdade7bb 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -208,7 +208,7 @@ impl Resolver {
None => self.asking.push(asking),
// No resolver could be sent a query: the lookup has its id and its end at once, and
// holds no socket.
- Some(result) => self.ended.push(Resolved { id: started, result }),
+ Some(_) => {}
}
Ok(started)
}
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..8fe43316e 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -643,7 +643,8 @@ impl Udp {
fn connected(&mut self, flow: &Flow) -> Option<&mut Socket> {
let index = *self.ports.get(&flow.source_port)?;
self.at(index).filter(|s| {
- s.binding == Binding::Connected { local: flow.source, peer: flow.destination, peer_port: flow.destination_port }
+ let _ = flow;
+ matches!(s.binding, Binding::Connected { .. })
})
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..99d3f8c5f 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -234,7 +234,7 @@ impl Resolver {
}
Heard::Reported => {
counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_report(query.asked, ms, || id(&mut *draw))
+ asking.lookup.on_unreached(query.asked, ms, || id(&mut *draw))
}
};
done = act(asking, step, now, stack, counters, &mut *draw);
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 0fe0260d1..28360c22d 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -624,6 +624,7 @@ impl Lookup {
if self.sent.last().is_none_or(|s| s.asked != asked) {
return Step::Wait;
}
+ self.sent.pop();
self.at_once(now, id)
}
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..855f0c92a 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -665,7 +665,7 @@ impl Udp {
};
let pending = match class {
ErrorClass::Refused => SocketError::Refused,
- ErrorClass::Prohibited => SocketError::Prohibited,
+ ErrorClass::Prohibited => SocketError::NextHopFailed,
ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
};
socket.pending = Some(pending);
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..798a9f404 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -675,7 +675,7 @@ impl Udp {
/// [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);
+ socket.pending = Some(SocketError::Prohibited);
}
}
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 0fe0260d1..62f9d0151 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -585,9 +585,6 @@ impl Lookup {
if self.run < self.servers.len() && self.asked < ROUNDS * self.servers.len() {
return self.send(now, id);
}
- if !self.sent.is_empty() {
- return Step::Wait;
- }
// Every query left the list answered with a failure or unreached.
Step::Done(Err(self.failed.map_or(Failure::Unreachable, Failure::ServerFailed)))
}
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 0fe0260d1..e30303cad 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -621,7 +621,7 @@ impl Lookup {
/// sends.
pub fn on_report(&mut self, asked: Asked, now: u64, id: impl FnOnce() -> u16) -> Step {
// A newer query still waits for its own answer.
- if self.sent.last().is_none_or(|s| s.asked != asked) {
+ if !self.sent.iter().any(|s| s.asked == asked) {
return Step::Wait;
}
self.at_once(now, id)
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..8bb7cdd14 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -664,7 +664,7 @@ impl Udp {
return self.count(counter);
};
let pending = match class {
- ErrorClass::Refused => SocketError::Refused,
+ ErrorClass::Refused => SocketError::NextHopFailed,
ErrorClass::Prohibited => SocketError::Prohibited,
ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
};
diff --git a/toyos-dns/src/tests.rs b/toyos-dns/src/tests.rs
index 0f7b9a36a..fec9b8a51 100644
--- a/toyos-dns/src/tests.rs
+++ b/toyos-dns/src/tests.rs
@@ -927,6 +927,55 @@ fn an_older_query_that_reached_nobody_asks_nobody_and_ends_nothing() {
assert_eq!(lookup.on_unreached(q2, WAIT_MS + 2, undrawn), Step::Done(Err(Failure::Unreachable)), "the newest reached nobody either");
}
+// The newest query reached nobody, and nobody is left to ask at once: the
+// lookup ends only where no older query's answer is still read, and otherwise
+// waits the newest one's wait out for it.
+#[test]
+fn a_lookup_whose_newest_query_reached_nobody_waits_for_an_older_querys_answer() {
+ let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, || 1).unwrap();
+ let (q1, ..) = asked(&first);
+ let second = lookup.on_time(WAIT_MS, || 2);
+ assert_eq!(lookup.on_unreached(asked(&second).0, WAIT_MS, undrawn), Step::Wait, "the first query may yet be answered");
+ assert_eq!(waiting(&lookup), [q1]);
+ assert_eq!(lookup.due(), 2 * WAIT_MS, "the wait is the one the query that reached nobody began");
+ let ok = reply(1, OK, "www.example", &[a("www.example", [1, 2, 3, 4])]);
+ assert_eq!(lookup.on_datagram(q1, S1, PORT, &ok, WAIT_MS + 1, undrawn), Step::Done(Ok(vec![[1, 2, 3, 4]])));
+}
+
+#[test]
+fn a_lookup_that_then_reached_neither_server_waits_for_an_older_querys_answer() {
+ let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 1).unwrap();
+ let (q1, ..) = asked(&first);
+ let second = lookup.on_time(WAIT_MS, || 2);
+ assert_eq!(to(&second), (S2, 2));
+ let third = lookup.on_unreached(asked(&second).0, WAIT_MS, || 3);
+ assert_eq!(to(&third), (S1, 3), "S1 again, a wait after its first query");
+ assert_eq!(lookup.on_unreached(asked(&third).0, WAIT_MS, undrawn), Step::Wait, "the first query may yet be answered");
+ assert_eq!(waiting(&lookup), [q1]);
+ assert_eq!(lookup.due(), 2 * WAIT_MS);
+ assert_eq!(lookup.on_time(2 * WAIT_MS - 1, undrawn), Step::Wait);
+ assert_eq!(to(&lookup.on_time(2 * WAIT_MS, || 4)), (S2, 4), "S2's turn, a wait after it was last asked");
+ let ok = reply(1, OK, "www.example", &[a("www.example", [1, 2, 3, 4])]);
+ assert_eq!(lookup.on_datagram(q1, S1, PORT, &ok, 2 * WAIT_MS + 1, undrawn), Step::Done(Ok(vec![[1, 2, 3, 4]])));
+}
+
+// And the last query a lookup has: the queries before it are read until its
+// wait is over, as if it had left.
+#[test]
+fn a_lookup_whose_last_query_reached_nobody_waits_for_the_answers_still_read() {
+ let (mut lookup, first) = Lookup::start(name("www.example"), &[S1], 0, || 1).unwrap();
+ let (q1, ..) = asked(&first);
+ let mut last = first;
+ for round in 1..ROUNDS as u64 {
+ last = lookup.on_time(round * WAIT_MS, || 2);
+ }
+ let end = ROUNDS as u64 * WAIT_MS;
+ assert_eq!(lookup.on_unreached(asked(&last).0, end - WAIT_MS, undrawn), Step::Wait, "the queries before it may yet be answered");
+ assert_eq!(waiting(&lookup).first(), Some(&q1));
+ assert_eq!(lookup.on_time(end - 1, undrawn), Step::Wait);
+ assert_eq!(lookup.on_time(end, undrawn), Step::Done(Err(Failure::TimedOut)), "queries that may have reached the server were waited for");
+}
+
#[test]
fn a_server_that_answered_with_a_failure_is_what_a_lookup_that_then_reached_nobody_reports() {
let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 1).unwrap();
diff --git a/userland/netstack/node/tests/resolve.rs b/userland/netstack/node/tests/resolve.rs
index 437bfb0ec..c9892861b 100644
--- a/userland/netstack/node/tests/resolve.rs
+++ b/userland/netstack/node/tests/resolve.rs
@@ -36,6 +36,8 @@ const UNROUTED_TOO: Ipv4Addr = Ipv4Addr::new(198, 51, 100, 54);
/// RFC 792: a destination unreachable's codes.
const HOST_UNREACHABLE: u8 = 1;
const PORT_UNREACHABLE: u8 = 3;
+/// RFC 1812 §5.2.7.1: communication administratively prohibited.
+const PROHIBITED: u8 = 13;
const ADDRESS: [u8; 4] = [203, 0, 113, 7];
/// The address every reply that must not be read carries.
const FORGED: [u8; 4] = [203, 0, 113, 66];
@@ -574,6 +576,32 @@ fn a_report_that_is_not_about_the_query_or_is_a_hint_ends_no_wait() {
assert_eq!(net.lan.node.take_resolved(), [Resolved { id, result: Ok(vec![ADDRESS]) }]);
}
+// RFC 8085 §5.2: "applications SHOULD appropriately validate the payload of ICMP messages to
+// ensure these are received in response to transmitted traffic", and such a message "SHOULD NOT
+// abort the communication": what a report quotes, an off-path sender can guess. A port
+// unreachable and a prohibition in the resolver's name that quote the query's own addresses and
+// ports each reach the query's socket, which is the premise, and end nothing: each is counted,
+// the socket is still the query's, and the resolver's answer behind them ends the lookup.
+#[test]
+fn a_forged_report_of_the_querys_own_addresses_and_ports_ends_nothing_and_the_answer_behind_it_is_taken() {
+ let mut net = Net::leased(&[ANSWERS], Some(R));
+ let id = net.resolve("www.example").unwrap();
+ net.pass();
+ let asked = net.queried[0].clone();
+ for (reported, code) in [(1, PORT_UNREACHABLE), (2, PROHIBITED)] {
+ let delivered = net.lan.counted(Rule::IcmpErrorDelivered);
+ net.lan.deliver(&reports(code, (ANSWERS, MAC_S), &asked.udp));
+ assert_eq!(net.lan.counted(Rule::IcmpErrorDelivered), delivered + 1, "the premise: the report of code {code} reached the query's socket");
+ assert_eq!(net.lan.node.take_resolved(), NONE, "a report of code {code} from the wire ended the lookup");
+ assert_eq!(net.lan.node.counters().get(Counter::QueryFailed), reported);
+ assert!(net.port_held(asked.udp.source_port), "the reported query's socket, after code {code}");
+ }
+ net.pass();
+ assert_eq!(net.queried.len(), 1, "one resolver: a report asked it again inside its wait");
+ net.resolver_says(asked.udp.source_port, &gives(&asked.udp.payload, ADDRESS));
+ assert_eq!(net.lan.node.take_resolved(), [Resolved { id, result: Ok(vec![ADDRESS]) }]);
+}
+
// RFC 1034 §5.3.3: an alias with no address is asked again at its end, every query afresh. The
// resolver answers only after three queries have left, a wait apart, and with an alias alone:
// the queries for the old name are let go, so the two other answers to them, arriving with the
diff --git a/userland/netstack/node/tests/resolve.rs b/userland/netstack/node/tests/resolve.rs
index 437bfb0ec..aef85ec60 100644
--- a/userland/netstack/node/tests/resolve.rs
+++ b/userland/netstack/node/tests/resolve.rs
@@ -575,19 +575,19 @@ fn a_report_that_is_not_about_the_query_or_is_a_hint_ends_no_wait() {
}
// RFC 1034 §5.3.3: an alias with no address is asked again at its end, every query afresh. The
-// resolver answers only after three queries have left, a wait apart, and with an alias alone:
-// the queries for the old name are let go, so the two other answers to them, arriving with the
-// first, find no socket (RFC 1122 §4.1.3.1), and the alias's address ends the lookup.
+// first resolver answers only after six queries have left, three of them to it, and with an alias
+// alone: the queries for the old name are let go, so the two other answers to them, arriving
+// with the first, find no socket (RFC 1122 §4.1.3.1), and the alias's address ends the lookup.
#[test]
fn an_alias_answered_late_restarts_the_lookup_and_lets_the_old_names_queries_go() {
- let mut net = Net::leased(&[ANSWERS], Some(R));
- net.zone.push(("www.example", 4_500, Says::Alias("cdn.example")));
+ let mut net = Net::leased(&[ANSWERS, SILENT], Some(R));
+ net.zone.push(("www.example", 10_500, Says::Alias("cdn.example")));
net.zone.push(("cdn.example", 0, Says::Address(ADDRESS)));
let nobodys = net.lan.counted(Rule::RxNoSocket);
let id = net.resolve("www.example").unwrap();
let ended = net.run(id, 40_000);
assert_eq!(ended, Some(Ok(vec![ADDRESS])), "the resolver heard {:?}", net.queried);
- assert_eq!(net.ms(), 4_500);
+ assert_eq!(net.ms(), 10_500);
let names: Vec<&str> = net.queried.iter().map(|query| query.name.as_str()).collect();
assert_eq!(names, ["www.example", "www.example", "www.example", "cdn.example"]);
assert_eq!(net.lan.counted(Rule::RxNoSocket), nobodys + 2, "the old name's other two answers");
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 0fe0260d1..ce96251be 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -609,10 +609,6 @@ impl Lookup {
return Step::Wait;
};
self.sent.remove(which);
- // A newer query still waits for its own answer.
- if which < self.sent.len() {
- return Step::Wait;
- }
self.at_once(now, id)
}
diff --git a/toyos-dns/src/tests.rs b/toyos-dns/src/tests.rs
index 7bfc26d59..82ff61567 100644
--- a/toyos-dns/src/tests.rs
+++ b/toyos-dns/src/tests.rs
@@ -926,17 +926,6 @@ fn an_older_query_that_reached_nobody_asks_nobody_and_ends_nothing() {
assert_eq!(lookup.on_unreached(q2, WAIT_MS + 2, undrawn), Step::Done(Err(Failure::Unreachable)), "the newest reached nobody either");
}
-// With a server left to ask at once, too: it is the newest query's wait.
-#[test]
-fn an_older_query_that_reached_nobody_does_not_hurry_the_next_server() {
- let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 1).unwrap();
- let second = lookup.on_time(WAIT_MS, || 2);
- assert_eq!(to(&second), (S2, 2));
- assert_eq!(lookup.on_unreached(asked(&first).0, WAIT_MS + 1, undrawn), Step::Wait, "the query to S2 still waits for its own answer");
- assert_eq!(waiting(&lookup), [asked(&second).0]);
- assert_eq!(lookup.due(), 2 * WAIT_MS);
-}
-
// The newest query reached nobody, and nobody is left to ask at once: the
// lookup ends only where no older query's answer is still read, and otherwise
// waits the newest one's wait out for it. |
|
Review, round 2, of Growth: Round 1's BLOCKERs
BLOCKER
NOTE
SEND BACK |
…er's failures chain nothing Round 2 of the review of #781, finding 5. Lookup counted the queries sent in a row and a server failure of the newest query reset the count, so a report of each query to one resolver and a failure from the other chained into ROUNDS queries to each with no wait, and the lookup ended ServerFailed with the first resolver's answer on its way. The count is gone. Each server carries when it may next be asked: WAIT_MS after its last query, WAIT_MS after the caller last knew it was not reached, and at once when it has answered. Every path that asks, the wait's end, a failure, a query known unreached and a report, is the one `ask`: it sends where the next server may be asked, waits while any answer is still read, until the next server's turn or the last query's wait is over, and ends at once only with no query left. The track names the slice that owns the node's two residuals where it lists stage 5's slices, datagram senders that wait on the hop, and its first residual says that an ICMP error can take the place of [ip]'s word on a socket until the late read that residual is about. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Round 3: mutation patches and the script that ran them, at
#!/bin/sh
# Each mutation: a checked patch applied to the clean committed tree, shown to
# build (or not), run, and reversed. Logs: $S/logs3/<mutation>-builds.log and
# $S/logs3/<mutation>.log, the command first and the exit code last.
W=<worktree>
S=<scratchpad>/orch/ownstack/resolver
P="-p toyos-dns -p toyos-net-node -p toyos-net-ip -p toyos-net-udp -p toyos-net-shard"
cd "$W" || exit 2
echo "HEAD=$(git rev-parse HEAD)"
clean() { [ -z "$(git status --porcelain --ignore-submodules=none)" ]; }
clean || { echo "the worktree is not clean; nothing run"; exit 2; }
for patch in "$S"/mutations3/${1:-m}*.patch; do
name=$(basename "$patch" .patch)
git apply --check "$patch" || { echo "$name does not apply"; exit 2; }
git apply "$patch"
# shellcheck disable=SC2086
{ echo "\$ cargo test --locked --no-run $P"; echo "HEAD=$(git rev-parse HEAD) + $name"; cargo test --locked --no-run $P; echo "EXIT=$?"; } > "$S/logs3/$name-builds.log" 2>&1
built=$(tail -1 "$S/logs3/$name-builds.log")
if [ "$built" = "EXIT=0" ]; then
# shellcheck disable=SC2086
{ echo "\$ cargo test --locked --no-fail-fast $P"; echo "HEAD=$(git rev-parse HEAD) + $name"; cargo test --locked --no-fail-fast $P; echo "EXIT=$?"; } > "$S/logs3/$name.log" 2>&1
ran=$(tail -1 "$S/logs3/$name.log")
else
ran="not run"
fi
git apply -R "$patch"
clean || { echo "$name left the tree dirty"; exit 2; }
echo "$name builds $built tests $ran"
done
echo "HEAD=$(git rev-parse HEAD) clean=$(clean && echo yes || echo no)"
echo "MUTATIONS DONE"
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..1f46cdaa9 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -431,12 +431,8 @@ pub enum Failure {
NoSuchName,
/// The name exists and has no `A` record (RFC 2308 §2.2).
NoAddress,
- /// No server answered, and a query that may have reached one was given
- /// its [`WAIT_MS`].
+ /// Every query went unanswered for [`WAIT_MS`].
TimedOut,
- /// No query reached a server ([`Lookup::on_unreached`]) and none is left
- /// to send at once: there is no answer to wait for.
- Unreachable,
/// The answer did not fit a UDP reply (TC).
Truncated,
/// Every server that answered could not answer, the last with this RCODE.
@@ -482,46 +478,21 @@ struct Sent {
/// any query this lookup sent for the name now asked is read, so an answer
/// late past its query's wait still ends the lookup; it is read only on the
/// query's own port, with the query's ID, from the server the query went to.
-///
-/// **A wait is for an answer that can come.** When the newest query is
-/// answered with a failure, or known or said not to have reached its server,
-/// the next server is asked at once: RFC 1035 §4.2.1's interval of two to
-/// five seconds is between repetitions of a query that may have been lost on
-/// its way, and the same section has the other servers tried first. **The
-/// interval is each server's**: one that has not answered is sent no second
-/// query inside [`WAIT_MS`] of its last, or of the caller's last knowledge
-/// that it was not reached, whatever is said of that query and whatever
-/// another server answers. Where the next server cannot be asked yet, the
-/// lookup waits until it can while any answer is still read, and ends at
-/// once only with none.
-///
-/// **What the caller knows and what it was told are two calls.** A query the
-/// caller itself knows never reached its server ([`Lookup::on_unreached`]) is
-/// let go, and a lookup with no query left whose answer is read ends with
-/// [`Failure::Unreachable`]. A report from the network
-/// ([`Lookup::on_report`]) is anyone's word (RFC 8085 §5.2): it lets no query
-/// go and ends nothing, so the answer to a query reported so is still read.
#[derive(Debug)]
pub struct Lookup {
/// The name now asked: the one given, or the last alias followed.
name: Name,
/// Aliases followed so far, counted against [`MAX_ALIASES`].
aliases: usize,
- /// Each server, and when it may be asked again: [`WAIT_MS`] after its
- /// last query or after it was last known not to be reached, and at once
- /// when it has answered.
- servers: Vec<([u8; 4], u64)>,
+ servers: Vec<[u8; 4]>,
/// Queries sent for `name`, oldest first; one answered with a server
- /// failure, or known not to have reached its server, is taken out.
- /// [`Lookup::waiting`] is this list, which holds a query while the
- /// lookup lasts.
+ /// failure is taken out. [`Lookup::waiting`] is this list.
sent: Vec<Sent>,
/// The [`Asked`] the next query gets: none is given twice in a lookup.
next: u32,
/// Queries sent for `name`, answered or not.
asked: usize,
- /// When the newest query is given up on, or the next server may be
- /// asked if that is later.
+ /// When the newest query is given up on.
due: u64,
/// The RCODE of the last server failure, which is what a lookup that runs
/// out of queries reports if any server answered at all.
@@ -538,7 +509,7 @@ impl Lookup {
let mut lookup = Self {
name,
aliases: 0,
- servers: servers.iter().map(|server| (*server, now)).collect(),
+ servers: servers.to_vec(),
sent: Vec::new(),
next: 0,
asked: 0,
@@ -560,40 +531,22 @@ impl Lookup {
self.sent.iter().map(|s| s.asked)
}
- /// The next server's query, if it may be asked at `now`; or the wait for
- /// the answers still read, until it may or the last query's wait is
- /// over; or the end.
+ /// The next query for `name`, or the end where every one has been sent.
fn ask(&mut self, now: u64, id: impl FnOnce() -> u16) -> Step {
- let turn = self.asked % self.servers.len();
- let (to, free) = self.servers[turn];
- let spent = self.asked == ROUNDS * self.servers.len();
- if !spent && now >= free {
- self.servers[turn].1 = now + WAIT_MS;
- self.asked += 1;
- let asked = Asked(self.next);
- self.next += 1;
- let id = id();
- self.sent.push(Sent { asked, id, to });
- self.due = now + WAIT_MS;
- return Step::Ask { asked, to, query: query(id, &self.name) };
- }
- if self.sent.is_empty() || (spent && now >= self.due) {
- // With no query left, each was answered with a failure or never
- // reached its server.
- let silence = if self.sent.is_empty() { Failure::Unreachable } else { Failure::TimedOut };
- return Step::Done(Err(self.failed.map_or(silence, Failure::ServerFailed)));
- }
- if !spent {
- self.due = self.due.max(free);
- }
- Step::Wait
- }
-
- /// `server` may next be asked at `at`.
- fn free(&mut self, server: [u8; 4], at: u64) {
- for (_, free) in self.servers.iter_mut().filter(|(addr, _)| *addr == server) {
- *free = at;
+ if self.asked == ROUNDS * self.servers.len() {
+ return Step::Done(Err(match self.failed {
+ Some(rcode) => Failure::ServerFailed(rcode),
+ None => Failure::TimedOut,
+ }));
}
+ let to = self.servers[self.asked % self.servers.len()];
+ self.asked += 1;
+ let asked = Asked(self.next);
+ self.next += 1;
+ let id = id();
+ self.sent.push(Sent { asked, id, to });
+ self.due = now + WAIT_MS;
+ Step::Ask { asked, to, query: query(id, &self.name) }
}
/// The time is `now`: the newest query has had its [`WAIT_MS`] where it
@@ -605,33 +558,6 @@ impl Lookup {
self.ask(now, id)
}
- /// The caller knows at `now`, of its own knowledge, that the query
- /// `asked` did not reach its server: it was never sent. Its answer is
- /// read no more. `id` draws the ID of the query this sends.
- pub fn on_unreached(&mut self, asked: Asked, now: u64, id: impl FnOnce() -> u16) -> Step {
- let Some(which) = self.sent.iter().position(|s| s.asked == asked) else {
- return Step::Wait;
- };
- let unreached = self.sent.remove(which);
- self.free(unreached.to, now + WAIT_MS);
- // A newer query still waits for its own answer.
- if which < self.sent.len() {
- return Step::Wait;
- }
- self.ask(now, id)
- }
-
- /// The network said at `now` that the query `asked` did not reach its
- /// server. Its answer is still read. `id` draws the ID of the query this
- /// sends.
- pub fn on_report(&mut self, asked: Asked, now: u64, id: impl FnOnce() -> u16) -> Step {
- // A newer query still waits for its own answer.
- if self.sent.last().is_none_or(|s| s.asked != asked) {
- return Step::Wait;
- }
- self.ask(now, id)
- }
-
/// `msg` arrived at `now` from `port` of `from`, on the port `asked` was
/// sent from. `id` draws the ID of the query this sends.
pub fn on_datagram(
@@ -659,9 +585,6 @@ impl Lookup {
Verdict::ServerFailed(rcode) => {
self.failed = Some(rcode);
self.sent.remove(which);
- // It answered: its interval, which is for a query that may
- // have been lost, is over.
- self.free(from, now);
// The next server is asked now, unless a newer query is still
// waiting for its own answer.
if which == self.sent.len() {
@@ -682,9 +605,6 @@ impl Lookup {
self.sent.clear();
self.asked = 0;
self.failed = None;
- for (_, free) in &mut self.servers {
- *free = now;
- }
self.ask(now, id)
}
},
diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 59be88ccf..9cbee3f78 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 = 16;
+ pub const PENDING_PER_NEIGHBOUR: usize = 8;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..a5fa0f73a 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -113,17 +113,11 @@ pub enum Binding {
Connected { local: Ipv4Addr, peer: Ipv4Addr, peer_port: Port },
}
-/// An error against a connected socket's datagrams: its next call returns it once. Two are what
-/// an ICMP message said, which anyone who knows the socket's addresses and ports can send; one
-/// is [ip]'s own.
+/// An error the network reported against a connected socket: its next call returns it once.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SocketError {
- /// An ICMP error said the peer refuses the protocol or the port.
Refused,
- /// An ICMP error said the way to the peer is administratively prohibited.
- Prohibited,
- /// [ip] found no link address for the next hop of a datagram it held: nothing left.
- NextHopFailed,
+ Unreachable,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -665,7 +659,7 @@ impl Udp {
};
let pending = match class {
ErrorClass::Refused => SocketError::Refused,
- ErrorClass::Prohibited => SocketError::Prohibited,
+ ErrorClass::Prohibited => SocketError::Unreachable,
ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
};
socket.pending = Some(pending);
@@ -675,7 +669,7 @@ impl Udp {
/// [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);
+ socket.pending = Some(SocketError::Unreachable);
}
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..d8a5cfc23 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -10,21 +10,9 @@
//! query other than the one whose socket it reached.
//! - **A socket lives as long as its query's answer is read**: it is closed when the lookup lets
//! the query go, ends, or is let go itself, and its port is free from then.
-//! - **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 [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.
-//! - **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 query [udp] refuses never left**: it is counted, holds no socket, and the lookup waits
+//! its wait out as for any query nobody answered. So does one the network reports back, its
+//! resolver unreachable or its port refused: counted, and waited out.
//! - **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.
@@ -39,16 +27,13 @@ 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_udp::{Error, SocketId};
use toyos_net_wire::Instant;
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,51 +105,42 @@ fn close(stack: &mut Stack, now: Instant, socket: SocketId) {
}
}
-/// What waits at a query's socket.
-enum Heard {
- /// A datagram of this length, now in the reply buffer.
- Reply(usize),
- /// [ip]'s word that the query never left.
- Unreached,
- /// An ICMP error's word that the query was refused.
- Reported,
-}
-
-/// The first of `queries` with something at its socket, and what: a reply is now in `reply`.
-fn waiting(queries: &[Query], stack: &mut Stack, reply: &mut [u8]) -> Option<(Query, Heard)> {
+/// The length of the next reply waiting at one of `queries`' sockets, now in `reply`, and the
+/// query it is for. An error the network reported against a query is counted and read past.
+fn waiting(queries: &[Query], stack: &mut Stack, reply: &mut [u8], counters: &mut Counters) -> Option<(Query, usize)> {
for query in queries {
- match stack.recv_from(query.socket, reply) {
- Ok(Some(received)) => return Some((*query, Heard::Reply(received.len))),
- Ok(None) => {}
- Err(Error::Failed(SocketError::NextHopFailed)) => return Some((*query, Heard::Unreached)),
- Err(Error::Failed(SocketError::Refused | SocketError::Prohibited)) => return Some((*query, Heard::Reported)),
- Err(Error::NoSuchSocket | Error::Refused(_)) => unreachable!("a query's socket is open while it is listed, and no rule refuses a receive"),
+ loop {
+ match stack.recv_from(query.socket, reply) {
+ Ok(Some(received)) => return Some((*query, received.len)),
+ Ok(None) => break,
+ Err(Error::Failed(_)) => counters.add(Counter::QueryFailed, 1),
+ Err(Error::NoSuchSocket | Error::Refused(_)) => unreachable!("a query's socket is open while it is listed, and no rule refuses a receive"),
+ }
}
}
None
}
-/// 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.
-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 } => {
- // An any-address bind that names no port is refused only for want of a free one.
- let Ok((socket, _)) = stack.bind(Ipv4Addr::UNSPECIFIED, None, &mut *draw) else { break Some(Err(Ended::NoPort)) };
+/// Carries out `step` for `asking`, then closes the socket of every query the lookup no longer
+/// reads an answer for. Returns how the lookup ended, if it did.
+fn act(asking: &mut Asking, step: Step, now: Instant, stack: &mut Stack, counters: &mut Counters, draw: &mut impl FnMut() -> u32) -> Option<Result<Vec<[u8; 4]>, Ended>> {
+ let done = match step {
+ // An any-address bind that names no port is refused only for want of a free one.
+ Step::Ask { asked, to, query } => match stack.bind(Ipv4Addr::UNSPECIFIED, None, &mut *draw) {
+ Ok((socket, _)) => {
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 });
- break None;
+ } else {
+ counters.add(Counter::QueryUnsent, 1);
+ close(stack, now, socket);
}
- counters.add(Counter::QueryUnsent, 1);
- close(stack, now, socket);
- step = asking.lookup.on_unreached(asked, millis(now), || id(&mut *draw));
+ None
}
- Step::Wait => break None,
- Step::Done(result) => break Some(result.map_err(Ended::Failed)),
- }
+ Err(_) => Some(Err(Ended::NoPort)),
+ },
+ Step::Wait => None,
+ Step::Done(result) => Some(result.map_err(Ended::Failed)),
};
let lookup = &asking.lookup;
asking.queries.retain(|query| {
@@ -198,19 +174,16 @@ 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);
- if done == Some(Err(Ended::NoPort)) {
- return Err(NotStarted::ResourceExhausted);
- }
- self.next = self.next.wrapping_add(1);
- let started = asking.id;
- match done {
- None => self.asking.push(asking),
- // No resolver could be sent a query: the lookup has its id and its end at once, and
- // holds no socket.
- Some(result) => self.ended.push(Resolved { id: started, result }),
+ match act(&mut asking, step, now, stack, counters, &mut *draw) {
+ None => {
+ self.next = self.next.wrapping_add(1);
+ let started = asking.id;
+ self.asking.push(asking);
+ Ok(started)
+ }
+ Some(Err(Ended::NoPort)) => Err(NotStarted::ResourceExhausted),
+ Some(other) => unreachable!("a lookup's first step is a query, not {other:?}"),
}
- Ok(started)
}
/// Ends every lookup whose lease went or names other resolvers, reads every reply that
@@ -223,20 +196,11 @@ impl Resolver {
let named = stack.lease().is_some_and(|lease| lease.dns == asking.resolvers);
let mut done = if named { None } else { Some(Err(Ended::LeaseChanged)) };
while done.is_none() {
- let Some((query, heard)) = waiting(&asking.queries, stack, reply.as_mut_slice()) else { break };
- let step = match heard {
- // The socket is connected: [udp] delivered this from port 53 of the resolver
- // the query went to, or not at all.
- Heard::Reply(len) => asking.lookup.on_datagram(query.asked, query.to, PORT, reply.get(..len).unwrap_or_default(), ms, || id(&mut *draw)),
- Heard::Unreached => {
- counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_unreached(query.asked, ms, || id(&mut *draw))
- }
- Heard::Reported => {
- counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_report(query.asked, ms, || id(&mut *draw))
- }
- };
+ let Some((query, len)) = waiting(&asking.queries, stack, reply.as_mut_slice(), counters) else { break };
+ let message = reply.get(..len).unwrap_or_default();
+ // The socket is connected: [udp] delivered this from port 53 of the resolver the
+ // query went to, or not at all.
+ let step = asking.lookup.on_datagram(query.asked, query.to, PORT, message, ms, || id(&mut *draw));
done = act(asking, step, now, stack, counters, &mut *draw);
}
if done.is_none() {
@@ -267,11 +231,9 @@ impl Resolver {
}
impl Node {
- /// Starts looking `name` up at the resolvers the held lease names, which spends two draws a
- /// query, its id and then its port: for the first query, and for each asked in its place
- /// because [udp] refused the one before. A call refused for want of a port has spent them;
- /// any other refused call spends none. A lookup no resolver could be sent a query of has
- /// started and ended: its end is in the next [`Self::take_resolved`].
+ /// Starts looking `name` up at the resolvers the held lease names, which spends two draws:
+ /// the first query's id, then its port. A call refused for want of a port has spent both;
+ /// any other refused call spends none.
pub fn resolve(&mut self, now: Instant, name: Name, mut draw: impl FnMut() -> u32) -> Result<LookupId, NotStarted> {
self.resolver.start(now, name, &mut self.stack, &mut self.counters, &mut draw)
}
diff --git a/userland/netstack/src/main.rs b/userland/netstack/src/main.rs
index 21c30f688..0bd5fcc9f 100644
--- a/userland/netstack/src/main.rs
+++ b/userland/netstack/src/main.rs
@@ -1550,9 +1550,6 @@ impl Netstack {
// whether the name or only its address is missing.
Err(Ended::Failed(Failure::NoSuchName | Failure::NoAddress)) => answer_lookup(&client, &[]),
Err(Ended::Failed(Failure::TimedOut)) => client.error(ERR_TIMED_OUT),
- Err(Ended::Failed(Failure::Unreachable)) => {
- unreachable!("netstack: no lookup here is told a query did not reach its server, and {name}'s ended so")
- }
Err(Ended::Failed(why @ (Failure::Truncated | Failure::ServerFailed(_) | Failure::TooManyAliases))) => {
say!("netstack: a lookup of {name} ended without an answer: {why:?}");
client.error(ERR_OTHER);
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..be7c09da3 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -160,7 +160,7 @@ fn act(asking: &mut Asking, mut step: Step, now: Instant, stack: &mut Stack, cou
}
counters.add(Counter::QueryUnsent, 1);
close(stack, now, socket);
- step = asking.lookup.on_unreached(asked, millis(now), || id(&mut *draw));
+ break None;
}
Step::Wait => break None,
Step::Done(result) => break Some(result.map_err(Ended::Failed)),
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..d4b2e7efe 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -230,7 +230,7 @@ impl Resolver {
Heard::Reply(len) => asking.lookup.on_datagram(query.asked, query.to, PORT, reply.get(..len).unwrap_or_default(), ms, || id(&mut *draw)),
Heard::Unreached => {
counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_unreached(query.asked, ms, || id(&mut *draw))
+ Step::Wait
}
Heard::Reported => {
counters.add(Counter::QueryFailed, 1);
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..8d38ef90b 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -615,10 +615,8 @@ impl Lookup {
let unreached = self.sent.remove(which);
self.free(unreached.to, now + WAIT_MS);
// A newer query still waits for its own answer.
- if which < self.sent.len() {
- return Step::Wait;
- }
- self.ask(now, id)
+ let _ = (now, id);
+ Step::Wait
}
/// The network said at `now` that the query `asked` did not reach its
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..18d61b52f 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -567,7 +567,8 @@ impl Lookup {
let turn = self.asked % self.servers.len();
let (to, free) = self.servers[turn];
let spent = self.asked == ROUNDS * self.servers.len();
- if !spent && now >= free {
+ let _ = free;
+ if !spent {
self.servers[turn].1 = now + WAIT_MS;
self.asked += 1;
let asked = Asked(self.next);
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..45613887e 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -614,10 +614,6 @@ impl Lookup {
};
let unreached = self.sent.remove(which);
self.free(unreached.to, now + WAIT_MS);
- // A newer query still waits for its own answer.
- if which < self.sent.len() {
- return Step::Wait;
- }
self.ask(now, id)
}
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..d16a4cb9e 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -568,7 +568,6 @@ impl Lookup {
let (to, free) = self.servers[turn];
let spent = self.asked == ROUNDS * self.servers.len();
if !spent && now >= free {
- self.servers[turn].1 = now + WAIT_MS;
self.asked += 1;
let asked = Asked(self.next);
self.next += 1;
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..861d17cab 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -612,10 +612,9 @@ impl Lookup {
let Some(which) = self.sent.iter().position(|s| s.asked == asked) else {
return Step::Wait;
};
- let unreached = self.sent.remove(which);
+ let unreached = self.sent[which];
self.free(unreached.to, now + WAIT_MS);
- // A newer query still waits for its own answer.
- if which < self.sent.len() {
+ if which + 1 < self.sent.len() {
return Step::Wait;
}
self.ask(now, id)
diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 59be88ccf..9cbee3f78 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 = 16;
+ pub const PENDING_PER_NEIGHBOUR: usize = 8;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 59be88ccf..9cbee3f78 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 = 16;
+ pub const PENDING_PER_NEIGHBOUR: usize = 8;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..990bcdc37 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -47,7 +47,7 @@ use crate::lease::Stack;
use crate::name::millis;
use crate::{Counter, Counters, Node};
-const _: () = assert!(PENDING_PER_NEIGHBOUR >= MAX_LOOKUPS);
+const _: usize = PENDING_PER_NEIGHBOUR;
/// One lookup, from [`Node::resolve`] until its answer is taken or it is let go.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..8d8a26938 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -666,7 +666,8 @@ impl Udp {
let pending = match class {
ErrorClass::Refused => SocketError::Refused,
ErrorClass::Prohibited => SocketError::Prohibited,
- ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
+ ErrorClass::PathMtu => return self.count(Counter::IcmpErrorSoft),
+ ErrorClass::Soft => SocketError::Prohibited,
};
socket.pending = Some(pending);
self.count(Counter::IcmpErrorDelivered);
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..8b6d0e153 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -581,7 +581,7 @@ impl Lookup {
// With no query left, each was answered with a failure or never
// reached its server.
let silence = if self.sent.is_empty() { Failure::Unreachable } else { Failure::TimedOut };
- return Step::Done(Err(self.failed.map_or(silence, Failure::ServerFailed)));
+ return Step::Done(Err(if self.sent.is_empty() { silence } else { self.failed.map_or(silence, Failure::ServerFailed) }));
}
if !spent {
self.due = self.due.max(free);
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..0fdade7bb 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -208,7 +208,7 @@ impl Resolver {
None => self.asking.push(asking),
// No resolver could be sent a query: the lookup has its id and its end at once, and
// holds no socket.
- Some(result) => self.ended.push(Resolved { id: started, result }),
+ Some(_) => {}
}
Ok(started)
}
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..8fe43316e 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -643,7 +643,8 @@ impl Udp {
fn connected(&mut self, flow: &Flow) -> Option<&mut Socket> {
let index = *self.ports.get(&flow.source_port)?;
self.at(index).filter(|s| {
- s.binding == Binding::Connected { local: flow.source, peer: flow.destination, peer_port: flow.destination_port }
+ let _ = flow;
+ matches!(s.binding, Binding::Connected { .. })
})
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..99d3f8c5f 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -234,7 +234,7 @@ impl Resolver {
}
Heard::Reported => {
counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_report(query.asked, ms, || id(&mut *draw))
+ asking.lookup.on_unreached(query.asked, ms, || id(&mut *draw))
}
};
done = act(asking, step, now, stack, counters, &mut *draw);
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..076c46777 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -629,6 +629,7 @@ impl Lookup {
if self.sent.last().is_none_or(|s| s.asked != asked) {
return Step::Wait;
}
+ self.sent.pop();
self.ask(now, id)
}
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..855f0c92a 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -665,7 +665,7 @@ impl Udp {
};
let pending = match class {
ErrorClass::Refused => SocketError::Refused,
- ErrorClass::Prohibited => SocketError::Prohibited,
+ ErrorClass::Prohibited => SocketError::NextHopFailed,
ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
};
socket.pending = Some(pending);
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..798a9f404 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -675,7 +675,7 @@ impl Udp {
/// [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);
+ socket.pending = Some(SocketError::Prohibited);
}
}
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..b6a81f2ff 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -577,7 +577,7 @@ impl Lookup {
self.due = now + WAIT_MS;
return Step::Ask { asked, to, query: query(id, &self.name) };
}
- if self.sent.is_empty() || (spent && now >= self.due) {
+ if self.sent.is_empty() || spent || now < self.due {
// With no query left, each was answered with a failure or never
// reached its server.
let silence = if self.sent.is_empty() { Failure::Unreachable } else { Failure::TimedOut };
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..17f643db1 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -626,7 +626,7 @@ impl Lookup {
/// sends.
pub fn on_report(&mut self, asked: Asked, now: u64, id: impl FnOnce() -> u16) -> Step {
// A newer query still waits for its own answer.
- if self.sent.last().is_none_or(|s| s.asked != asked) {
+ if !self.sent.iter().any(|s| s.asked == asked) {
return Step::Wait;
}
self.ask(now, id)
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..8bb7cdd14 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -664,7 +664,7 @@ impl Udp {
return self.count(counter);
};
let pending = match class {
- ErrorClass::Refused => SocketError::Refused,
+ ErrorClass::Refused => SocketError::NextHopFailed,
ErrorClass::Prohibited => SocketError::Prohibited,
ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
};
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..30626950c 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -661,7 +661,6 @@ impl Lookup {
self.sent.remove(which);
// It answered: its interval, which is for a query that may
// have been lost, is over.
- self.free(from, now);
// The next server is asked now, unless a newer query is still
// waiting for its own answer.
if which == self.sent.len() {
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..2dad223d9 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -613,7 +613,7 @@ impl Lookup {
return Step::Wait;
};
let unreached = self.sent.remove(which);
- self.free(unreached.to, now + WAIT_MS);
+ let _ = unreached;
// A newer query still waits for its own answer.
if which < self.sent.len() {
return Step::Wait;
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..353ce59a6 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -583,9 +583,6 @@ impl Lookup {
let silence = if self.sent.is_empty() { Failure::Unreachable } else { Failure::TimedOut };
return Step::Done(Err(self.failed.map_or(silence, Failure::ServerFailed)));
}
- if !spent {
- self.due = self.due.max(free);
- }
Step::Wait
}
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..97c899740 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -682,9 +682,6 @@ impl Lookup {
self.sent.clear();
self.asked = 0;
self.failed = None;
- for (_, free) in &mut self.servers {
- *free = now;
- }
self.ask(now, id)
}
},
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..9ef8f06d3 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -577,7 +577,7 @@ impl Lookup {
self.due = now + WAIT_MS;
return Step::Ask { asked, to, query: query(id, &self.name) };
}
- if self.sent.is_empty() || (spent && now >= self.due) {
+ if self.sent.is_empty() || spent {
// With no query left, each was answered with a failure or never
// reached its server.
let silence = if self.sent.is_empty() { Failure::Unreachable } else { Failure::TimedOut };
diff --git a/toyos-dns/src/tests.rs b/toyos-dns/src/tests.rs
index 7bfc26d59..92540b0c2 100644
--- a/toyos-dns/src/tests.rs
+++ b/toyos-dns/src/tests.rs
@@ -1074,6 +1074,80 @@ fn a_reported_query_the_caller_then_knows_reached_nobody_is_let_go() {
assert_eq!(lookup.on_unreached(q1, 3, undrawn), Step::Done(Err(Failure::Unreachable)), "no query is left to read an answer for");
}
+// --- Whoever reports or fails what ---
+//
+// RFC 1035 §4.2.1's interval is a server's: one that has not answered is not
+// sent a second query inside it, whatever is said of its first and whatever
+// another server answers. And a lookup does not end on another server's
+// failure while a query's answer is read and its wait is not over.
+
+// S1's every query is reported the moment it is sent, and S2 answers each of
+// its own with a failure at once.
+#[test]
+fn reports_and_another_servers_failures_hurry_no_server_that_has_not_answered_and_end_no_lookup_early() {
+ let refused = reply(2, OK | 5, "www.example", &[]);
+ let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 1).unwrap();
+ let (q1, ..) = asked(&first);
+ let mut sent = vec![(0, S1)];
+ let mut newest = q1;
+ let mut now = 0;
+ let ended = loop {
+ let step = lookup.on_report(newest, now, || 2);
+ assert_eq!(to(&step), (S2, 2), "the other server, at once");
+ sent.push((now, S2));
+ assert_eq!(lookup.on_datagram(asked(&step).0, S2, PORT, &refused, now, || 9), Step::Wait, "S1 has not answered and was asked this millisecond");
+ assert_eq!(lookup.on_report(newest, now, || 9), Step::Wait, "the same report again");
+ assert_eq!(lookup.on_time(now + WAIT_MS - 1, || 9), Step::Wait, "the wait for S1 was cut short");
+ now += WAIT_MS;
+ match lookup.on_time(now, || 1) {
+ Step::Done(result) => break result,
+ step => {
+ assert_eq!(to(&step), (S1, 1));
+ sent.push((now, S1));
+ newest = asked(&step).0;
+ }
+ }
+ };
+ let rounds: Vec<(u64, [u8; 4])> = (0..ROUNDS as u64).flat_map(|round| [(round * WAIT_MS, S1), (round * WAIT_MS, S2)]).collect();
+ assert_eq!(sent, rounds, "each server once a round, the rounds a wait apart");
+ assert_eq!((now, ended), (ROUNDS as u64 * WAIT_MS, Err(Failure::ServerFailed(5))));
+ assert_eq!(waiting(&lookup).len(), ROUNDS, "every query to S1 was read to the end");
+
+ // And S1's answer to its first query, a millisecond before that query's
+ // wait is over, is the lookup's.
+ let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 1).unwrap();
+ let (q1, ..) = asked(&first);
+ let second = lookup.on_report(q1, 0, || 2);
+ assert_eq!(lookup.on_datagram(asked(&second).0, S2, PORT, &refused, 0, || 9), Step::Wait);
+ assert_eq!(waiting(&lookup), [q1]);
+ let ok = reply(1, OK, "www.example", &[a("www.example", [1, 2, 3, 4])]);
+ assert_eq!(lookup.on_datagram(q1, S1, PORT, &ok, WAIT_MS - 1, || 9), Step::Done(Ok(vec![[1, 2, 3, 4]])));
+}
+
+// The same of a query the caller knows did not leave: S1 is not asked again
+// inside its interval because S2 failed, and with no answer read the lookup
+// ends by S2's word.
+#[test]
+fn a_server_not_reached_is_not_asked_again_at_once_because_another_failed() {
+ let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 1).unwrap();
+ let second = lookup.on_unreached(asked(&first).0, 0, || 2);
+ assert_eq!(to(&second), (S2, 2));
+ let refused = reply(2, OK | 5, "www.example", &[]);
+ assert_eq!(lookup.on_datagram(asked(&second).0, S2, PORT, &refused, 0, || 9), Step::Done(Err(Failure::ServerFailed(5))), "S1 was asked this millisecond, and no query is read");
+
+ // With a query to S1 still read, the lookup waits for it instead.
+ let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 1).unwrap();
+ let (q1, ..) = asked(&first);
+ let second = lookup.on_time(WAIT_MS, || 2);
+ let third = lookup.on_datagram(asked(&second).0, S2, PORT, &refused, WAIT_MS, || 3);
+ assert_eq!(to(&third), (S1, 3), "S1 again, a wait after its first query");
+ let fourth = lookup.on_unreached(asked(&third).0, WAIT_MS, || 2);
+ assert_eq!(to(&fourth), (S2, 2), "S2 answered, and is asked again at once like any server that fails");
+ assert_eq!(lookup.on_datagram(asked(&fourth).0, S2, PORT, &refused, WAIT_MS, || 9), Step::Wait, "S1 was asked this millisecond");
+ assert_eq!(waiting(&lookup), [q1]);
+ assert_eq!(lookup.due(), 2 * WAIT_MS);
+}
+
#[test]
fn a_server_that_answered_with_a_failure_is_what_a_lookup_that_then_reached_nobody_reports() {
let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 1).unwrap();
diff --git a/userland/netstack/node/tests/resolve.rs b/userland/netstack/node/tests/resolve.rs
index 0a922149b..d67ae6ddd 100644
--- a/userland/netstack/node/tests/resolve.rs
+++ b/userland/netstack/node/tests/resolve.rs
@@ -30,6 +30,9 @@ const SILENT: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 52);
/// On the link and answering ARP, with nothing on its port 53: it reports every query back.
const REFUSES: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 54);
const MAC_F: [u8; 6] = [2, 0, 0, 0, 0, 0x54];
+/// On the link, answering ARP and every query with RCODE 5, refused (RFC 1035 §4.1.1).
+const FAILS: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 55);
+const MAC_X: [u8; 6] = [2, 0, 0, 0, 0, 0x55];
/// Off the link, asked of a lease that names no router.
const UNROUTED: Ipv4Addr = Ipv4Addr::new(198, 51, 100, 53);
const UNROUTED_TOO: Ipv4Addr = Ipv4Addr::new(198, 51, 100, 54);
@@ -191,6 +194,8 @@ struct Net {
queried: Vec<Query>,
/// Every query that left for [`REFUSES`], in order.
refused: Vec<Query>,
+ /// Every query that left for [`FAILS`], in order.
+ failed: Vec<Query>,
ended: Vec<Resolved>,
/// The last draw [`Self::resolve`] handed out: its ids are 0x8000 and up, and those the node
/// draws for itself from `lan`'s count are below it.
@@ -219,6 +224,7 @@ impl Net {
held: Vec::new(),
queried: Vec::new(),
refused: Vec::new(),
+ failed: Vec::new(),
ended: Vec::new(),
draws: 0x7c00_8000,
}
@@ -263,6 +269,12 @@ impl Net {
match seen {
Seen::Arp { request: true, target, .. } if *target == ANSWERS => self.lan.deliver(&arp(MAC, false, MAC_S, ANSWERS, A)),
Seen::Arp { request: true, target, .. } if *target == REFUSES => self.lan.deliver(&arp(MAC, false, MAC_F, REFUSES, A)),
+ Seen::Arp { request: true, target, .. } if *target == FAILS => self.lan.deliver(&arp(MAC, false, MAC_X, FAILS, A)),
+ Seen::Udp(udp) if udp.port == 53 && udp.destination == FAILS => {
+ assert_eq!(udp.to, MAC_X, "a query left for a server the wire cannot reach");
+ self.held.push((self.lan.now, lan::udp(MAC, MAC_X, (FAILS, 53), (A, udp.source_port), &reply(&udp.payload, RESPONSE | 5, &[]))));
+ self.failed.push(Query { at, udp: udp.clone(), id: u16::from_be_bytes([udp.payload[0], udp.payload[1]]), name: asked_name(&udp.payload) });
+ }
Seen::Udp(udp) if udp.port == 53 && udp.destination == REFUSES => {
assert_eq!(udp.to, MAC_F, "a query left for a server the wire cannot reach");
self.held.push((self.lan.now, reports(PORT_UNREACHABLE, (REFUSES, MAC_F), udp)));
@@ -617,6 +629,32 @@ fn a_forged_report_of_the_querys_own_addresses_and_ports_ends_nothing_and_the_an
assert_eq!(net.lan.node.take_resolved(), [Resolved { id, result: Ok(vec![ADDRESS]) }]);
}
+// And with a second resolver that answers every query with a failure, which a report of each
+// query to the first would chain into a round of queries with no wait between them: every query
+// that leaves for the answering resolver is reported in its name the moment it is on the wire,
+// until none follows. One has left for each resolver, the lookup has not ended, and the answer
+// the first resolver gives a millisecond before its query's wait is over ends it. RFC 1035
+// §4.2.1: the interval is the resolver's, whoever says what of its query.
+#[test]
+fn forged_reports_and_another_resolvers_failures_end_nothing_and_the_answer_behind_them_is_taken() {
+ let mut net = Net::leased(&[ANSWERS, FAILS], Some(R));
+ net.zone.push(("www.example", WAIT_MS - 1, Says::Address(ADDRESS)));
+ let id = net.resolve("www.example").unwrap();
+ let mut reported = 0;
+ for _ in 0..=2 * ROUNDS {
+ net.pass();
+ let Some(query) = net.queried.get(reported).cloned() else { break };
+ net.lan.deliver(&reports(PORT_UNREACHABLE, (ANSWERS, MAC_S), &query.udp));
+ reported += 1;
+ }
+ assert_eq!(net.lan.node.counters().get(Counter::QueryFailed), u64::try_from(reported).unwrap(), "the premise: every report reached its query's socket");
+ assert_eq!((net.queried.len(), net.failed.len(), net.ms()), (1, 1, 0), "one query to each resolver, the second's at the report of the first");
+ assert_eq!(net.lan.node.take_resolved(), NONE, "reports and another resolver's failure ended the lookup");
+ assert_eq!(net.run(id, 10 * WAIT_MS), Some(Ok(vec![ADDRESS])), "the resolvers heard {:?} and {:?}", net.queried, net.failed);
+ assert_eq!(net.ms(), WAIT_MS - 1);
+ assert_eq!((net.queried.len(), net.failed.len()), (1, 1));
+}
+
// RFC 1034 §5.3.3: an alias with no address is asked again at its end, every query afresh. The
// first resolver answers only after six queries have left, three of them to it, and with an alias
// alone: the queries for the old name are let go, so the two other answers to them, arriving |
Review, round 3, of
|
…er stage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…nd a lookup wakes at the next server's turn Round 3 of the review of #781, finding 6. A send started the interval of one place in the server list while every other writer keyed it by address, so a lease that names one resolver twice got a second query in the same millisecond on a report. The send calls `free` like the rest: one key for an interval. Where the newest query is not waited for and the next server may not be asked yet, `due` is that server's turn, and no longer the later of it and the newest query's own wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Round 4: mutation patches and the script that ran them, at
#!/bin/sh
# Each mutation: a checked patch applied to the clean committed tree, shown to
# build (or not), run, and reversed. Logs: $S/logs4/<mutation>-builds.log and
# $S/logs4/<mutation>.log, the command first and the exit code last.
W=<worktree>
S=<scratchpad>/orch/ownstack/resolver
P="-p toyos-dns -p toyos-net-node -p toyos-net-ip -p toyos-net-udp -p toyos-net-shard"
cd "$W" || exit 2
echo "HEAD=$(git rev-parse HEAD)"
clean() { [ -z "$(git status --porcelain --ignore-submodules=none)" ]; }
clean || { echo "the worktree is not clean; nothing run"; exit 2; }
for patch in "$S"/mutations4/${1:-m}*.patch; do
name=$(basename "$patch" .patch)
git apply --check "$patch" || { echo "$name does not apply"; exit 2; }
git apply "$patch"
# shellcheck disable=SC2086
{ echo "\$ cargo test --locked --no-run $P"; echo "HEAD=$(git rev-parse HEAD) + $name"; cargo test --locked --no-run $P; echo "EXIT=$?"; } > "$S/logs4/$name-builds.log" 2>&1
built=$(tail -1 "$S/logs4/$name-builds.log")
if [ "$built" = "EXIT=0" ]; then
# shellcheck disable=SC2086
{ echo "\$ cargo test --locked --no-fail-fast $P"; echo "HEAD=$(git rev-parse HEAD) + $name"; cargo test --locked --no-fail-fast $P; echo "EXIT=$?"; } > "$S/logs4/$name.log" 2>&1
ran=$(tail -1 "$S/logs4/$name.log")
else
ran="not run"
fi
git apply -R "$patch"
clean || { echo "$name left the tree dirty"; exit 2; }
echo "$name builds $built tests $ran"
done
echo "HEAD=$(git rev-parse HEAD) clean=$(clean && echo yes || echo no)"
echo "MUTATIONS DONE"
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 1712abefd..1f46cdaa9 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -431,12 +431,8 @@ pub enum Failure {
NoSuchName,
/// The name exists and has no `A` record (RFC 2308 §2.2).
NoAddress,
- /// No server answered, and a query that may have reached one was given
- /// its [`WAIT_MS`].
+ /// Every query went unanswered for [`WAIT_MS`].
TimedOut,
- /// No query reached a server ([`Lookup::on_unreached`]) and none is left
- /// to send at once: there is no answer to wait for.
- Unreachable,
/// The answer did not fit a UDP reply (TC).
Truncated,
/// Every server that answered could not answer, the last with this RCODE.
@@ -482,49 +478,21 @@ struct Sent {
/// any query this lookup sent for the name now asked is read, so an answer
/// late past its query's wait still ends the lookup; it is read only on the
/// query's own port, with the query's ID, from the server the query went to.
-///
-/// **A wait is for an answer that can come.** When the newest query is
-/// answered with a failure, or known or said not to have reached its server,
-/// the next server is asked at once: RFC 1035 §4.2.1's interval of two to
-/// five seconds is between repetitions of a query that may have been lost on
-/// its way, and the same section has the other servers tried first. **The
-/// interval is each server's**: one that has not answered is sent no second
-/// query inside [`WAIT_MS`] of its last, or of the caller's last knowledge
-/// that it was not reached, whatever is said of that query and whatever
-/// another server answers; a server is its address, however often the list
-/// names it. Where the next server cannot be asked yet, the lookup waits
-/// until it can while any answer is still read, and ends at once only with
-/// none.
-///
-/// **What the caller knows and what it was told are two calls.** A query the
-/// caller itself knows never reached its server ([`Lookup::on_unreached`]) is
-/// let go, and a lookup with no query left whose answer is read ends with
-/// [`Failure::Unreachable`]. A report from the network
-/// ([`Lookup::on_report`]) is anyone's word (RFC 8085 §5.2): it lets no query
-/// go and ends nothing, so the answer to a query reported so is still read.
#[derive(Debug)]
pub struct Lookup {
/// The name now asked: the one given, or the last alias followed.
name: Name,
/// Aliases followed so far, counted against [`MAX_ALIASES`].
aliases: usize,
- /// Each server, and when it may be asked again: [`WAIT_MS`] after its
- /// last query or after it was last known not to be reached, and at once
- /// when it has answered any query, an older one's late failure included.
- /// An address named twice is one server: every place it has carries the
- /// same time.
- servers: Vec<([u8; 4], u64)>,
+ servers: Vec<[u8; 4]>,
/// Queries sent for `name`, oldest first; one answered with a server
- /// failure, or known not to have reached its server, is taken out.
- /// [`Lookup::waiting`] is this list, which holds a query while the
- /// lookup lasts.
+ /// failure is taken out. [`Lookup::waiting`] is this list.
sent: Vec<Sent>,
/// The [`Asked`] the next query gets: none is given twice in a lookup.
next: u32,
/// Queries sent for `name`, answered or not.
asked: usize,
- /// When the newest query is given up on; or, where it is not waited for
- /// and the next server may not be asked yet, that server's turn.
+ /// When the newest query is given up on.
due: u64,
/// The RCODE of the last server failure, which is what a lookup that runs
/// out of queries reports if any server answered at all.
@@ -541,7 +509,7 @@ impl Lookup {
let mut lookup = Self {
name,
aliases: 0,
- servers: servers.iter().map(|server| (*server, now)).collect(),
+ servers: servers.to_vec(),
sent: Vec::new(),
next: 0,
asked: 0,
@@ -563,41 +531,22 @@ impl Lookup {
self.sent.iter().map(|s| s.asked)
}
- /// The next server's query, if it may be asked at `now`; or the wait for
- /// the answers still read, until it may or the last query's wait is
- /// over; or the end.
+ /// The next query for `name`, or the end where every one has been sent.
fn ask(&mut self, now: u64, id: impl FnOnce() -> u16) -> Step {
- let turn = self.asked % self.servers.len();
- let (to, free) = self.servers[turn];
- let spent = self.asked == ROUNDS * self.servers.len();
- if !spent && now >= free {
- self.free(to, now + WAIT_MS);
- self.asked += 1;
- let asked = Asked(self.next);
- self.next += 1;
- let id = id();
- self.sent.push(Sent { asked, id, to });
- self.due = now + WAIT_MS;
- return Step::Ask { asked, to, query: query(id, &self.name) };
- }
- if self.sent.is_empty() || (spent && now >= self.due) {
- // With no query left, each was answered with a failure or never
- // reached its server.
- let silence = if self.sent.is_empty() { Failure::Unreachable } else { Failure::TimedOut };
- return Step::Done(Err(self.failed.map_or(silence, Failure::ServerFailed)));
- }
- if !spent {
- // The newest query is not waited for: the next server's turn is.
- self.due = free;
- }
- Step::Wait
- }
-
- /// `server` may next be asked at `at`.
- fn free(&mut self, server: [u8; 4], at: u64) {
- for (_, free) in self.servers.iter_mut().filter(|(addr, _)| *addr == server) {
- *free = at;
+ if self.asked == ROUNDS * self.servers.len() {
+ return Step::Done(Err(match self.failed {
+ Some(rcode) => Failure::ServerFailed(rcode),
+ None => Failure::TimedOut,
+ }));
}
+ let to = self.servers[self.asked % self.servers.len()];
+ self.asked += 1;
+ let asked = Asked(self.next);
+ self.next += 1;
+ let id = id();
+ self.sent.push(Sent { asked, id, to });
+ self.due = now + WAIT_MS;
+ Step::Ask { asked, to, query: query(id, &self.name) }
}
/// The time is `now`: the newest query has had its [`WAIT_MS`] where it
@@ -609,33 +558,6 @@ impl Lookup {
self.ask(now, id)
}
- /// The caller knows at `now`, of its own knowledge, that the query
- /// `asked` did not reach its server: it was never sent. Its answer is
- /// read no more. `id` draws the ID of the query this sends.
- pub fn on_unreached(&mut self, asked: Asked, now: u64, id: impl FnOnce() -> u16) -> Step {
- let Some(which) = self.sent.iter().position(|s| s.asked == asked) else {
- return Step::Wait;
- };
- let unreached = self.sent.remove(which);
- self.free(unreached.to, now + WAIT_MS);
- // A newer query still waits for its own answer.
- if which < self.sent.len() {
- return Step::Wait;
- }
- self.ask(now, id)
- }
-
- /// The network said at `now` that the query `asked` did not reach its
- /// server. Its answer is still read. `id` draws the ID of the query this
- /// sends.
- pub fn on_report(&mut self, asked: Asked, now: u64, id: impl FnOnce() -> u16) -> Step {
- // A newer query still waits for its own answer.
- if self.sent.last().is_none_or(|s| s.asked != asked) {
- return Step::Wait;
- }
- self.ask(now, id)
- }
-
/// `msg` arrived at `now` from `port` of `from`, on the port `asked` was
/// sent from. `id` draws the ID of the query this sends.
pub fn on_datagram(
@@ -663,9 +585,6 @@ impl Lookup {
Verdict::ServerFailed(rcode) => {
self.failed = Some(rcode);
self.sent.remove(which);
- // It answered: its interval, which is for a query that may
- // have been lost, is over.
- self.free(from, now);
// The next server is asked now, unless a newer query is still
// waiting for its own answer.
if which == self.sent.len() {
@@ -686,9 +605,6 @@ impl Lookup {
self.sent.clear();
self.asked = 0;
self.failed = None;
- for (_, free) in &mut self.servers {
- *free = now;
- }
self.ask(now, id)
}
},
diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 59be88ccf..9cbee3f78 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 = 16;
+ pub const PENDING_PER_NEIGHBOUR: usize = 8;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..a5fa0f73a 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -113,17 +113,11 @@ pub enum Binding {
Connected { local: Ipv4Addr, peer: Ipv4Addr, peer_port: Port },
}
-/// An error against a connected socket's datagrams: its next call returns it once. Two are what
-/// an ICMP message said, which anyone who knows the socket's addresses and ports can send; one
-/// is [ip]'s own.
+/// An error the network reported against a connected socket: its next call returns it once.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SocketError {
- /// An ICMP error said the peer refuses the protocol or the port.
Refused,
- /// An ICMP error said the way to the peer is administratively prohibited.
- Prohibited,
- /// [ip] found no link address for the next hop of a datagram it held: nothing left.
- NextHopFailed,
+ Unreachable,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -665,7 +659,7 @@ impl Udp {
};
let pending = match class {
ErrorClass::Refused => SocketError::Refused,
- ErrorClass::Prohibited => SocketError::Prohibited,
+ ErrorClass::Prohibited => SocketError::Unreachable,
ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
};
socket.pending = Some(pending);
@@ -675,7 +669,7 @@ impl Udp {
/// [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);
+ socket.pending = Some(SocketError::Unreachable);
}
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..d8a5cfc23 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -10,21 +10,9 @@
//! query other than the one whose socket it reached.
//! - **A socket lives as long as its query's answer is read**: it is closed when the lookup lets
//! the query go, ends, or is let go itself, and its port is free from then.
-//! - **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 [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.
-//! - **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 query [udp] refuses never left**: it is counted, holds no socket, and the lookup waits
+//! its wait out as for any query nobody answered. So does one the network reports back, its
+//! resolver unreachable or its port refused: counted, and waited out.
//! - **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.
@@ -39,16 +27,13 @@ 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_udp::{Error, SocketId};
use toyos_net_wire::Instant;
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,51 +105,42 @@ fn close(stack: &mut Stack, now: Instant, socket: SocketId) {
}
}
-/// What waits at a query's socket.
-enum Heard {
- /// A datagram of this length, now in the reply buffer.
- Reply(usize),
- /// [ip]'s word that the query never left.
- Unreached,
- /// An ICMP error's word that the query was refused.
- Reported,
-}
-
-/// The first of `queries` with something at its socket, and what: a reply is now in `reply`.
-fn waiting(queries: &[Query], stack: &mut Stack, reply: &mut [u8]) -> Option<(Query, Heard)> {
+/// The length of the next reply waiting at one of `queries`' sockets, now in `reply`, and the
+/// query it is for. An error the network reported against a query is counted and read past.
+fn waiting(queries: &[Query], stack: &mut Stack, reply: &mut [u8], counters: &mut Counters) -> Option<(Query, usize)> {
for query in queries {
- match stack.recv_from(query.socket, reply) {
- Ok(Some(received)) => return Some((*query, Heard::Reply(received.len))),
- Ok(None) => {}
- Err(Error::Failed(SocketError::NextHopFailed)) => return Some((*query, Heard::Unreached)),
- Err(Error::Failed(SocketError::Refused | SocketError::Prohibited)) => return Some((*query, Heard::Reported)),
- Err(Error::NoSuchSocket | Error::Refused(_)) => unreachable!("a query's socket is open while it is listed, and no rule refuses a receive"),
+ loop {
+ match stack.recv_from(query.socket, reply) {
+ Ok(Some(received)) => return Some((*query, received.len)),
+ Ok(None) => break,
+ Err(Error::Failed(_)) => counters.add(Counter::QueryFailed, 1),
+ Err(Error::NoSuchSocket | Error::Refused(_)) => unreachable!("a query's socket is open while it is listed, and no rule refuses a receive"),
+ }
}
}
None
}
-/// 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.
-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 } => {
- // An any-address bind that names no port is refused only for want of a free one.
- let Ok((socket, _)) = stack.bind(Ipv4Addr::UNSPECIFIED, None, &mut *draw) else { break Some(Err(Ended::NoPort)) };
+/// Carries out `step` for `asking`, then closes the socket of every query the lookup no longer
+/// reads an answer for. Returns how the lookup ended, if it did.
+fn act(asking: &mut Asking, step: Step, now: Instant, stack: &mut Stack, counters: &mut Counters, draw: &mut impl FnMut() -> u32) -> Option<Result<Vec<[u8; 4]>, Ended>> {
+ let done = match step {
+ // An any-address bind that names no port is refused only for want of a free one.
+ Step::Ask { asked, to, query } => match stack.bind(Ipv4Addr::UNSPECIFIED, None, &mut *draw) {
+ Ok((socket, _)) => {
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 });
- break None;
+ } else {
+ counters.add(Counter::QueryUnsent, 1);
+ close(stack, now, socket);
}
- counters.add(Counter::QueryUnsent, 1);
- close(stack, now, socket);
- step = asking.lookup.on_unreached(asked, millis(now), || id(&mut *draw));
+ None
}
- Step::Wait => break None,
- Step::Done(result) => break Some(result.map_err(Ended::Failed)),
- }
+ Err(_) => Some(Err(Ended::NoPort)),
+ },
+ Step::Wait => None,
+ Step::Done(result) => Some(result.map_err(Ended::Failed)),
};
let lookup = &asking.lookup;
asking.queries.retain(|query| {
@@ -198,19 +174,16 @@ 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);
- if done == Some(Err(Ended::NoPort)) {
- return Err(NotStarted::ResourceExhausted);
- }
- self.next = self.next.wrapping_add(1);
- let started = asking.id;
- match done {
- None => self.asking.push(asking),
- // No resolver could be sent a query: the lookup has its id and its end at once, and
- // holds no socket.
- Some(result) => self.ended.push(Resolved { id: started, result }),
+ match act(&mut asking, step, now, stack, counters, &mut *draw) {
+ None => {
+ self.next = self.next.wrapping_add(1);
+ let started = asking.id;
+ self.asking.push(asking);
+ Ok(started)
+ }
+ Some(Err(Ended::NoPort)) => Err(NotStarted::ResourceExhausted),
+ Some(other) => unreachable!("a lookup's first step is a query, not {other:?}"),
}
- Ok(started)
}
/// Ends every lookup whose lease went or names other resolvers, reads every reply that
@@ -223,20 +196,11 @@ impl Resolver {
let named = stack.lease().is_some_and(|lease| lease.dns == asking.resolvers);
let mut done = if named { None } else { Some(Err(Ended::LeaseChanged)) };
while done.is_none() {
- let Some((query, heard)) = waiting(&asking.queries, stack, reply.as_mut_slice()) else { break };
- let step = match heard {
- // The socket is connected: [udp] delivered this from port 53 of the resolver
- // the query went to, or not at all.
- Heard::Reply(len) => asking.lookup.on_datagram(query.asked, query.to, PORT, reply.get(..len).unwrap_or_default(), ms, || id(&mut *draw)),
- Heard::Unreached => {
- counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_unreached(query.asked, ms, || id(&mut *draw))
- }
- Heard::Reported => {
- counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_report(query.asked, ms, || id(&mut *draw))
- }
- };
+ let Some((query, len)) = waiting(&asking.queries, stack, reply.as_mut_slice(), counters) else { break };
+ let message = reply.get(..len).unwrap_or_default();
+ // The socket is connected: [udp] delivered this from port 53 of the resolver the
+ // query went to, or not at all.
+ let step = asking.lookup.on_datagram(query.asked, query.to, PORT, message, ms, || id(&mut *draw));
done = act(asking, step, now, stack, counters, &mut *draw);
}
if done.is_none() {
@@ -267,11 +231,9 @@ impl Resolver {
}
impl Node {
- /// Starts looking `name` up at the resolvers the held lease names, which spends two draws a
- /// query, its id and then its port: for the first query, and for each asked in its place
- /// because [udp] refused the one before. A call refused for want of a port has spent them;
- /// any other refused call spends none. A lookup no resolver could be sent a query of has
- /// started and ended: its end is in the next [`Self::take_resolved`].
+ /// Starts looking `name` up at the resolvers the held lease names, which spends two draws:
+ /// the first query's id, then its port. A call refused for want of a port has spent both;
+ /// any other refused call spends none.
pub fn resolve(&mut self, now: Instant, name: Name, mut draw: impl FnMut() -> u32) -> Result<LookupId, NotStarted> {
self.resolver.start(now, name, &mut self.stack, &mut self.counters, &mut draw)
}
diff --git a/userland/netstack/src/main.rs b/userland/netstack/src/main.rs
index 21c30f688..0bd5fcc9f 100644
--- a/userland/netstack/src/main.rs
+++ b/userland/netstack/src/main.rs
@@ -1550,9 +1550,6 @@ impl Netstack {
// whether the name or only its address is missing.
Err(Ended::Failed(Failure::NoSuchName | Failure::NoAddress)) => answer_lookup(&client, &[]),
Err(Ended::Failed(Failure::TimedOut)) => client.error(ERR_TIMED_OUT),
- Err(Ended::Failed(Failure::Unreachable)) => {
- unreachable!("netstack: no lookup here is told a query did not reach its server, and {name}'s ended so")
- }
Err(Ended::Failed(why @ (Failure::Truncated | Failure::ServerFailed(_) | Failure::TooManyAliases))) => {
say!("netstack: a lookup of {name} ended without an answer: {why:?}");
client.error(ERR_OTHER);
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..be7c09da3 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -160,7 +160,7 @@ fn act(asking: &mut Asking, mut step: Step, now: Instant, stack: &mut Stack, cou
}
counters.add(Counter::QueryUnsent, 1);
close(stack, now, socket);
- step = asking.lookup.on_unreached(asked, millis(now), || id(&mut *draw));
+ break None;
}
Step::Wait => break None,
Step::Done(result) => break Some(result.map_err(Ended::Failed)),
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..d4b2e7efe 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -230,7 +230,7 @@ impl Resolver {
Heard::Reply(len) => asking.lookup.on_datagram(query.asked, query.to, PORT, reply.get(..len).unwrap_or_default(), ms, || id(&mut *draw)),
Heard::Unreached => {
counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_unreached(query.asked, ms, || id(&mut *draw))
+ Step::Wait
}
Heard::Reported => {
counters.add(Counter::QueryFailed, 1);
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..8d38ef90b 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -615,10 +615,8 @@ impl Lookup {
let unreached = self.sent.remove(which);
self.free(unreached.to, now + WAIT_MS);
// A newer query still waits for its own answer.
- if which < self.sent.len() {
- return Step::Wait;
- }
- self.ask(now, id)
+ let _ = (now, id);
+ Step::Wait
}
/// The network said at `now` that the query `asked` did not reach its
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 1712abefd..394ae25e1 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -570,7 +570,7 @@ impl Lookup {
let turn = self.asked % self.servers.len();
let (to, free) = self.servers[turn];
let spent = self.asked == ROUNDS * self.servers.len();
- if !spent && now >= free {
+ if !spent && free < u64::MAX {
self.free(to, now + WAIT_MS);
self.asked += 1;
let asked = Asked(self.next);
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..45613887e 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -614,10 +614,6 @@ impl Lookup {
};
let unreached = self.sent.remove(which);
self.free(unreached.to, now + WAIT_MS);
- // A newer query still waits for its own answer.
- if which < self.sent.len() {
- return Step::Wait;
- }
self.ask(now, id)
}
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 1712abefd..57d195f25 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -571,7 +571,6 @@ impl Lookup {
let (to, free) = self.servers[turn];
let spent = self.asked == ROUNDS * self.servers.len();
if !spent && now >= free {
- self.free(to, now + WAIT_MS);
self.asked += 1;
let asked = Asked(self.next);
self.next += 1;
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..861d17cab 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -612,10 +612,9 @@ impl Lookup {
let Some(which) = self.sent.iter().position(|s| s.asked == asked) else {
return Step::Wait;
};
- let unreached = self.sent.remove(which);
+ let unreached = self.sent[which];
self.free(unreached.to, now + WAIT_MS);
- // A newer query still waits for its own answer.
- if which < self.sent.len() {
+ if which + 1 < self.sent.len() {
return Step::Wait;
}
self.ask(now, id)
diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 59be88ccf..9cbee3f78 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 = 16;
+ pub const PENDING_PER_NEIGHBOUR: usize = 8;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 59be88ccf..9cbee3f78 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 = 16;
+ pub const PENDING_PER_NEIGHBOUR: usize = 8;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..990bcdc37 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -47,7 +47,7 @@ use crate::lease::Stack;
use crate::name::millis;
use crate::{Counter, Counters, Node};
-const _: () = assert!(PENDING_PER_NEIGHBOUR >= MAX_LOOKUPS);
+const _: usize = PENDING_PER_NEIGHBOUR;
/// One lookup, from [`Node::resolve`] until its answer is taken or it is let go.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..8d8a26938 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -666,7 +666,8 @@ impl Udp {
let pending = match class {
ErrorClass::Refused => SocketError::Refused,
ErrorClass::Prohibited => SocketError::Prohibited,
- ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
+ ErrorClass::PathMtu => return self.count(Counter::IcmpErrorSoft),
+ ErrorClass::Soft => SocketError::Prohibited,
};
socket.pending = Some(pending);
self.count(Counter::IcmpErrorDelivered);
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 1712abefd..0d2a1fbf6 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -584,7 +584,7 @@ impl Lookup {
// With no query left, each was answered with a failure or never
// reached its server.
let silence = if self.sent.is_empty() { Failure::Unreachable } else { Failure::TimedOut };
- return Step::Done(Err(self.failed.map_or(silence, Failure::ServerFailed)));
+ return Step::Done(Err(if self.sent.is_empty() { silence } else { self.failed.map_or(silence, Failure::ServerFailed) }));
}
if !spent {
// The newest query is not waited for: the next server's turn is.
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..0fdade7bb 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -208,7 +208,7 @@ impl Resolver {
None => self.asking.push(asking),
// No resolver could be sent a query: the lookup has its id and its end at once, and
// holds no socket.
- Some(result) => self.ended.push(Resolved { id: started, result }),
+ Some(_) => {}
}
Ok(started)
}
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..8fe43316e 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -643,7 +643,8 @@ impl Udp {
fn connected(&mut self, flow: &Flow) -> Option<&mut Socket> {
let index = *self.ports.get(&flow.source_port)?;
self.at(index).filter(|s| {
- s.binding == Binding::Connected { local: flow.source, peer: flow.destination, peer_port: flow.destination_port }
+ let _ = flow;
+ matches!(s.binding, Binding::Connected { .. })
})
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..99d3f8c5f 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -234,7 +234,7 @@ impl Resolver {
}
Heard::Reported => {
counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_report(query.asked, ms, || id(&mut *draw))
+ asking.lookup.on_unreached(query.asked, ms, || id(&mut *draw))
}
};
done = act(asking, step, now, stack, counters, &mut *draw);
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..076c46777 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -629,6 +629,7 @@ impl Lookup {
if self.sent.last().is_none_or(|s| s.asked != asked) {
return Step::Wait;
}
+ self.sent.pop();
self.ask(now, id)
}
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..855f0c92a 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -665,7 +665,7 @@ impl Udp {
};
let pending = match class {
ErrorClass::Refused => SocketError::Refused,
- ErrorClass::Prohibited => SocketError::Prohibited,
+ ErrorClass::Prohibited => SocketError::NextHopFailed,
ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
};
socket.pending = Some(pending);
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..798a9f404 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -675,7 +675,7 @@ impl Udp {
/// [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);
+ socket.pending = Some(SocketError::Prohibited);
}
}
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..b6a81f2ff 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -577,7 +577,7 @@ impl Lookup {
self.due = now + WAIT_MS;
return Step::Ask { asked, to, query: query(id, &self.name) };
}
- if self.sent.is_empty() || (spent && now >= self.due) {
+ if self.sent.is_empty() || spent || now < self.due {
// With no query left, each was answered with a failure or never
// reached its server.
let silence = if self.sent.is_empty() { Failure::Unreachable } else { Failure::TimedOut };
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..17f643db1 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -626,7 +626,7 @@ impl Lookup {
/// sends.
pub fn on_report(&mut self, asked: Asked, now: u64, id: impl FnOnce() -> u16) -> Step {
// A newer query still waits for its own answer.
- if self.sent.last().is_none_or(|s| s.asked != asked) {
+ if !self.sent.iter().any(|s| s.asked == asked) {
return Step::Wait;
}
self.ask(now, id)
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..8bb7cdd14 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -664,7 +664,7 @@ impl Udp {
return self.count(counter);
};
let pending = match class {
- ErrorClass::Refused => SocketError::Refused,
+ ErrorClass::Refused => SocketError::NextHopFailed,
ErrorClass::Prohibited => SocketError::Prohibited,
ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
};
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..30626950c 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -661,7 +661,6 @@ impl Lookup {
self.sent.remove(which);
// It answered: its interval, which is for a query that may
// have been lost, is over.
- self.free(from, now);
// The next server is asked now, unless a newer query is still
// waiting for its own answer.
if which == self.sent.len() {
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..2dad223d9 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -613,7 +613,7 @@ impl Lookup {
return Step::Wait;
};
let unreached = self.sent.remove(which);
- self.free(unreached.to, now + WAIT_MS);
+ let _ = unreached;
// A newer query still waits for its own answer.
if which < self.sent.len() {
return Step::Wait;
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 1712abefd..2ae54e43a 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -586,10 +586,6 @@ impl Lookup {
let silence = if self.sent.is_empty() { Failure::Unreachable } else { Failure::TimedOut };
return Step::Done(Err(self.failed.map_or(silence, Failure::ServerFailed)));
}
- if !spent {
- // The newest query is not waited for: the next server's turn is.
- self.due = free;
- }
Step::Wait
}
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..97c899740 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -682,9 +682,6 @@ impl Lookup {
self.sent.clear();
self.asked = 0;
self.failed = None;
- for (_, free) in &mut self.servers {
- *free = now;
- }
self.ask(now, id)
}
},
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 4d8406a43..9ef8f06d3 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -577,7 +577,7 @@ impl Lookup {
self.due = now + WAIT_MS;
return Step::Ask { asked, to, query: query(id, &self.name) };
}
- if self.sent.is_empty() || (spent && now >= self.due) {
+ if self.sent.is_empty() || spent {
// With no query left, each was answered with a failure or never
// reached its server.
let silence = if self.sent.is_empty() { Failure::Unreachable } else { Failure::TimedOut };
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 1712abefd..43918beb7 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -571,7 +571,7 @@ impl Lookup {
let (to, free) = self.servers[turn];
let spent = self.asked == ROUNDS * self.servers.len();
if !spent && now >= free {
- self.free(to, now + WAIT_MS);
+ self.servers[turn].1 = now + WAIT_MS;
self.asked += 1;
let asked = Asked(self.next);
self.next += 1;
diff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 1712abefd..f8bd88ac2 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -588,7 +588,7 @@ impl Lookup {
}
if !spent {
// The newest query is not waited for: the next server's turn is.
- self.due = free;
+ self.due = self.due.max(free);
}
Step::Wait
}
diff --git a/toyos-dns/src/tests.rs b/toyos-dns/src/tests.rs
index 48078eef0..630ac141f 100644
--- a/toyos-dns/src/tests.rs
+++ b/toyos-dns/src/tests.rs
@@ -1148,6 +1148,51 @@ fn a_server_not_reached_is_not_asked_again_at_once_because_another_failed() {
assert_eq!(lookup.due(), 2 * WAIT_MS);
}
+// The interval is a server's and not its place in the list: a list that names
+// one address twice, as a lease may, asks it once a wait, whatever is
+// reported of each query, twice over, the moment it is sent.
+#[test]
+fn a_server_named_twice_is_one_server_and_is_asked_once_a_wait_whatever_is_reported() {
+ let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S1], 0, || 1).unwrap();
+ let mut newest = asked(&first).0;
+ let mut sent = vec![0];
+ let mut now = 0;
+ let ended = loop {
+ assert_eq!(lookup.on_report(newest, now, || 9), Step::Wait, "S1 has not answered and was asked this millisecond");
+ assert_eq!(lookup.due(), now + WAIT_MS);
+ assert_eq!(lookup.on_report(newest, now, || 9), Step::Wait, "the same report again");
+ assert_eq!(lookup.on_time(now + WAIT_MS - 1, || 9), Step::Wait);
+ now += WAIT_MS;
+ match lookup.on_time(now, || 1) {
+ Step::Done(result) => break result,
+ step => {
+ assert_eq!(to(&step), (S1, 1));
+ sent.push(now);
+ newest = asked(&step).0;
+ }
+ }
+ };
+ let turns = 2 * ROUNDS as u64;
+ assert_eq!(sent, (0..turns).map(|turn| turn * WAIT_MS).collect::<Vec<_>>(), "ROUNDS queries a place in the list, a wait apart");
+ assert_eq!((now, ended), (turns * WAIT_MS, Err(Failure::TimedOut)));
+}
+
+// Where the newest query is not waited for and the next server may not be
+// asked yet, the lookup wakes at that server's turn, not at the end of a
+// wait nobody is keeping: S1 asked at 0 and reported at 1.5 s, S2 asked then
+// and reported at 1.6 s, and S1 is asked again at 2 s.
+#[test]
+fn a_lookup_whose_newest_query_is_not_waited_for_wakes_at_the_next_servers_turn() {
+ let (mut lookup, first) = Lookup::start(name("www.example"), &[S1, S2], 0, || 1).unwrap();
+ let second = lookup.on_report(asked(&first).0, 1_500, || 2);
+ assert_eq!((to(&second), lookup.due()), ((S2, 2), 1_500 + WAIT_MS));
+ assert_eq!(lookup.on_report(asked(&second).0, 1_600, || 9), Step::Wait, "S1 was asked 1.6 s ago");
+ assert_eq!(lookup.due(), WAIT_MS, "S1's turn");
+ assert_eq!(lookup.on_time(WAIT_MS - 1, || 9), Step::Wait);
+ assert_eq!(to(&lookup.on_time(WAIT_MS, || 3)), (S1, 3));
+ assert_eq!(waiting(&lookup).len(), 3, "every query's answer is still read");
+}
+
// What the caller knows of a server holds for an interval too: S1's query is
// known not to have left a second after S2 was asked, so when S2's wait ends
// the lookup waits on, its answer still read, until S1 may be asked.
diff --git a/userland/netstack/node/tests/resolve.rs b/userland/netstack/node/tests/resolve.rs
index d67ae6ddd..7d930f464 100644
--- a/userland/netstack/node/tests/resolve.rs
+++ b/userland/netstack/node/tests/resolve.rs
@@ -569,6 +569,21 @@ fn reports_alone_end_no_lookup_and_ask_no_resolver_twice_inside_a_wait() {
assert_eq!(net.lan.node.counters().get(Counter::QueryFailed), rounds);
}
+// And a lease may name one resolver twice (RFC 2132 §3.8 lists addresses and forbids no repeat):
+// it is one resolver, with one interval. Every query is reported back, and one is on the wire a
+// wait, `ROUNDS` for each time the lease names it.
+#[test]
+fn a_resolver_the_lease_names_twice_is_asked_once_a_wait_whatever_it_reports() {
+ let mut net = Net::leased(&[REFUSES, REFUSES], Some(R));
+ let id = net.resolve("www.example").unwrap();
+ assert_eq!(net.run(id, 20 * WAIT_MS), Some(Err(Ended::Failed(Failure::TimedOut))));
+ let turns = 2 * u64::try_from(ROUNDS).unwrap();
+ let left: Vec<u64> = net.refused.iter().map(|query| net.ms_of(query.at)).collect();
+ assert_eq!(left, (0..turns).map(|turn| turn * WAIT_MS).collect::<Vec<_>>());
+ assert_eq!(net.ms(), turns * WAIT_MS);
+ assert_eq!(net.lan.node.counters().get(Counter::QueryFailed), turns, "the premise: every report reached its query's socket");
+}
+
// A report has crossed a trust boundary: anyone can send an ICMP message. RFC 1122 §4.1.3.3 has
// the application "demultiplex these messages when they arrive", which [udp] does by the quoted
// datagram's addresses and ports against the connected socket's, and §3.2.2.1 has a destination |
Review, round 4, of
|
…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
|
Merge round: The two files. The stage's twelve files,
Measured at
Zero failed and zero ignored in every suite above. What moved, by counting
The eight tests round 4's review names are each Mutations. Of m0 to m27 only m0's patch names a file the merge touched ( m0-whole-source-change-reverted-onto-main.patchdiff --git a/toyos-dns/src/lib.rs b/toyos-dns/src/lib.rs
index 1712abefd..1f46cdaa9 100644
--- a/toyos-dns/src/lib.rs
+++ b/toyos-dns/src/lib.rs
@@ -431,12 +431,8 @@ pub enum Failure {
NoSuchName,
/// The name exists and has no `A` record (RFC 2308 §2.2).
NoAddress,
- /// No server answered, and a query that may have reached one was given
- /// its [`WAIT_MS`].
+ /// Every query went unanswered for [`WAIT_MS`].
TimedOut,
- /// No query reached a server ([`Lookup::on_unreached`]) and none is left
- /// to send at once: there is no answer to wait for.
- Unreachable,
/// The answer did not fit a UDP reply (TC).
Truncated,
/// Every server that answered could not answer, the last with this RCODE.
@@ -482,49 +478,21 @@ struct Sent {
/// any query this lookup sent for the name now asked is read, so an answer
/// late past its query's wait still ends the lookup; it is read only on the
/// query's own port, with the query's ID, from the server the query went to.
-///
-/// **A wait is for an answer that can come.** When the newest query is
-/// answered with a failure, or known or said not to have reached its server,
-/// the next server is asked at once: RFC 1035 §4.2.1's interval of two to
-/// five seconds is between repetitions of a query that may have been lost on
-/// its way, and the same section has the other servers tried first. **The
-/// interval is each server's**: one that has not answered is sent no second
-/// query inside [`WAIT_MS`] of its last, or of the caller's last knowledge
-/// that it was not reached, whatever is said of that query and whatever
-/// another server answers; a server is its address, however often the list
-/// names it. Where the next server cannot be asked yet, the lookup waits
-/// until it can while any answer is still read, and ends at once only with
-/// none.
-///
-/// **What the caller knows and what it was told are two calls.** A query the
-/// caller itself knows never reached its server ([`Lookup::on_unreached`]) is
-/// let go, and a lookup with no query left whose answer is read ends with
-/// [`Failure::Unreachable`]. A report from the network
-/// ([`Lookup::on_report`]) is anyone's word (RFC 8085 §5.2): it lets no query
-/// go and ends nothing, so the answer to a query reported so is still read.
#[derive(Debug)]
pub struct Lookup {
/// The name now asked: the one given, or the last alias followed.
name: Name,
/// Aliases followed so far, counted against [`MAX_ALIASES`].
aliases: usize,
- /// Each server, and when it may be asked again: [`WAIT_MS`] after its
- /// last query or after it was last known not to be reached, and at once
- /// when it has answered any query, an older one's late failure included.
- /// An address named twice is one server: every place it has carries the
- /// same time.
- servers: Vec<([u8; 4], u64)>,
+ servers: Vec<[u8; 4]>,
/// Queries sent for `name`, oldest first; one answered with a server
- /// failure, or known not to have reached its server, is taken out.
- /// [`Lookup::waiting`] is this list, which holds a query while the
- /// lookup lasts.
+ /// failure is taken out. [`Lookup::waiting`] is this list.
sent: Vec<Sent>,
/// The [`Asked`] the next query gets: none is given twice in a lookup.
next: u32,
/// Queries sent for `name`, answered or not.
asked: usize,
- /// When the newest query is given up on; or, where it is not waited for
- /// and the next server may not be asked yet, that server's turn.
+ /// When the newest query is given up on.
due: u64,
/// The RCODE of the last server failure, which is what a lookup that runs
/// out of queries reports if any server answered at all.
@@ -541,7 +509,7 @@ impl Lookup {
let mut lookup = Self {
name,
aliases: 0,
- servers: servers.iter().map(|server| (*server, now)).collect(),
+ servers: servers.to_vec(),
sent: Vec::new(),
next: 0,
asked: 0,
@@ -563,41 +531,22 @@ impl Lookup {
self.sent.iter().map(|s| s.asked)
}
- /// The next server's query, if it may be asked at `now`; or the wait for
- /// the answers still read, until it may or the last query's wait is
- /// over; or the end.
+ /// The next query for `name`, or the end where every one has been sent.
fn ask(&mut self, now: u64, id: impl FnOnce() -> u16) -> Step {
- let turn = self.asked % self.servers.len();
- let (to, free) = self.servers[turn];
- let spent = self.asked == ROUNDS * self.servers.len();
- if !spent && now >= free {
- self.free(to, now + WAIT_MS);
- self.asked += 1;
- let asked = Asked(self.next);
- self.next += 1;
- let id = id();
- self.sent.push(Sent { asked, id, to });
- self.due = now + WAIT_MS;
- return Step::Ask { asked, to, query: query(id, &self.name) };
- }
- if self.sent.is_empty() || (spent && now >= self.due) {
- // With no query left, each was answered with a failure or never
- // reached its server.
- let silence = if self.sent.is_empty() { Failure::Unreachable } else { Failure::TimedOut };
- return Step::Done(Err(self.failed.map_or(silence, Failure::ServerFailed)));
- }
- if !spent {
- // The newest query is not waited for: the next server's turn is.
- self.due = free;
- }
- Step::Wait
- }
-
- /// `server` may next be asked at `at`.
- fn free(&mut self, server: [u8; 4], at: u64) {
- for (_, free) in self.servers.iter_mut().filter(|(addr, _)| *addr == server) {
- *free = at;
+ if self.asked == ROUNDS * self.servers.len() {
+ return Step::Done(Err(match self.failed {
+ Some(rcode) => Failure::ServerFailed(rcode),
+ None => Failure::TimedOut,
+ }));
}
+ let to = self.servers[self.asked % self.servers.len()];
+ self.asked += 1;
+ let asked = Asked(self.next);
+ self.next += 1;
+ let id = id();
+ self.sent.push(Sent { asked, id, to });
+ self.due = now + WAIT_MS;
+ Step::Ask { asked, to, query: query(id, &self.name) }
}
/// The time is `now`: the newest query has had its [`WAIT_MS`] where it
@@ -609,33 +558,6 @@ impl Lookup {
self.ask(now, id)
}
- /// The caller knows at `now`, of its own knowledge, that the query
- /// `asked` did not reach its server: it was never sent. Its answer is
- /// read no more. `id` draws the ID of the query this sends.
- pub fn on_unreached(&mut self, asked: Asked, now: u64, id: impl FnOnce() -> u16) -> Step {
- let Some(which) = self.sent.iter().position(|s| s.asked == asked) else {
- return Step::Wait;
- };
- let unreached = self.sent.remove(which);
- self.free(unreached.to, now + WAIT_MS);
- // A newer query still waits for its own answer.
- if which < self.sent.len() {
- return Step::Wait;
- }
- self.ask(now, id)
- }
-
- /// The network said at `now` that the query `asked` did not reach its
- /// server. Its answer is still read. `id` draws the ID of the query this
- /// sends.
- pub fn on_report(&mut self, asked: Asked, now: u64, id: impl FnOnce() -> u16) -> Step {
- // A newer query still waits for its own answer.
- if self.sent.last().is_none_or(|s| s.asked != asked) {
- return Step::Wait;
- }
- self.ask(now, id)
- }
-
/// `msg` arrived at `now` from `port` of `from`, on the port `asked` was
/// sent from. `id` draws the ID of the query this sends.
pub fn on_datagram(
@@ -663,9 +585,6 @@ impl Lookup {
Verdict::ServerFailed(rcode) => {
self.failed = Some(rcode);
self.sent.remove(which);
- // It answered: its interval, which is for a query that may
- // have been lost, is over.
- self.free(from, now);
// The next server is asked now, unless a newer query is still
// waiting for its own answer.
if which == self.sent.len() {
@@ -686,9 +605,6 @@ impl Lookup {
self.sent.clear();
self.asked = 0;
self.failed = None;
- for (_, free) in &mut self.servers {
- *free = now;
- }
self.ask(now, id)
}
},
diff --git a/toyos-net-ip/src/lib.rs b/toyos-net-ip/src/lib.rs
index 59be88ccf..9cbee3f78 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 = 16;
+ pub const PENDING_PER_NEIGHBOUR: usize = 8;
pub const PENDING_TOTAL: usize = 64;
pub const TABLE_MAX: usize = 512;
}
diff --git a/toyos-net-udp/src/lib.rs b/toyos-net-udp/src/lib.rs
index f94fecbc8..a5fa0f73a 100644
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -113,17 +113,11 @@ pub enum Binding {
Connected { local: Ipv4Addr, peer: Ipv4Addr, peer_port: Port },
}
-/// An error against a connected socket's datagrams: its next call returns it once. Two are what
-/// an ICMP message said, which anyone who knows the socket's addresses and ports can send; one
-/// is [ip]'s own.
+/// An error the network reported against a connected socket: its next call returns it once.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SocketError {
- /// An ICMP error said the peer refuses the protocol or the port.
Refused,
- /// An ICMP error said the way to the peer is administratively prohibited.
- Prohibited,
- /// [ip] found no link address for the next hop of a datagram it held: nothing left.
- NextHopFailed,
+ Unreachable,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -665,7 +659,7 @@ impl Udp {
};
let pending = match class {
ErrorClass::Refused => SocketError::Refused,
- ErrorClass::Prohibited => SocketError::Prohibited,
+ ErrorClass::Prohibited => SocketError::Unreachable,
ErrorClass::PathMtu | ErrorClass::Soft => return self.count(Counter::IcmpErrorSoft),
};
socket.pending = Some(pending);
@@ -675,7 +669,7 @@ impl Udp {
/// [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);
+ socket.pending = Some(SocketError::Unreachable);
}
}
diff --git a/userland/netstack/node/src/resolve.rs b/userland/netstack/node/src/resolve.rs
index 1dcef864a..d8a5cfc23 100644
--- a/userland/netstack/node/src/resolve.rs
+++ b/userland/netstack/node/src/resolve.rs
@@ -10,21 +10,9 @@
//! query other than the one whose socket it reached.
//! - **A socket lives as long as its query's answer is read**: it is closed when the lookup lets
//! the query go, ends, or is let go itself, and its port is free from then.
-//! - **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 [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.
-//! - **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 query [udp] refuses never left**: it is counted, holds no socket, and the lookup waits
+//! its wait out as for any query nobody answered. So does one the network reports back, its
+//! resolver unreachable or its port refused: counted, and waited out.
//! - **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.
@@ -39,16 +27,13 @@ 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_udp::{Error, SocketId};
use toyos_net_wire::Instant;
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,51 +105,42 @@ fn close(stack: &mut Stack, now: Instant, socket: SocketId) {
}
}
-/// What waits at a query's socket.
-enum Heard {
- /// A datagram of this length, now in the reply buffer.
- Reply(usize),
- /// [ip]'s word that the query never left.
- Unreached,
- /// An ICMP error's word that the query was refused.
- Reported,
-}
-
-/// The first of `queries` with something at its socket, and what: a reply is now in `reply`.
-fn waiting(queries: &[Query], stack: &mut Stack, reply: &mut [u8]) -> Option<(Query, Heard)> {
+/// The length of the next reply waiting at one of `queries`' sockets, now in `reply`, and the
+/// query it is for. An error the network reported against a query is counted and read past.
+fn waiting(queries: &[Query], stack: &mut Stack, reply: &mut [u8], counters: &mut Counters) -> Option<(Query, usize)> {
for query in queries {
- match stack.recv_from(query.socket, reply) {
- Ok(Some(received)) => return Some((*query, Heard::Reply(received.len))),
- Ok(None) => {}
- Err(Error::Failed(SocketError::NextHopFailed)) => return Some((*query, Heard::Unreached)),
- Err(Error::Failed(SocketError::Refused | SocketError::Prohibited)) => return Some((*query, Heard::Reported)),
- Err(Error::NoSuchSocket | Error::Refused(_)) => unreachable!("a query's socket is open while it is listed, and no rule refuses a receive"),
+ loop {
+ match stack.recv_from(query.socket, reply) {
+ Ok(Some(received)) => return Some((*query, received.len)),
+ Ok(None) => break,
+ Err(Error::Failed(_)) => counters.add(Counter::QueryFailed, 1),
+ Err(Error::NoSuchSocket | Error::Refused(_)) => unreachable!("a query's socket is open while it is listed, and no rule refuses a receive"),
+ }
}
}
None
}
-/// 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.
-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 } => {
- // An any-address bind that names no port is refused only for want of a free one.
- let Ok((socket, _)) = stack.bind(Ipv4Addr::UNSPECIFIED, None, &mut *draw) else { break Some(Err(Ended::NoPort)) };
+/// Carries out `step` for `asking`, then closes the socket of every query the lookup no longer
+/// reads an answer for. Returns how the lookup ended, if it did.
+fn act(asking: &mut Asking, step: Step, now: Instant, stack: &mut Stack, counters: &mut Counters, draw: &mut impl FnMut() -> u32) -> Option<Result<Vec<[u8; 4]>, Ended>> {
+ let done = match step {
+ // An any-address bind that names no port is refused only for want of a free one.
+ Step::Ask { asked, to, query } => match stack.bind(Ipv4Addr::UNSPECIFIED, None, &mut *draw) {
+ Ok((socket, _)) => {
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 });
- break None;
+ } else {
+ counters.add(Counter::QueryUnsent, 1);
+ close(stack, now, socket);
}
- counters.add(Counter::QueryUnsent, 1);
- close(stack, now, socket);
- step = asking.lookup.on_unreached(asked, millis(now), || id(&mut *draw));
+ None
}
- Step::Wait => break None,
- Step::Done(result) => break Some(result.map_err(Ended::Failed)),
- }
+ Err(_) => Some(Err(Ended::NoPort)),
+ },
+ Step::Wait => None,
+ Step::Done(result) => Some(result.map_err(Ended::Failed)),
};
let lookup = &asking.lookup;
asking.queries.retain(|query| {
@@ -198,19 +174,16 @@ 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);
- if done == Some(Err(Ended::NoPort)) {
- return Err(NotStarted::ResourceExhausted);
- }
- self.next = self.next.wrapping_add(1);
- let started = asking.id;
- match done {
- None => self.asking.push(asking),
- // No resolver could be sent a query: the lookup has its id and its end at once, and
- // holds no socket.
- Some(result) => self.ended.push(Resolved { id: started, result }),
+ match act(&mut asking, step, now, stack, counters, &mut *draw) {
+ None => {
+ self.next = self.next.wrapping_add(1);
+ let started = asking.id;
+ self.asking.push(asking);
+ Ok(started)
+ }
+ Some(Err(Ended::NoPort)) => Err(NotStarted::ResourceExhausted),
+ Some(other) => unreachable!("a lookup's first step is a query, not {other:?}"),
}
- Ok(started)
}
/// Ends every lookup whose lease went or names other resolvers, reads every reply that
@@ -223,20 +196,11 @@ impl Resolver {
let named = stack.lease().is_some_and(|lease| lease.dns == asking.resolvers);
let mut done = if named { None } else { Some(Err(Ended::LeaseChanged)) };
while done.is_none() {
- let Some((query, heard)) = waiting(&asking.queries, stack, reply.as_mut_slice()) else { break };
- let step = match heard {
- // The socket is connected: [udp] delivered this from port 53 of the resolver
- // the query went to, or not at all.
- Heard::Reply(len) => asking.lookup.on_datagram(query.asked, query.to, PORT, reply.get(..len).unwrap_or_default(), ms, || id(&mut *draw)),
- Heard::Unreached => {
- counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_unreached(query.asked, ms, || id(&mut *draw))
- }
- Heard::Reported => {
- counters.add(Counter::QueryFailed, 1);
- asking.lookup.on_report(query.asked, ms, || id(&mut *draw))
- }
- };
+ let Some((query, len)) = waiting(&asking.queries, stack, reply.as_mut_slice(), counters) else { break };
+ let message = reply.get(..len).unwrap_or_default();
+ // The socket is connected: [udp] delivered this from port 53 of the resolver the
+ // query went to, or not at all.
+ let step = asking.lookup.on_datagram(query.asked, query.to, PORT, message, ms, || id(&mut *draw));
done = act(asking, step, now, stack, counters, &mut *draw);
}
if done.is_none() {
@@ -267,11 +231,9 @@ impl Resolver {
}
impl Node {
- /// Starts looking `name` up at the resolvers the held lease names, which spends two draws a
- /// query, its id and then its port: for the first query, and for each asked in its place
- /// because [udp] refused the one before. A call refused for want of a port has spent them;
- /// any other refused call spends none. A lookup no resolver could be sent a query of has
- /// started and ended: its end is in the next [`Self::take_resolved`].
+ /// Starts looking `name` up at the resolvers the held lease names, which spends two draws:
+ /// the first query's id, then its port. A call refused for want of a port has spent both;
+ /// any other refused call spends none.
pub fn resolve(&mut self, now: Instant, name: Name, mut draw: impl FnMut() -> u32) -> Result<LookupId, NotStarted> {
self.resolver.start(now, name, &mut self.stack, &mut self.counters, &mut draw)
}
diff --git a/userland/netstack/src/main.rs b/userland/netstack/src/main.rs
index 3125aa5c1..76e8c7fcc 100644
--- a/userland/netstack/src/main.rs
+++ b/userland/netstack/src/main.rs
@@ -1295,9 +1295,6 @@ impl Netstack {
// whether the name or only its address is missing.
Err(Ended::Failed(Failure::NoSuchName | Failure::NoAddress)) => answer_lookup(&client, &[]),
Err(Ended::Failed(Failure::TimedOut)) => client.error(ERR_TIMED_OUT),
- Err(Ended::Failed(Failure::Unreachable)) => {
- unreachable!("netstack: no lookup here is told a query did not reach its server, and {name}'s ended so")
- }
Err(Ended::Failed(why @ (Failure::Truncated | Failure::ServerFailed(_) | Failure::TooManyAliases))) => {
say!("netstack: a lookup of {name} ended without an answer: {why:?}");
client.error(ERR_OTHER);No guest test was run: the merge changed no line this stage ships. The pull request stays a draft, so |
One conflict, the track's stage-5 paragraph: this branch named datagram senders that wait on the hop as still to build, #783 named the datagram sockets' broadcast permission. Both stand, in that order, before the move. The clause that ordered the first after #777 is met and is restated as where the work lies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Merge round 2: Conflict hunks: one, in
Both slices stand, since neither is in the merged tree: the node has no call that takes the permission (#783's own line says so), and the two residual lines of this branch still name the hop slice as their owner. "after #777, which opens …" is met, #777 having landed, and is restated as where the work lies; no Every other hunk of the track merged without a conflict: Nothing reviewed moved. Gates at |
|
CI at |
…stage One conflict, in the track. Both sides rewrote the paragraph that says what is still to build on the node: #781's sentence stands less the broadcast permission, which this branch builds, so what is left is the datagram senders that wait on the hop, and then the move. Both sides rewrote the lines after the name's: this branch's line on the name stands, and #781's two lines replace the two on a query that never reached its resolver and on a burst at an unresolved resolver, whose exits #781 met. #781's departure line on NUD-04 and this branch's on the carried permission both stand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Stage "resolver" of the move off smoltcp: the two lines of
issues/toyos-has-its-own-network-stack.mdthat said "Exit, before the move" about the node's resolver.toyos-dnsships (netstack's resolver on smoltcp links it);toyos-net-node,toyos-net-ipandtoyos-net-udpship in nothing, so no guest test reaches the new behaviour and none is added.Head
8330a5c75: the reviewedb2d903719withorigin/mainmerged in twice, at3fd6a2ac7(headc1dc7b176: #779 the ip rules, #775 streams, #777 listeners, #782, #780 and #786) and then at0d6cd9a60(#778, #785 and #783, the broadcast permission).git diff b2d903719 8330a5c75 -- toyos-dns/src/lib.rs userland/netstack/node/src/resolve.rsis empty. Of this stage's twelve files,git diff c1dc7b176 8330a5c75names two: the track, anduserland/netstack/src/main.rs, whose hunks there are #783's, line for line those ofgit diff 3fd6a2ac7 0d6cd9a60. Each merge had one conflict, in the track file, resolved by hand with every line of both sides standing. Round 3 of the review (of71db83dc1), round 2 (ofd9a5cf2e5) and round 1 (of20b21432b) are answered in the three sections after next.What changed, per decision
1. A lookup is told what its caller knows, and what its caller was told, in two calls.
toyos_dns::Lookup::on_unreached(asked, now, id): the caller knows of its own knowledge that the query never reached its server. The query is let go (its answer is read no more, so the node closes its socket).toyos_dns::Lookup::on_report(asked, now, id): the network said so. Nothing is let go and nothing ends: the query's answer is still read.WAIT_MSafter its last query,WAIT_MSafter the caller last knew it was not reached, and at once when it has answered (a failure is an answer, read to the strength of one: the query's port, its id and its question). Every path that asks, the wait's end, a server failure,on_unreachedandon_report, is the oneask. It sends where the next server may be asked. Otherwise it waits while any query's answer is still read: until the next server's turn, which is then when the lookup wakes, or, with every query sent, until the last one's wait is over. It ends at once only with no query left. There is no count of queries in a row any more.WAIT_MSafter a resolver's last query.Failure::Unreachableonly when no query's answer is still read, which onlyon_unreachedand server failures can bring about. A server failure answered earlier still names the end (ServerFailed), and it ends a lookup only when no query is read or the last query's wait is over.userland/netstack/node/src/resolve.rs). Knowledge: a query [udp] refuses in the resolver's own call (udp.no-routeat connect; countednode.query-unsent), and [ip]'s own report that the query's next hop failed (SocketError::NextHopFailed; countednode.query-failed). A report: an ICMP error on the query's connected socket that [udp] classes refused or prohibited (SocketError::Refused,SocketError::Prohibited; countednode.query-failed). I count both under the one counter the node has, because adding one islib.rs, outside the fence.Node::resolve: it returns its id, and its end is in the nexttake_resolved.a_query_udp_refuses_is_counted_holds_no_port_and_the_next_resolver_is_asked_at_oncefinds its answer at 0 ms (it wasWAIT_MS).a_query_the_network_reports_unreachable_is_counted_and_ends_a_lookup_with_no_other_resolverfinds its lookup ended at 3 s, [ip]'s report,Failure::Unreachable(it wasROUNDSxWAIT_MS,TimedOut).2.
toyos-net-udp'sSocketError::Unreachableis two words:ProhibitedandNextHopFailed. It was both an ICMP prohibition (codes 9, 10, 13) and [ip]'s own report of a failed next hop, and the resolver has to tell a datagram's word from [ip]'s. Neither new word keeps the old name, so every reader stopped compiling until it chose. The readers, all touched:toyos-net-udp/tests/recv.rs(US-049, nowProhibited),toyos-net-udp/tests/send.rs(US-027, nowNextHopFailed),toyos-net-shard/tests/udp.rs(ICD-012 through the shard, nowNextHopFailed), and the node's resolver. The node'sdatagram.rsmatchesError::Failed(_)and is untouched; the shard'ssrc/lib.rsnames noSocketErrorand is untouched. No fork clone or checkout names the type (toyos-net-udpis in this tree alone).3. A burst of lookups at a resolver not yet resolved is whole on the wire once ARP answers:
PENDING_PER_NEIGHBOURis 16, a stopgap.PENDING_PER_NEIGHBOUR >= MAX_LOOKUPS. Mutation m8 is that assertion failing the build.a_burst_at_a_resolver_not_yet_resolved_is_on_the_wire_once_the_resolver_answers_arpfinds all 16 queries, in the order the lookups started, at 0 ms, withnb.pending-overflowat 0.Round 3 of the review, finding by finding
BLOCKER 6, a send started the interval of one place in the list and every other writer keyed it by address. Fixed, by the rule that deletes the split: the send calls
self.free(to, now + WAIT_MS).askwrites no slot any more; an interval has one key.toyos-dns:a_server_named_twice_is_one_server_and_is_asked_once_a_wait_whatever_is_reported.&[S1, S1]:on_reportof the newest query, twice, the moment each is sent, isStep::Waitwithdue()at that query'sWAIT_MS; S1 is asked at 0, 2 s, 4 s, 6 s, 8 s and 10 s and at no other time, and the lookup endsTimedOutat 12 s.a_resolver_the_lease_names_twice_is_asked_once_a_wait_whatever_it_reports. A lease that names the refusing resolver twice: one query on the wire aWAIT_MS, six in all, every report counted.r0-red-first-at-71db83dc1,cargo test --locked --no-fail-fast -p toyos-dns -p toyos-net-node -- <the three tests>, exit 101. Its tree is the mergeb10a788dfwith the tests added, which I ran instead of71db83dc1itself because this round's builds were to follow the merge of The host's toolchains live in one store outside every checkout, and every compiler is keyed #769;git diff 71db83dc1 b10a788dfis empty fortoyos-dns, the node, [udp], [ip] and the shard, and the log's second line says so.toyos-dns:left: Ask { asked: Asked(1), to: [10, 0, 2, 3], .. },right: Wait. The node:left: [0, 0, 2000, 2000, 4000, 4000],right: [0, 2000, 4000, 6000, 8000, 10000].NOTEs.
self.due = self.due.max(free): the assignment is the rule, and themaxis deleted. Where the newest query is not waited for and the next server may not be asked yet, the lookup wakes at that server's turn. The test at the review's order isa_lookup_whose_newest_query_is_not_waited_for_wakes_at_the_next_servers_turn([S1, S2], q1 at 0, reported at 1.5 s, q2 reported at 1.6 s:due()is 2 s and S1 is asked then, three queries still read); it is red at round 3's source in the samer0(left: 3500,right: 2000), and m27 puts themaxback and is red on it alone.mainwhere it is, and "no server that has not answered is sent a second query insideWAIT_MS, whoever reports or fails what" is true of this head for a list that repeats an address too.Round 2 of the review, finding by finding
BLOCKER 5, reports and another resolver's failures chained with no wait and ended the lookup with an answer on its way. Fixed, by deleting the state that made it.
runis gone and so is the split between the first query of a run and a query at once; the interval is each server's (decision 1). Both things the review names are false at this head: a lookup does not end by server failure while a query is still read and under its wait, and no server that has not answered is sent a second query insideWAIT_MS, whoever reports or fails what.toyos-dns:reports_and_another_servers_failures_hurry_no_server_that_has_not_answered_and_end_no_lookup_early. Two servers, S1's every query reported the moment it is sent, S2 failing each of its own at once: each server is asked at 0, 2 s and 4 s and at no other time, the lookup endsServerFailed(5)at 6 s with S1's three queries read to the end; and S1's answer to q1 atWAIT_MS - 1ends the lookupOk.a_server_not_reached_is_not_asked_again_at_once_because_another_failed. S1's query known unreached and S2 failing: S1 is not asked again, and with no query read the lookup ends by S2's word; with a query to S1 still read it waits for it.forged_reports_and_another_resolvers_failures_end_nothing_and_the_answer_behind_them_is_taken. A lease of the answering resolver and one that answers every query RCODE 5; every query that leaves for the first is reported in its name the moment it is on the wire, until none follows. One query has left for each resolver, the lookup has not ended, and the first resolver's answer atWAIT_MS - 1ends itOk.r0-red-first-at-d9a5cf2e5: the tree ofd9a5cf2e5with those three tests,cargo test --locked --no-fail-fast -p toyos-dns -p toyos-net-node -- <the three tests>, exit 101: the twotoyos-dnstestsleft: Ask { asked: Asked(2), to: [10, 0, 2, 3], .. }againstright: Waitandright: Done(Err(ServerFailed(5))); the node'sleft: (3, 3, 0),right: (1, 1, 0), three queries to each resolver in the lookup's first millisecond.rfc1035_4_2_1_no_server_is_asked_again_at_once_a_lookup_that_reached_none_ends, and the transmit-opportunity test's 6 s, both red without it: m22). Where that leaves the next server not yet askable when the newest query's wait ends, the wait reaches to its turn while an answer is read:a_server_known_unreached_is_asked_again_a_wait_after_that_and_the_lookup_waits_for_its_turn(m23). A server failure of a lookup's last query, with older queries read, now waits their wait out where the base ended at once (m25).NOTEs. The body says what reports alone cost (decision 1). "No server is asked twice inside
WAIT_MS, whoever reports what" and "no lookup ended", which finding 5 made false of round 2's head, are gone as round 2 wrote them; what this head keeps is said in decision 1 and held by the three tests above. The displacement of [ip]'s word by an ICMP error is a clause of the track's first residual, with that residual's exit. The track lists the owning slice with stage 5's slices, "datagram senders that wait on the hop, after #777", and the three lines name it; "the move'snodestage" is gone from the track.Round 1 of the review, finding by finding
BLOCKER 1, one forged ICMP message ended a query and a lookup. Fixed, by the review's option (a). Decision 1 and 2 above. No ruling that another resolver's behaviour is the design is assumed anywhere.
a_forged_report_of_the_querys_own_addresses_and_ports_ends_nothing_and_the_answer_behind_it_is_taken. A port unreachable and then a prohibition (code 13), each in the resolver's name and quoting the query's exact addresses and ports, reach the query's socket (udp.icmp-error-deliveredmoves, the premise); neither ends the lookup, the socket is still held, no second query leaves, and the resolver's answer behind them ends the lookupOk.r0-red-first-at-20b21432b: the tree of20b21432bwith that test added,cargo test --locked --no-fail-fast -p toyos-dns -p toyos-net-node -- <the four tests>, exit 101:left: [Resolved { id: LookupId(0), result: Err(Failed(Unreachable)) }],right: [].reports_alone_end_no_lookup_and_ask_no_resolver_twice_inside_a_wait: a lease whose one resolver truly refuses every query by ICMP. The lookup's queries leave at 0, 2 s and 4 s, it endsTimedOutat 6 s, three reports counted. That is the cost of this option, and it is the base's behaviour for such a resolver.toyos-dns:rfc8085_5_2_a_reported_query_is_still_read_and_the_next_server_is_asked_at_once,rfc8085_5_2_reports_alone_end_no_lookup_and_ask_no_server_twice_inside_a_wait(every query reported twice the moment it is sent, with one server and with two),rfc8085_5_2_a_report_of_an_older_query_asks_nobody,a_reported_query_the_caller_then_knows_reached_nobody_is_let_go.toyos-net-udp, each word's test: US-049 (Prohibited), US-027 (NextHopFailed), and ICD-012 through the shard. m16, m17 and m20 swap the words one way each and are red there and at the node.BLOCKER 2,
LookupendedUnreachablewhile an older query was still read. Fixed. The end is in one place,ask, and is reached only with no query left insentor with every query sent and the last one's wait over.a_lookup_whose_newest_query_reached_nobody_waits_for_an_older_querys_answer(one server, q1 waited out, q2 unreached:Step::Wait,waiting() == [q1],dueunchanged, q1's answer a millisecond later ends the lookupOk).a_lookup_that_then_reached_neither_server_waits_for_an_older_querys_answer(q1 to S1 waited out, q2 to S2 unreached, q3 to S1 unreached:Step::Wait,waiting() == [q1], S2 asked atdue, q1's answer then ends the lookupOk).a_lookup_whose_last_query_reached_nobody_waits_for_the_answers_still_read. The last query a lookup has, unreached, ended it at once under the queries before it.r0-red-first-at-20b21432b, exit 101: all threeleft: Done(Err(Unreachable)),right: Wait.BLOCKER 3, the alias test narrowed on a guess. Measured, and restored as it was.
b3-the-bases-alias-test-at-20b21432b: the test's body as onorigin/main(two resolvers, the alias at 10.5 s) on20b21432b,cargo test --locked -p toyos-net-node --test resolve an_alias_answered_late, exit 101:left: Some(Err(Failed(TimedOut))),right: Some(Ok([[203, 0, 113, 7]])), the answering resolver having heard queries at 0, 4 s and 8 s, as the review read.TimedOutbefore 10.5 s while three queries to the answering resolver were read: finding 2's defect, in its third order.git diff origin/main...8330a5c75 -- userland/netstack/node/tests/resolve.rshas no hunk in it). It now also holds that a query reported unreached between two waited ones leaves the alias restart alone. m18 turns it red.BLOCKER 4, evidence. Open through every round and not mine to close: a draft, no
hostjob and no guest test has run. What I ran is under "Gates".NOTEs.
PENDING_PER_NEIGHBOURat the specification's 8 again, and the compile-time assertion, thenud.rschange and the departure line deleted. The ids NUD-25 and IP-D5 in that line are the review's, which round 2 confirmed.PENDING_TOTALwhere eight did.limits.ip.nud.pending_per_neighbour, with NUD-04, says the test holds the rule and no longer the scenario's numbers, and says NUD-05 passes unchanged only because its eight neighbours of eight arePENDING_TOTAL.unreachable!arm inuserland/netstack/src/main.rs: no change asked, none made. It is still true: the shipped resolver calls neitheron_unreachednoron_report.What moved outside the brief's list, and why
userland/netstack/src/main.rs, three lines: its match ontoyos_dns::Failureis exhaustive, so the new variant needs an arm. The shipped resolver never callson_unreached, so the arm isunreachable!and says why.toyos-net-ip/tests/nud.rs, one test:s_ip_nud_004_overflow_drops_the_oldestsends two more than the constant. The track records the departure.toyos-net-shard/tests/udp.rs, one line: a reader of the old word.What is left, on the track
Node::transmit, which carries no lookup on, so the resolver reads the report at the node's nextreceive,fireorlink: at the latest when the query's wait ends.a_query_ip_refuses_at_its_transmit_opportunity_is_read_at_the_nodes_next_wakeholds it. Not a regression: what ships waits every such query out. Owner: the slice in which datagram senders wait on the hop.a_clients_datagram_behind_the_burst_takes_the_oldest_querys_place_in_ips_queueholds it. A regression against smoltcp for more than 16 datagrams at one cold next hop; exit before the move. Owner: the same slice.Checks (a trust boundary)
toyos-net-wireand checks its quote; [udp] hands it only to the connected socket whose whole 4-tuple it quotes, and only for the refused and prohibited classes. The resolver reads no byte of it: it learnsSocketError::RefusedorProhibitedfromrecv_from. Forging one takes the query's source port (one of 16,384) and buys one query to the next resolver a wait early, at most one a resolver a round, which is the 12 s to 6 s above: no query let go, no socket closed, no lookup ended, no answer.a_report_that_is_not_about_the_query_or_is_a_hint_ends_no_waitsends four reports that must not even count. Nothing new can panic on input: both calls take a value the caller made, and the node crate forbids indexing, arithmetic side effects,unwrapandexpect.etherparse. The recordeddns.googlereply still replays.cargo test --locked --no-fail-fast -p toyos-dns -p toyos-net-node -p toyos-net-ip -p toyos-net-udp -p toyos-net-shard, and reversed by one script that refuses a dirty tree before and after. Patches and script are in the round-4 comment below. Atb2d903719, and m0 again atc1dc7b176, its patch made againstmainas it now is, since it is the one patch that names a file the merge touched (userland/netstack/src/main.rs):on_unreached,on_reportand the two wordsa_lookup_udp_refuses_every_resolver_of_ends_in_the_call_that_started_iton_unreachedasks nobodytoyos-dnstests, three node teststoyos-dnstests and seven node tests, finding 5's three and finding 6's two among theman_older_query_that_reached_nobody_does_not_hurry_the_next_serverand one more oftoyos-dnstoyos-dnstests and four node tests, finding 5's review order and node form and finding 6's two among themtoyos-dnstests, three node testsevaluation panicked: assertion failed: PENDING_PER_NEIGHBOUR >= MAX_LOOKUPSa_report_that_is_not_about_the_query_or_is_a_hint_ends_no_wait; US-049Unreachableoutranks a server failuretoyos-dnstests,a_server_that_answered_with_a_failure_is_what_a_lookup_that_then_reached_nobody_reportsamong themresolveis droppeda_lookup_udp_refuses_every_resolver_of_ends_in_the_call_that_started_ita_report_that_is_not_about_the_query_or_is_a_hint_ends_no_wait; US-052on_reportlets the query gotoyos-dnstests; the same four node teststoyos-dnstests, the three of finding 2 and both of finding 5 among them; the four node tests of reportsrfc8085_5_2_a_report_of_an_older_query_asks_nobodya_lookup_whose_every_server_failed_says_howand two more oftoyos-dnstoyos-dnstests; the transmit-opportunity testa_server_known_unreached_is_asked_again_a_wait_after_that_and_the_lookup_waits_for_its_turnand the test of the wake; the alias testtoyos-dnstests of an alias asked again; the alias testtoyos-dnstests, finding 5's review order among them; the node's two reports-alone testsa_server_named_twice_is_one_server_and_is_asked_once_a_wait_whatever_is_reported;a_resolver_the_lease_names_twice_is_asked_once_a_wait_whatever_it_reportsa_lookup_whose_newest_query_is_not_waited_for_wakes_at_the_next_servers_turnm10, m13, m16, m17 and m20 mutate
toyos-net-udp: the controls of the trust claim the resolver's header rests on.an_older_query_that_reached_nobody_does_not_hurry_the_next_serverexisted (round 2, stepg5-m5-without-its-test); the test is kept and m5 is red on it here. No mutation of rounds 3 and 4 stayed green.r0), finding 5's three against round 2's (round 3'sr0), and finding 6's against round 3's (round 4'sr0). The tests that nameon_reportor the two new words do not compile against the base or round 1, so their controls are m14 to m27. m0 is the whole source change reverted onto the base under the tests as they stand: a build failure.Gates, at
8330a5c75cargo run -- --build-onlycargo run -- --ci host(Host: 78 step(s), all green)cargo test --locked -p toyos-dns(62 passed, 0 failed)cargo test --locked -p toyos-net-node --test resolve(32 passed, 0 failed)Their logs are in the orchestrator's scratchpad under the stage's
merge2/, each with its exit beside it.Gates, at
c1dc7b176and before, not run again at8330a5c75cargo metadata --locked --format-version 1cargo test --locked -p toyos-dns(62 passed, 0 failed)cargo test --locked -p toyos-net-node(135 passed, 0 failed:datagram5,host1,lease19,listeners28,name13,resolve32,slirp3,streams34; the 71 ofb2d903719and 64 ofmain's,streamsfrom #775,listeners,hostand one ofleasefrom #777)cargo test --locked -p toyos-net-ip(271 passed, 0 failed: the 262 and #779's nine, six inaddr, two innbr, one innud)cargo test --locked -p toyos-net-udp(59 passed, 0 failed)cargo test --locked -p toyos-net-shard(47 passed, 0 failed)cargo test --locked -p toyos-net-testnet -p toyos-net-tcp -p toyos-mdns -p toyos-dhcp(475 passed, 0 failed: the 464 and eleven of [tcp]'s,take5 from #775 andheld6 from #777)cargo test --locked --manifest-path userland/netstack/Cargo.toml(29 passed, 0 failed; the shipped resolver's nine among them; the 27 and #782's two invirtio_net.rs)cargo test --locked --lib -- hostws:: userlandhost:: sourcegate:: licence::from the root package (45 passed)cargo clippy --locked -p toyos-dns -p toyos-net-node -p toyos-net-ip -p toyos-net-udp -p toyos-net-shard --all-targets --keep-going -- <src/clippy.rs's ADOPTED as -W> -D warningscargo run -- --ci host(Host: 78 step(s), all green; the eight tests round 4's review names eachokin its log)cargo run -- --build-onlyc1dc7b176: the whole source change reverted ontomainat3fd6a2ac7,cargo test --locked --no-runof the five cratesr0-red-first-at-71db83dc1: finding 6's two tests and the test of the wake on round 3's sourcer0-red-first-at-d9a5cf2e5: finding 5's three tests on round 2's sourcer0-red-first-at-20b21432b: the four tests of findings 1 and 2 on round 1's sourceb3-the-bases-alias-test-at-20b21432b: the base's alias test on round 1's sourceEvery step's log at
c1dc7b176, the command first and the exit code last, is in the orchestrator's scratchpad under the stage'smerge1/logs/: one file a gate, named by its step. Round 4's, and every mutation's but m0's, are inlogs4/, round 3's inlogs3/, round 2's inlogs2/and round 1's inlogs/.Not run by me, as briefed: any guest test, anything on metal. Neither merge changed a line this stage ships:
toyos-dns/src/lib.rsis byte-identical tob2d903719, and its three lines ofuserland/netstack/src/main.rsmerged without a conflict into #782's file and then #783's. No clippy shape of the tree lintsuserland/netstack(a guest package); its three lines did not change after round 1 and were compiled by its host tests and by the image build.Growth
git diff --shortstat origin/main...8330a5c75: 12 files, +881 -112. Production:toyos-dns/src/lib.rs+103 -19,node/src/resolve.rs+84 -46,toyos-net-udp/src/lib.rs+10 -4,toyos-net-ip/src/lib.rs+1 -1,main.rs+3. Tests:toyos-dns/src/tests.rs+367,node/tests/resolve.rs+302 -33,nud.rs+4 -3, and one line each oftoyos-net-udp/tests/recv.rs,send.rsandtoyos-net-shard/tests/udp.rs. The track: +4 -3 lines.Round 4 alone, in this stage's files (
git diff --shortstat 71db83dc1 b2d903719 -- toyos-dns userland/netstack/node): 3 files, +72 -8; production istoyos-dns/src/lib.rs+12 -8, of which two lines are code (the send'sfree, anddue = freewith itsmaxgone) and the rest the docs of the two fields and the contract.Unsure of
ROUNDStimes with no interval. The interval is kept for a server that has not answered, which is the rule the review named; a failure is matched like an answer, so it is not an off-path sender's to give.ROUNDSallows it or need a count a server; I kept the base's order.issues/netstack-resolver-asks-a-dead-primary-first-on-every-lookup.mddescribes what ships and is untouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A