Repository navigation
A lost .local name is probed for again a minute after each loss: claimed when no host answers, at most one probing a minute whatever a peer sends, and one log line a loss - #800
Conversation
The owner ruled today on the question the network-stack track held open. Asked "A ToyOS machine that loses its .local name to a conflict stays nameless until its network link next returns, even after the other machine has left. Should it try for its name again by itself?", he answered "Retry on a slow interval (Recommended)". toyos-mdns: a conflicting response under the probe no longer parks the name in a state nothing leaves but a link after none. It owes a probing RETRY_MS, a minute, after the loss: `Claim::Lost` is gone, and a lost name is `Claim::Owed` a minute out, so `owed_at` carries the retry to both callers with no change in either. A probing no host answers claims and announces as at start-up; one the holder answers waits a minute from that answer. `Event::Lost` is said once until the name is claimed again, so a retry costs its owner's log nothing. A lost name that loses a tiebreak (RFC 6762 §8.2) waits the minute and not §8.2's second, which forged probes would turn into one probe in five seconds from a host that holds no name. A link after none still probes at once and replaces the retry owed. The minute: the owner's example was once a minute; it is twelve times §8.1's five seconds, so a host that really holds the name answers one probe a minute per host that wants it. The five tests that held "a lost name stays lost" now hold the minute; six new ones in toyos-mdns and two in the node's tests/name.rs hold the rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Mutations at Exit 101 is a test failure in each case, not a build failure; the tests each turned red:
m0-the-whole-rule-reverted.patchdiff --git a/userland/netstack/mdns/src/lib.rs b/userland/netstack/mdns/src/lib.rs
index eac4f424d..08dce4865 100644
--- a/userland/netstack/mdns/src/lib.rs
+++ b/userland/netstack/mdns/src/lib.rs
@@ -45,27 +45,19 @@
//! when the last 250 ms end is heard under the probe and takes the name,
//! where the other order would claim and announce it first.
//!
-//! **A lost name is not replaced, and is probed for again.** §9 recommends a
-//! responder change its name and probe again; this one holds none and says so
-//! to its owner once. The name was moved in by the owner, who also asks the
+//! **A lost name is not replaced.** §9 recommends a responder change its name
+//! and probe again; this one holds none, says so to its owner and waits for a
+//! link after none. The name was moved in by the owner, who also asks the
//! network to record it with the lease: a responder that picked another would
//! answer to a name nothing else on the machine knows, that no storage keeps
//! for the next boot (§9's third step), and that any host on the link could
-//! move again by answering for it. It probes for the same name again
-//! [`RETRY_MS`] after each loss, and at once on a link after none: a probing
-//! no host answers claims and announces the name as at start-up, and one the
-//! holder answers waits the interval again and says nothing. §9's loser "MUST
-//! cease using the name", and a probe uses none: it is a question, and
-//! nothing is announced or answered between a loss and a claim. §8.1 lets a
-//! failed probe be tried again five seconds later and asks for no retry at
-//! all.
+//! move again by answering for it.
//!
//! **What a peer can make this responder do.** Every byte read is a peer's,
//! from a source on this link (§11); none is trusted, and none is kept.
//!
//! - It keeps nothing a message brought: its state is its fixed fields, of
-//! which a conflict writes the claim, whether its loss was said and one
-//! time.
+//! which a conflict writes the claim and one time.
//! - One conflict costs at most one probing: three probes and two
//! announcements.
//! - §8.1: "If fifteen conflicts occur within any ten-second period, then the
@@ -78,25 +70,10 @@
//! the ten seconds before it is probed on at once, as §9 asks. So a peer's
//! messages buy at most one probing in five seconds, for as long as it
//! sends them, and the name is held again when it stops.
-//! - A lost name's probing begins at least [`RETRY_MS`] after the one
-//! before, whatever arrives: between two nothing is read, since no probe is
-//! out for a message to conflict with or tie; under a probe a conflicting
-//! response and a probe that wins the tiebreak each put the next probing
-//! the interval later, where §8.2's second would let forged probes buy one
-//! in five seconds. So a peer's messages buy at most three probes in the
-//! interval from a host that holds no name, no announcement and no word to
-//! its owner.
-//! - A link after none is probed on at once, whoever holds the name, in place
-//! of the retry that was owed and never beside it. Under a lost name it
-//! costs what it costs under any, its three probes, and §8.1's five seconds
-//! first within ten of a conflict: one probing in five seconds while a
-//! holder answers each.
-//! - Two packets from any host on the link take a held name, a response that
-//! takes it back to probing and a response under the probe, and one more in
-//! each interval keeps it: a host that answers every probe is what a holder
-//! of the name is. When it stops, the next probing claims the name. One
-//! that lets each retry claim the name and then takes it buys the owner two
-//! words in the interval, claimed and lost.
+//! - A lost name sends nothing and reads nothing until a link after none: it
+//! stays lost after the other host has left or a forger has stopped. Two
+//! packets from any host on the link buy that, a response that takes a held
+//! name back to probing and a response under the probe.
//!
//! Where an answer goes follows the question (§5.4, §6.7):
//!
@@ -294,9 +271,8 @@ pub enum Event {
/// announced and answered with from here on.
Claimed,
/// §8.1: another host answered for the name under the probe. It is not
- /// this host's, and nothing is announced or answered until it is
- /// [`Event::Claimed`]. Said once: a later probing that host answers too
- /// says nothing.
+ /// this host's, and nothing is announced or answered until a link comes
+ /// after none.
Lost,
}
@@ -320,13 +296,6 @@ const DEFER_MS: u64 = 1_000;
const CONFLICT_WINDOW_MS: u64 = 10_000;
const LIMITED_WAIT_MS: u64 = 5_000;
-/// How long after losing the name it is probed for again: a minute. A host
-/// that holds the name answers one probe a minute for each host that wants
-/// it, twelve times fewer than §8.1's five seconds would ask of it, and a
-/// name whose holder has left is back within the minute and the second a
-/// probing takes. §8.1's five seconds after a failed attempt are inside it.
-const RETRY_MS: u64 = 60_000;
-
/// §6: a record is multicast on an interface at most once a second.
const GROUP_EVERY_MS: u64 = 1_000;
@@ -350,16 +319,16 @@ enum Claim {
/// the name is held. A message conflicts only once `sent` is not zero.
Probing { sent: u8, at_ms: u64 },
Held,
+ Lost,
}
/// This host's one record on its link, on the caller's monotonic clock in
-/// milliseconds: probed for on every link after none and [`RETRY_MS`] after
-/// each loss, then announced twice a second apart (§8, §8.3), answered to
-/// whoever asks, and multicast at most once a second (§6), so no host can
-/// turn the queries it sends into a multicast to every host on the link at
-/// its own rate. A query §6 holds back is answered when the second ends, not
-/// dropped: the caller wakes at [`Responder::owed_at`], for that and for
-/// every probe, a retry's included.
+/// milliseconds: probed for on every link after none, then announced twice a
+/// second apart (§8, §8.3), answered to whoever asks, and multicast at most
+/// once a second (§6), so no host can turn the queries it sends into a
+/// multicast to every host on the link at its own rate. A query §6 holds back
+/// is answered when the second ends, not dropped: the caller wakes at
+/// [`Responder::owed_at`], for that and for every probe.
///
/// A message counts as sent once [`Responder::owed`] has handed it over.
#[derive(Debug)]
@@ -368,8 +337,6 @@ pub struct Responder<'a> {
/// The link the address is held on.
link: Option<Link>,
claim: Claim,
- /// [`Event::Lost`] was said, and [`Event::Claimed`] not since.
- lost: bool,
/// When the name last met a conflict.
conflict_ms: Option<u64>,
last_group_ms: Option<u64>,
@@ -384,7 +351,7 @@ pub struct Responder<'a> {
impl<'a> Responder<'a> {
pub const fn new(host: Host<'a>) -> Self {
- Self { host, link: None, claim: Claim::Owed { from_ms: 0 }, lost: false, conflict_ms: None, last_group_ms: None, owed_ms: None, again: false }
+ Self { host, link: None, claim: Claim::Lost, conflict_ms: None, last_group_ms: None, owed_ms: None, again: false }
}
/// This host is on `link` at `now_ms`, or on none while it holds no
@@ -428,7 +395,6 @@ impl<'a> Responder<'a> {
return (Some(probe(self.host, link.addr)), None);
}
self.claim = Claim::Held;
- self.lost = false;
event = Some(Event::Claimed);
self.owed_ms = Some(self.group_free_ms(now_ms, GROUP_EVERY_MS));
self.again = true;
@@ -480,6 +446,7 @@ impl<'a> Responder<'a> {
Claim::Owed { from_ms } => Some(from_ms),
Claim::Probing { at_ms, .. } => Some(at_ms),
Claim::Held => self.owed_ms,
+ Claim::Lost => None,
}
}
@@ -506,35 +473,33 @@ impl<'a> Responder<'a> {
self.rival(message, link, now_ms);
(None, None)
}
- Claim::Owed { .. } | Claim::Probing { .. } => (None, None),
+ Claim::Owed { .. } | Claim::Probing { .. } | Claim::Lost => (None, None),
}
}
/// A response: §6 has one from a port other than 5353 silently ignored,
- /// and one that conflicts takes a name under probe (§8.1), which is
- /// probed for again [`RETRY_MS`] later, and sends a held one back to
- /// probing (§9).
+ /// and one that conflicts takes a name under probe (§8.1) and sends a
+ /// held one back to probing (§9).
fn response(&mut self, message: &[u8], from: Source, link: Link, now_ms: u64) -> Option<Event> {
let probing = match self.claim {
Claim::Probing { sent, .. } if sent > 0 => true,
Claim::Held => false,
- Claim::Owed { .. } | Claim::Probing { .. } => return None,
+ Claim::Owed { .. } | Claim::Probing { .. } | Claim::Lost => return None,
};
if from.port != PORT || !conflicts(message, self.host, link.addr).unwrap_or(false) {
return None;
}
let wait = self.conflict(now_ms);
if probing {
- self.release(Claim::Owed { from_ms: now_ms.saturating_add(RETRY_MS) });
- return (!core::mem::replace(&mut self.lost, true)).then_some(Event::Lost);
+ self.release(Claim::Lost);
+ return Some(Event::Lost);
}
self.release(Claim::Owed { from_ms: now_ms.saturating_add(wait) });
None
}
/// A query heard under this host's probe: §8.2's comparison if it is
- /// another host's probe for the name. A lost name defers for
- /// [`RETRY_MS`], as it does to an answer.
+ /// another host's probe for the name.
fn rival(&mut self, query: &[u8], link: Link, now_ms: u64) {
let Some((kind, _)) = asked(query, self.host) else { return };
let Some((theirs, more)) = proposed(query, self.host, kind) else { return };
@@ -547,7 +512,7 @@ impl<'a> Responder<'a> {
Ordering::Less => false,
};
if later {
- let wait = self.conflict(now_ms).max(if self.lost { RETRY_MS } else { DEFER_MS });
+ let wait = self.conflict(now_ms).max(DEFER_MS);
self.release(Claim::Probing { sent: 0, at_ms: now_ms.saturating_add(wait) });
}
}m1-interval-five-seconds.patchdiff --git a/userland/netstack/mdns/src/lib.rs b/userland/netstack/mdns/src/lib.rs
index eac4f424d..bfac64c82 100644
--- a/userland/netstack/mdns/src/lib.rs
+++ b/userland/netstack/mdns/src/lib.rs
@@ -325,7 +325,7 @@ const LIMITED_WAIT_MS: u64 = 5_000;
/// it, twelve times fewer than §8.1's five seconds would ask of it, and a
/// name whose holder has left is back within the minute and the second a
/// probing takes. §8.1's five seconds after a failed attempt are inside it.
-const RETRY_MS: u64 = 60_000;
+const RETRY_MS: u64 = 5_000;
/// §6: a record is multicast on an interface at most once a second.
const GROUP_EVERY_MS: u64 = 1_000;m2-lost-said-at-every-loss.patchdiff --git a/userland/netstack/mdns/src/lib.rs b/userland/netstack/mdns/src/lib.rs
index eac4f424d..14f2016b6 100644
--- a/userland/netstack/mdns/src/lib.rs
+++ b/userland/netstack/mdns/src/lib.rs
@@ -526,7 +526,8 @@ impl<'a> Responder<'a> {
let wait = self.conflict(now_ms);
if probing {
self.release(Claim::Owed { from_ms: now_ms.saturating_add(RETRY_MS) });
- return (!core::mem::replace(&mut self.lost, true)).then_some(Event::Lost);
+ self.lost = true;
+ return Some(Event::Lost);
}
self.release(Claim::Owed { from_ms: now_ms.saturating_add(wait) });
Nonem3-claim-does-not-clear-lost.patchdiff --git a/userland/netstack/mdns/src/lib.rs b/userland/netstack/mdns/src/lib.rs
index eac4f424d..11954dfe2 100644
--- a/userland/netstack/mdns/src/lib.rs
+++ b/userland/netstack/mdns/src/lib.rs
@@ -428,7 +428,6 @@ impl<'a> Responder<'a> {
return (Some(probe(self.host, link.addr)), None);
}
self.claim = Claim::Held;
- self.lost = false;
event = Some(Event::Claimed);
self.owed_ms = Some(self.group_free_ms(now_ms, GROUP_EVERY_MS));
self.again = true;m4-lost-tiebreak-waits-a-second.patchdiff --git a/userland/netstack/mdns/src/lib.rs b/userland/netstack/mdns/src/lib.rs
index eac4f424d..f2d6fe990 100644
--- a/userland/netstack/mdns/src/lib.rs
+++ b/userland/netstack/mdns/src/lib.rs
@@ -547,7 +547,7 @@ impl<'a> Responder<'a> {
Ordering::Less => false,
};
if later {
- let wait = self.conflict(now_ms).max(if self.lost { RETRY_MS } else { DEFER_MS });
+ let wait = self.conflict(now_ms).max(DEFER_MS);
self.release(Claim::Probing { sent: 0, at_ms: now_ms.saturating_add(wait) });
}
}m5-links-return-under-a-lost-name-keeps-the-retry.patchdiff --git a/userland/netstack/mdns/src/lib.rs b/userland/netstack/mdns/src/lib.rs
index eac4f424d..1836793cd 100644
--- a/userland/netstack/mdns/src/lib.rs
+++ b/userland/netstack/mdns/src/lib.rs
@@ -398,6 +398,7 @@ impl<'a> Responder<'a> {
return;
};
match self.link.replace(link) {
+ None if self.lost => {}
None => self.release(Claim::Owed { from_ms: now_ms.saturating_add(self.limit_ms(now_ms)) }),
Some(was) if was.addr != link.addr && matches!(self.claim, Claim::Held) => {
self.owed_ms = Some(now_ms);m6-a-retry-the-holder-answers-is-retried-at-once.patchdiff --git a/userland/netstack/mdns/src/lib.rs b/userland/netstack/mdns/src/lib.rs
index eac4f424d..8894f2459 100644
--- a/userland/netstack/mdns/src/lib.rs
+++ b/userland/netstack/mdns/src/lib.rs
@@ -525,7 +525,7 @@ impl<'a> Responder<'a> {
}
let wait = self.conflict(now_ms);
if probing {
- self.release(Claim::Owed { from_ms: now_ms.saturating_add(RETRY_MS) });
+ self.release(Claim::Owed { from_ms: now_ms.saturating_add(if self.lost { wait } else { RETRY_MS }) });
return (!core::mem::replace(&mut self.lost, true)).then_some(Event::Lost);
}
self.release(Claim::Owed { from_ms: now_ms.saturating_add(wait) }); |
|
Review, round 1, of Net: 5 files, +292 −47. Production BLOCKERNone. NOTE
The rule against RFC 6762
The three decisions
Bounds while nameless
The mechanismVerified by reading. A loss leaves Tests and mutationsThe five rewritten responder tests each assert the deadline at the minute and silence to the millisecond before it; the two node tests moved from 60 s to 59 s of silence and the retry is asserted beside them. None is merely loosened: m1 reds all seven. The seven mutation patches apply to the code as it stands, their logs postdate the commit, each exits 101 on test failures. No test is built on smoltcp. I name no further mutation: a retry is the start-up state, so what breaks one breaks the other's tests. Evidence at this head
Other branches#796 and #797 touch none of these files. The move collides on the track's line only (NOTE above); LandingThe pull request is a draft, and LAND AFTER NAMED CHANGES |
…w often With `Event::Lost` said once until the next claim, that line is all the log holds for as long as the name stays lost, and "answers to no name" read as final. It now ends "and asks for <host>.local again every 60 s", so the silence after it reads as still asking and the claim line as its end. The number is `toyos_mdns::RETRY_MS / 1000`, which makes the constant `pub` beside `PORT`, `GROUP` and `TTL`: the interval has one declaration and the line cannot drift from it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
CI at |
…m end's order (#803) and the .local name's re-probe (#800), into the app grants Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
…nd's order (#803) and the .local name's probing (#800), into the HTTPS client ring_kat's line in DRIVEN_AND_SHARED met random_draws' there; the merge keeps main's, and the next commit decides ring_kat's place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
… the stream end's order (#803), into the stop's hold of the console wire Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
A machine that lost its
.localname stayed nameless until its link next returned, even after the other machine had left. It now probes for its own name again a minute after each loss, and takes it back when no host answers.Head
d6e9beaaf8aaad289141236c987c1bf9072ebdbe, onorigin/main1621281ae(#793); mergingorigin/mainat round 2 found it already there (git merge origin/main: already up to date).The rulings
Asked "When two machines on one network have the same ToyOS host name, what should the second one do?", the owner answered "Hold no name, say so (Recommended)". Asked on 2026-10-09 "A ToyOS machine that loses its .local name to a conflict stays nameless until its network link next returns, even after the other machine has left. Should it try for its name again by itself?", he answered "Retry on a slow interval (Recommended)". The option's description gave once a minute as an example.
What changed, per decision
The rule is in
toyos-mdns(userland/netstack/mdns/src/lib.rs). Of the callers, only the shipped shell's loss line changes.Claim::Lost, a state nothing left but a link after none, is deleted. A conflicting response under the probe setsClaim::Owed { from_ms: now + RETRY_MS }, the state a start-up probing begins in. SoResponder::owed_atcarries the retry with no new arm, and both callers already wake on it: the node throughName::next_deadline(userland/netstack/node/src/name.rs), the shipped shell throughResponder::wake_in(userland/netstack/src/mdns.rs), each a one-line map ofowed_at. Read, not changed.RETRY_MS = 60_000, my choice. The owner's example was once a minute. It is twelve times RFC 6762 §8.1's five seconds ("wait five seconds after any failed probe attempt before trying again"), so a host that really holds the name answers one probe a minute per host that wants it. A name whose holder left is back within 61 s of the last loss: the minute, at most 250 ms of delay, 750 ms of probing.Event::Claimed); one the holder answers waits a minute from that answer.userland/netstack/src/mdns.rs): "netstack: mDNS: another host answered for {host}.local; this machine answers to no name and asks for {host}.local again every 60 s", the numberRETRY_MS / 1000, soRETRY_MSispubbesidePORT,GROUPandTTL. WithLostsaid once, it is all the log holds while the name stays lost; the silence after it reads as still asking and the claim line as its end. No other line is added. The move (wt/toyos-move, which deletesmdns.rsand carries a copy of this string in itsserve.rs) takes the same text, built from the same constant, when it merges this.Event::Lostis said once until the name is claimed again (lost: bool, the one new field). A retry that meets the holder says nothing, so a retry writes no log line. Both callers log an event as it comes, with no logic of their own.Claimis one value, so the probing replaces the retry owed: two at once is unrepresentable.What a peer can buy from a host that holds no name
Tests
No test is built on smoltcp. The shell's part is the deadline it passes on: held by its existing
wake_in_asks_for_what_the_name_owes_and_nothing_else(userland/netstack/src/mdns.rs), since a retry is the sameClaim::Probing { sent: 0, at_ms }as a start-up delay; no test below drives the shell. No guest test is added or changed: no test reads the loss line, and the netcase boots wait for the claim line, unchanged, by event as before.toyos-mdns, six new (src/tests.rs):a_lost_name_is_probed_for_again_a_minute_after_the_loss_and_not_before: a pass every millisecond of the minute sends nothing; the probe leaves at the minute plus the drawn delay, for four draws; a new address under a lost name is the one the retry proposes.a_retry_the_holder_answers_loses_again_and_the_next_is_a_minute_after_it: answered after the first probe and in the last 250 ms; no secondLost.a_retry_no_host_answers_claims_and_announces_the_name: three probes, two announcements; and a name claimed is said lost when lost again.a_storm_of_forged_conflicts_at_a_lost_name_costs_one_probe_a_minute: a response, a winning probe and a query each millisecond for five minutes buy four probes and no event.forged_probes_that_win_every_tiebreak_at_a_lost_name_cost_one_probe_a_minute.a_links_return_under_a_lost_name_probes_at_once_in_place_of_the_retry: one probing, none where the retry was owed; with the holder there, the next is a minute from that loss.Five existing tests asserted that a lost name stays lost for an hour; they now assert nothing for 59,999 ms and the deadline at the minute.
The node, two new (
tests/name.rs), driven byNode::next_deadlinealone:a_lost_name_is_probed_for_again_a_minute_after_each_loss_and_claimed_once_no_host_answersa_links_return_under_a_lost_name_is_probed_on_at_once_and_no_retry_follows_itTwo existing node tests fired sixty seconds past a loss and expected silence; they fire 59.
Mutations
Seven, each a checked patch applied, run and restored in one script, tree clean after each. Patches, exits and the tests each turned red are in the first comment on this pull request. All seven exit 101 in
toyos-mdns.toyos-mdnstests/name.rslib.rsas onorigin/main)Lostsaid at every losslostm3 and m4 are green in the node's file: both are the responder's decisions, held by its own tests.
Gates, at this head (
d6e9beaaf), each by its own exit codecargo test --locked -p toyos-mdnscargo test --locked -p toyos-net-nodetests/name.rs21cargo test --locked -p netstackcargo run -- --ci hostcargo run -- --build-onlycargo test --test toyos-build -- netstack_socket_churnboot_netcaseawaits the claim line, so this boot read itThe netcase boot ran on a loaded, shared host:
uptimeload averages 31.87 37.89 35.61 before and 28.51 36.97 35.31 after, 14 cores. The tree was clean after.At round 1 (
129d45af2), before the loss line changed,cargo run -- --clippyexited 0, and the whole guest suite exited 0, 37 passed of 37 over 39 guests, x86-64 and AArch64; neither was re-run at this head.The oracle is RFC 6762 §8.1, §8.2 and §9, quoted at each test. The negative control is m0.
Size
6 files, +297 −49. Production:
lib.rs+63 −28, of which code is +10 −8 and the rest the module header and doc comments;node/src/name.rs+2 −2, a comment; the shell'smdns.rs+5 −2, the loss line. Tests: +226 −16. The track's name line: +1 −1.Unsure of
Claim::Oweddraws at the nextowed, as it already does inside §8.1's five-second wait. The alternative, a state that waits before it draws, is more code for the same distribution.Lostis no longer said for a loss that follows a link's return under a name already said lost. Onmaineach such loss was a line. The log now says a change of what the machine answers to, and nothing else.🤖 Generated with Claude Code
https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C