Repository navigation
ToyOS's own network stack, stage 3: toyos-net-ip, toyos-net-udp and toyos-dhcp - #635
Conversation
…net crate shares Stage 3 adds three crates (toyos-net-ip, toyos-net-udp, toyos-dhcp) that need the same injected Instant, the same SipHash-2-4 keyed function and the same counter declaration toyos-net-tcp carried privately. Each moves to toyos-net-wire, the crate every net crate already depends on, so the shard that composes them hands one Instant to all of them and no crate carries a second copy of either. toyos-net-tcp re-exports Instant, siphash24 and Key, so its API is unchanged; the TSval of an instant becomes a free function, and its counters are declared through toyos_net_wire::counters!. Its suite is the same 654 tests, green, before and after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 3's first crate, written clean-room from the stage-3 IP specification and the RFCs: per-interface addresses and the host routing table, frame dispatch and the IPv4 input policy, the reachability machine over ARP (RFC 4861 §7.3 with RFC 7048's UNREACHABLE), RFC 5227 conflict detection on the owner's 200 ms schedule, ICMP echo and errors under the two-level limiter, inbound errors attributed for the transports, and the IGMPv3 host with its IGMPv2 compatibility mode. Pull egress throughout: [ip]'s own frames wait in one FIFO and are built when they leave; every timer they feed starts at the hand-off; a UDP datagram is written straight into the caller's frame or held here for its next hop, and a full ring spends no datagram. toyos-net-wire gains what W-1 needs (IGMPv3 IS_IN (B), RFC 9776 §5.2 Table 5), `MulticastAddr: Ord`, and the counter macro takes an expression for a name and generates the per-rule refusal limiter, which toyos-net-tcp now uses instead of its own copy. Scenario tests so far: CLK, ADDR, RTE, IN, OUT, NUD, and the wire specification's owed [ip] and [shell] rows (IPP, ETH, IP, IPO, UDP-22). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every [5] scenario of the IP specification has its test but OUT-07, which needs the composed shard (stage 4): 185 of 186, plus the NUD-28 compile-fail doctests, and the wire specification's owed [ip] and [shell] rows except UDP-17 and UDP-18, which toyos-net-udp owns. Scenarios whose numbers the IP-D1 ruling moved are tested on the ruled schedule: probing scaled so the add-to-first-announcement worst case is 200 ms, ANNOUNCE_WAIT after the third probe's hand-off, announcements 2 s apart as RFC 5227 keeps them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 3's second crate, written clean-room from Part U of the stage-3 UDP/DHCP specification and the RFCs: sockets bound to any address, one address or one peer, at most one socket per port whatever the address (U-1), RFC 6056 Algorithm 1 over 49152-65535 with one draw per bind (U-2), bounded queues both ways (16 datagrams, 64 KiB) whose transmit side refuses back to the caller, broadcast only by permission (U-4), 0.0.0.0 as a source only for the acquisition socket and only to the limited broadcast, and ICMP errors for connected sockets alone (U-5). Egress is pulled: a datagram leaves only when the transmit opportunity offers it to toyos-net-ip, which writes it into the device's frame or holds it for its next hop; a held one spends no credit, so a socket's next datagram is never behind it (US-26). Accepted datagrams of a closed socket still leave (U-9). toyos-net-ip now counts every datagram it held and dropped (nb.pending-dropped): at resolution failure, and at link-down, where a datagram resolution had released but not yet sent is dropped and its sender told too. Every Part U scenario has its test but US-57, the pipe ABI mapping, which is netd's (stage 5); UDP-17 and UDP-18 are here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Stage 3's third crate, written clean-room from Part D of the stage-3 UDP/DHCP specification and the RFCs: RFC 2131's client with RFC 3396 long options, RFC 4039 rapid commit, an RFC 4361 client identifier (H-2) a server may echo (RFC 6842), and RFC 5227 conflict detection through toyos-net-ip before an acknowledged address is used (H-3 = IP-D1). Pure: each call takes the time and a draw source and returns at most one transmission, a configuration change and an address request. Messages are built to one layout, options in §D2.2's order, padded to 300 bytes; a server's is read by §D3's checks in order, each refusal one named counter, BOOTP replies, unauthenticated FORCERENEW and a lease without a mask refused and logged. Renewals follow RFC 2131 §4.4.5's halving with the 60 s floor; a late timer runs only the latest due event; NAK loops back off (H-7). The tests compare every client message the stack frames, through toyos-net-ip and toyos-net-udp, against the specification's frame vectors byte for byte (DISCOVER, REQUEST, RENEW, REBIND, REBOOT, DECLINE). DH-73's property found that link_up on a lease already expired entered INIT-REBOOT with a deadline in the past; such a lease now expires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…resentable toyos-net-ip: FAILED carries nothing (its hold-down is the entry's deadline), so Nud::Failed is a unit variant; the accessors no test or caller read go (Ip::mac, Ip::joined, Linked/Probing/Unreachable::mac, Unreachable::requests, Failed::since), and Limiter::global_burst is private. NUD's fail takes the INCOMPLETE entry's queue from the arm that matched it rather than re-matching a state it cannot be in. toyos-net-udp and toyos-dhcp: a refusal is a Refusal drained by drain_refusals, as it was the only event either crate had. UDP's port-0 checks yield the typed port they refuse on, so no unreachable second check remains; DHCP's renew takes the lease's timers from the one caller that has them, the lease degradation is a Client method, and the two limits nothing read (JITTER, NAK_BACKOFF_MAX) go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…ue's bound
ip.md §5.4 lists datagrams released from a pending queue among the frames
its 64-frame FIFO drops when full. A released datagram's sender was already
told it is held, so dropping it there is the silent loss the owner's premise
rules out ("a full ring backpressures the sender and never drops"); it stays
bounded by PENDING_TOTAL, counted where it was held until it leaves. The
test fills the queue with 64 of [ip]'s own frames and resolves eight
neighbours holding 64 datagrams: all 128 frames leave and nothing is counted
ip.control-queue-full. A mutation making release subject to the bound turns
it red, and no other test did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
A checked mutation per rule found four that no test turned red: - NUD request spacing removed: NUD-16's advice at 600 lands on PROBE, which ignores advice anyway, so the scenario never reached the rule. The new NUD-16 test sends the request at 0, confirms at 5 and advises at 100: the poll waits for 1,000 (ip.md §6.3, RFC 1122 §2.3.2.1). - Transmit credit ignored by [ip]: OUT-06 now asserts that no credit moves nothing and one credit moves the oldest frame. - The acquisition exception delivered to any socket on port 68: US-44 now also has a socket on 68 without the mark, which must not hear it (udp-dhcp.md §U5.3). - A datagram [ip] holds spending UDP's credit: with one credit, the next socket datagram leaves while the first waits for resolution (§U4.6 (4)). NUD-28's FAILED block had become vacuous when FAILED lost its fields; it now states that FAILED carries nothing, and the compiling twin binds STALE's Linked so the STALE block can fail only on the missing queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…3 owes ip.md §17 gives every scenario exactly one test, and a scenario with several inputs is one test that checks each; the four checks the surviving mutations asked for become parts of NUD-16, OUT-06, US-26 and US-44 instead of second tests under the same ids. The track's list of wire scenarios owed by stages 3 to 5 is gone: every one is tested here. In its place, what the stage 3 specifications still owe (OUT-07 to the shard, US-57 to netd, the fragment and path-MTU scenarios to IP hardening) and the three places stage 3 departs from them, each with its exit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
|
Review of #635 at ce7fcb9. This is the first posted round. The earlier reviewer was cut off and posted nothing. PROVENANCE: CLEAN.
Ready for review.
Growth (
Measured on ce7fcb9.
Scenario map.
BLOCKER
NOTE
REMOVE
Appendix: the green mutations and the three probesApply one with # R01
--- a/toyos-net-ip/src/counters.rs
+++ b/toyos-net-ip/src/counters.rs
@@ -125,10 +125,6 @@ impl Log {
if !rule.logged() {
return;
}
- if self.refusals >= limits::EVENTS {
- self.count(Counter::IpEventOverflow);
- return;
- }
self.refusals = self.refusals.saturating_add(1);
self.events.push(Event::Refused(Refusal { rule, iface, peer }));
}
# R02
--- a/toyos-net-ip/src/input.rs
+++ b/toyos-net-ip/src/input.rs
@@ -48,8 +48,7 @@ fn dispatch(i: &Interface, frame: &Frame<'_>) -> Option<Counter> {
/// The acquisition exception (ruling H-1): before the interface holds a usable address, a
/// unicast datagram to UDP port 68 in a frame to our MAC is admitted for the DHCP client alone.
fn acquisition(i: &Interface, link: MacClass, packet: &Ipv4Packet<'_>) -> bool {
- link == MacClass::Individual
- && i.usable().next().is_none()
+ i.usable().next().is_none()
&& packet.protocol() == Protocol::Udp
&& !packet.is_fragment()
&& UdpDatagram::parse(packet).is_ok_and(|d| d.destination_port().get() == ACQUISITION_PORT)
# R03
--- a/toyos-dhcp/src/lib.rs
+++ b/toyos-dhcp/src/lib.rs
@@ -44,7 +44,7 @@ toyos_net_wire::counters! {
NotReply = "dhcp.not-reply", logged;
HardwareType = "dhcp.hardware-type", logged;
ChaddrMismatch = "dhcp.chaddr-mismatch";
- BootpReply = "dhcp.bootp-reply", logged;
+ BootpReply = "dhcp.bootp-reply";
OptionTruncated = "dhcp.option-truncated", logged;
NoEnd = "dhcp.no-end";
OverloadInvalid = "dhcp.overload-invalid", logged;
# R04
--- a/toyos-net-ip/src/lib.rs
+++ b/toyos-net-ip/src/lib.rs
@@ -131,7 +131,7 @@ pub mod limits {
pub const ANNOUNCE_NUM: u8 = 2;
pub const ANNOUNCE_INTERVAL: Duration = Duration::from_secs(2);
pub const MAX_CONFLICTS: u32 = 10;
- pub const RATE_LIMIT_INTERVAL: Duration = Duration::from_secs(60);
+ pub const RATE_LIMIT_INTERVAL: Duration = Duration::from_secs(6);
pub const DEFEND_INTERVAL: Duration = Duration::from_secs(10);
/// The conflict count starts over after this long without a conflict.
pub const CONFLICT_RESET: Duration = Duration::from_secs(600);
# R07
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -205,9 +205,9 @@ fn make_room(i: &mut Interface, cx: &mut Cx<'_>) -> bool {
return true;
}
let class = |n: &Neighbour| match &n.state {
- Nud::Failed => Some(0),
+ Nud::Failed => Some(2),
Nud::Unreachable(u) if u.quiescent() && !n.queued => Some(1),
- Nud::Stale(_) => Some(2),
+ Nud::Stale(_) => Some(0),
_ => None,
};
let victim = i.neighbours.iter().filter_map(|(a, n)| class(n).map(|c| (c, n.used, *a))).min().map(|(_, _, a)| a);
# R09
--- a/toyos-net-ip/src/input.rs
+++ b/toyos-net-ip/src/input.rs
@@ -144,10 +144,6 @@ impl Ip {
igmp::input(i, &mut cx, &packet, message.message());
None
}
- Protocol::Tcp if cast != Cast::Unicast => {
- self.log.count(Counter::IpTcpNotUnicast);
- None
- }
Protocol::Tcp => {
let segment = TcpSegment::parse(&packet).map_err(|e| self.log.wire(e.name())).ok()?;
Some(Delivery::Tcp(arrival, segment))
# R11
--- a/toyos-net-udp/src/lib.rs
+++ b/toyos-net-udp/src/lib.rs
@@ -253,7 +253,7 @@ impl Udp {
fn refuse<T>(&mut self, rule: Counter, local: (Ipv4Addr, Port), peer: (Ipv4Addr, u16)) -> Result<T, Error> {
self.count(rule);
if rule.logged() {
- if self.refusals.len() >= limits::EVENTS {
+ if false && self.refusals.len() >= limits::EVENTS {
self.count(Counter::EventOverflow);
} else {
self.refusals.push(Refusal { rule, local, peer });
# R12
--- a/toyos-dhcp/src/lib.rs
+++ b/toyos-dhcp/src/lib.rs
@@ -424,7 +424,7 @@ impl Client {
if !rule.logged() {
return;
}
- if self.refusals.len() >= limits::EVENTS {
+ if false && self.refusals.len() >= limits::EVENTS {
self.counters.add(Counter::EventOverflow, 1);
} else {
self.refusals.push(Refusal { rule, peer });
# R17
--- a/toyos-net-ip/src/counters.rs
+++ b/toyos-net-ip/src/counters.rs
@@ -84,7 +84,7 @@ toyos_net_wire::counters! {
IcmpQuoteShort = "icmp.quote-short";
IcmpRedirect = "icmp.redirect", logged;
IcmpTimestampRequest = IcmpError::TimestampRequest.name(), logged;
- IcmpSourceQuench = IcmpError::SourceQuench.name(), logged;
+ IcmpSourceQuench = IcmpError::SourceQuench.name();
IcmpEchoRepliesSent = "icmp.echo-replies-sent";
IcmpErrorsSent = "icmp.errors-sent";
IgmpV1Query = "igmp.v1-query", logged;
# R18
--- a/toyos-dhcp/src/lib.rs
+++ b/toyos-dhcp/src/lib.rs
@@ -657,7 +657,7 @@ impl Client {
/// A server's UDP payload and the IPv4 source it came from (§D3).
pub fn receive(&mut self, now: Instant, payload: &[u8], from: Ipv4Addr, mut draw: impl FnMut() -> u32) -> Output {
- let peer = Peer::From(from);
+ let peer = Peer::From(Ipv4Addr::UNSPECIFIED);
let reply = match Reply::parse(payload, self.mac.get()) {
Ok(reply) => reply,
Err(rule) => {
# R19
--- a/toyos-dhcp/src/lib.rs
+++ b/toyos-dhcp/src/lib.rs
@@ -63,7 +63,7 @@ toyos_net_wire::counters! {
YiaddrInvalid = "dhcp.yiaddr-invalid", logged;
NoLeaseTime = "dhcp.no-lease-time", logged;
LeaseZero = "dhcp.lease-zero", logged;
- NoSubnetMask = "dhcp.no-subnet-mask", logged;
+ NoSubnetMask = "dhcp.no-subnet-mask";
MaskInvalid = "dhcp.mask-invalid", logged;
RouterInvalid = "dhcp.router-invalid", logged;
DnsInvalid = "dhcp.dns-invalid", logged;
# R20
--- a/toyos-dhcp/src/lib.rs
+++ b/toyos-dhcp/src/lib.rs
@@ -53,7 +53,7 @@ toyos_net_wire::counters! {
WrongDirection = "dhcp.wrong-direction", logged;
Forcerenew = "dhcp.forcerenew", logged;
MessageTypeUnsupported = "dhcp.message-type-unsupported", logged;
- XidMismatch = "dhcp.xid-mismatch";
+ XidMismatch = "dhcp.xid-mismatch", logged;
ClientIdMismatch = "dhcp.client-id-mismatch", logged;
NoServerId = "dhcp.no-server-id", logged;
ServerIdInvalid = "dhcp.server-id-invalid", logged;P1 and P2 are in #[test]
fn p1_close_parks_accepted_datagrams_past_every_bound() {
let mut u = U::uf();
let mut accepted = 0usize;
for _ in 0..100 {
let id = u.bind(ANY, 50_001).unwrap();
for _ in 0..limits::TX_DATAGRAMS {
u.udp.send_to(&mut u.ip, id, DNS, 53, &[7; 1_000]).unwrap();
accepted += 1;
}
u.udp.close(id).unwrap();
}
let left = u.out().len();
assert!(left <= limits::TX_DATAGRAMS, "{accepted} accepted, {left} held past any socket's bound");
}
#[test]
fn p2_a_reused_slot_takes_one_turn() {
let mut u = U::uf();
let x = u.bind(ANY, 50_001).unwrap();
u.udp.send_to(&mut u.ip, x, DNS, 53, b"x").unwrap();
u.udp.close(x).unwrap();
assert_eq!(u.out_with(1).len(), 1);
let y = u.bind(ANY, 50_002).unwrap();
let z = u.bind(ANY, 50_003).unwrap();
for _ in 0..2 {
u.udp.send_to(&mut u.ip, y, DNS, 53, b"y").unwrap();
u.udp.send_to(&mut u.ip, z, DNS, 53, b"z").unwrap();
}
let order: String = u.out_with(2).iter().map(|f| char::from(ip_of(f).unwrap().payload()[8])).collect();
assert_eq!(order, "yz");
}P3 is in #[test]
fn p3_the_acquisition_exception_admits_unicast_only() {
let mut h = H::bare();
let ip = udp(R, Ipv4Addr::new(239, 9, 9, 9), 67, 68, b"offer");
assert_eq!(cast_of(&h.frame(ð(MAC_A, MAC_R, 0x0800, &ip))), None);
}SEND BACK |
…rithmetic, and pinned refusals The review of #635 at ce7fcb9 upheld nine blockers; the spec owner's rulings for this round bind beside them. Per decision: - toyos-net-wire holds the IPv4 address classes and `Cidr` (addr.rs): `is_host`, `is_martian`, and a prefix built from an address and a length or a contiguous mask. toyos-net-ip, toyos-net-udp and toyos-dhcp read them from there; their copies are deleted. toyos-dhcp does not depend on toyos-net-ip, so the arithmetic moved down rather than across. - The acquisition exception (C-3, ip.md §2.4 (5)) admits a destination that names one host only. A host destination can only arrive in a frame to our MAC (§4.1 step 3 refuses it in a group frame first), so the separate link check was dead and is gone. - toyos-net-udp's close holds at most CLOSED_DATAGRAMS (one socket's queue) of accepted datagrams together, and counts the rest `udp.tx-discarded-on-close`: architecture §3.3 holds nothing without a bound, and nobody is left to refuse them to. A closed socket's turn leaves with it, and the closed sockets' datagrams take one turn among the sockets instead of going first. - ip.md §5.4 as amended: the control queue holds at most 64 frames, released datagrams included. A released datagram that finds it full stays at the head of its neighbour's pending queue and enters as room appears; it is its neighbour's until it has left. An entry holding one is evicted after every other candidate (§6.8), dropping them as `nb.pending-evicted`, and is not idle: an idle lifetime that passed while it held them ends when the last leaves (NUD-29, NUD-30). - IGMP caps a query's sources at one past the limit before merging, so a merge is bounded. - tcp's unused re-exports of REFUSAL_LOG_INTERVAL and siphash24 go; its tests read siphash24 from toyos-net-wire. `Ip::wire_refusals` goes; `wire_counters` answers the same. Tests the review asked for: MUST-57 in IN-09; the undrained-refusal bound in MOD-01, US-20 and DH-38; DHCP refusals pinned with §D3.1's log flag as a literal and the refusal's peer; §13.2's logged list as literals in MOD-02; ACD-08's 60 s as a literal; §6.8's full eviction order in NUD-22; the acquisition exception's negative case, its probing case and `ip.acquisition-admitted` in US-44; close's bound, slot reuse and turn in US-53; `udp.no-route` against `udp.no-source-address` in US-24; DH-73's exactly one reason; NUD-28's Probe and Unreachable. New scenarios OUT-08, NUD-29 and NUD-30 each have their test. The track drops the §5.4 departure and the renew-unroutable departure (netd's at stage 5 now), and records DH-64's announce request, close's bound, and stage 3's missing independent oracle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
A closed socket's stale turn is consumed by the next transmit opportunity, so a slot reused only after one never meets it: the check that close takes its turn away (the review's P2) passed whether or not close did. The slot is now reused at once, and the mutation that leaves the turn behind goes red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
|
Review of #635 at 02dde43, round 4. Ready for review.
PROVENANCE: CLEAN.
Round 2's BLOCKERs. Judged on the PR body's exits on 6a72869 (baseline exit 0, 1,044 passed). None was re-run here.
The PR body's Unsure items, judged.
Growth.
BLOCKER
NOTE
REMOVE
Appendix: the mutationsApply one with # A1: the last released datagram's leaving deletes the entry even when its lifetime has not passed
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -342,7 +342,7 @@
/// a deadline armed but one whose idle lifetime passed while it held them, which goes now.
pub(crate) fn drained(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) {
let Some(n) = i.neighbours.get(&addr) else { return };
- let idle = matches!(n.state, Nud::Stale(_) | Nud::Unreachable(_)) && !n.queued && cx.timers.get(timer(cx, addr)).is_none();
+ let idle = matches!(n.state, Nud::Stale(_) | Nud::Unreachable(_)) && !n.queued;
if idle {
remove(i, cx, addr);
route::refresh_active(i, cx);
# A2: a quiescent UNREACHABLE entry idles out while it holds released datagrams
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -330,7 +330,7 @@
cx.timers.arm(timer(cx, addr), now.after(IDLE_LIFETIME));
}
// Not idle while released datagrams are its: `drained` deletes it once they have left.
- Nud::Stale(_) | Nud::Unreachable(_) if cx.control.holds(cx.iface, addr) => {}
+ Nud::Stale(_) if cx.control.holds(cx.iface, addr) => {}
Nud::Stale(_) | Nud::Unreachable(_) | Nud::Failed => {
remove(i, cx, addr);
route::refresh_active(i, cx);
# B: every probing interval halved (first use 57 to 100 ms)
--- a/toyos-net-ip/src/lib.rs
+++ b/toyos-net-ip/src/lib.rs
@@ -120,7 +120,7 @@
use core::time::Duration;
const fn scaled(rfc_ms: u64) -> Duration {
- Duration::from_nanos(rfc_ms.saturating_mul(1_000_000).saturating_mul(200) / 7_000)
+ Duration::from_nanos(rfc_ms.saturating_mul(1_000_000).saturating_mul(100) / 7_000)
}
pub const PROBE_WAIT: Duration = scaled(1_000);SEND BACK |
…ir queue The resolved states carry the pending queue §6.2 gives them. REACHABLE, STALE, DELAY, PROBE and UNREACHABLE hold a `Released` queue that only drains; INCOMPLETE's `Pending` is the only one with `push`, and resolution moves it into the resolved state. NUD-28's doctests now push into each resolved queue (compile_fail) beside a twin that pushes into INCOMPLETE's and drains every resolved one. The control queue keeps only the order: a released datagram's turn names its entry by address and id, and the datagram leaves from the entry's queue when the turn comes, to the MAC of that moment. `Control::holds`, `Control::take` and `Released::toward` are gone, and with them the scans `make_room` ran per candidate. An evicted entry's turns name an id no entry holds, so they leave nothing and spend no credit; the id keeps them from giving a later entry for the same address an earlier place, which NUD-29's second arm now pins. The idle rule is pinned on both sides. NUD-29's first arm asserts that B stays STALE once 1 and 2 have left, since its lifetime has not passed. NUD-30 gains a quiescent UNREACHABLE arm beside the STALE one. ACD-01 asserts IP-D1's scaled constants (28,571,428 and 57,142,857 ns) and the 114,285,713 ns floor as literals. `toyos-net-tcp`'s connect refuses a remote that `addr::is_host` refuses, and `unicast()` is gone; HS-35's three addresses stay refused, and 0/8 and 127/8 are refused too (MUST-46). The spec owner's ruling on the ACD NOTE: a probe or an announcement that finds the control queue full is not counted as sent. It waits for room, as a released datagram's turn does, so an address is never verified without its probes on the wire. A defence waits one at a time. ACD-18, a proposed id, fills the control queue from a second interface during probing, and again when a defence is due. A request is built when it leaves, from the state of that moment: broadcast in INCOMPLETE and UNREACHABLE, unicast in PROBE, and in REACHABLE, STALE and DELAY not at all (§6.3). OUT-08's first frame, B's request queued before the reply, no longer leaves. The review's REMOVEs are deleted: egress's header clause and `toyos-net-wire::addr`'s "the one place" clause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
A datagram resolution released waits in its entry's queue and leaves to the MAC the entry holds when its turn comes. NBR-08 gains the arm that pins it: R resolves to MAC R while the control queue is full, moves to MAC X, and the datagram leaves to MAC X. Stamping the MAC at resolution turns it red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
connect now refuses what `toyos_net_wire::addr::is_host` refuses, which takes in 0/8 and 127/8 beside HS-35's broadcast, multicast and unspecified remotes (MUST-46). HS-35 adds 127.0.0.1 and 0.1.2.3, so the old predicate turns it red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
A turn whose entry was evicted stays in the control queue until the transmit opportunity reaches it; it leaves nothing then. Until the eviction counted it out of the interface's `held`, so repeated last-resort evictions behind a stalled transmit could stack dead turns without bound. `held` now counts pending datagrams and every turn not yet reached, and a turn is spent when it is reached, so PENDING_TOTAL bounds them together. NUD-29 gains an arm: after B's eviction the interface takes 62 datagrams, not 64, until B's two turns are reached. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
|
Review of #635 at 3bd75d3, round 5. Ready for review.
PROVENANCE: CLEAN.
Round 4's BLOCKERs.
The six rulings. Each is met.
The four changes beyond the brief.
Growth.
BLOCKER
NOTE
REMOVE
SEND BACK |
Round 5's review upheld a BLOCKER: an evicted entry's turns stayed in the control queue and counted against PENDING_TOTAL until the transmit opportunity reached them, so NUD-29 pinned a refusal with 62 datagrams queued where ip §6.5 refuses only at 64. `remove` now drops the entry's turns from the control queue, both from the queue itself and from those waiting for room, refills it, and takes them out of the interface's count. Every other removal happens with the entry's queue empty, so the turns for an address are always its current entry's. No turn outlives its entry, and the id that told a later entry's turns from an evicted one's goes: `Neighbour::id`, `Interface::entries`, `insert`'s counter, `Turn::id` and `leave`'s filter. NUD-29's second arm now accepts PENDING_TOTAL datagrams while the control queue still holds everything it held before the eviction. Its first arm, a later entry at B's address behind a defence, now goes red if the turns stay: the first of them would carry the new entry's datagram out ahead of the defence. A request queued behind released datagrams could strand a STALE entry: if its idle lifetime passed while the datagrams waited, the last one's leaving kept the entry because the request was still queued, and the request's drop at egress deleted nothing. `request_leaves` now calls `drained` when it drops a request, and `drained` checks the released queue itself, so `leave` calls it after every datagram rather than only after the last. NUD-30's third arm pins the deletion. OUT-08's second arm pins the other half of the request form chosen at departure: a request queued in INCOMPLETE and still queued when the entry reaches PROBE leaves unicast to the cached MAC, and it is the only request that leaves. `Nud::released()` had one caller and is folded into `releasing()`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
|
Review of #635 at 8ea5e93, round 6. Ready for review.
PROVENANCE: CLEAN.
Round 5's BLOCKER.
Round 5's NOTEs.
Growth.
BLOCKER
NOTE
REMOVE
Appendix: the mutationsApply one with # M-a: drop_turns ignores the interface
--- a/toyos-net-ip/src/egress.rs
+++ b/toyos-net-ip/src/egress.rs
@@ -135,7 +135,7 @@
pub fn drop_turns(&mut self, iface: IfIndex, next_hop: Ipv4Addr) -> usize {
let mut dropped = 0usize;
let mut keep = |item: &Item| {
- let theirs = matches!(item, Item::Turn(t) if t.iface == iface && t.next_hop == next_hop);
+ let theirs = matches!(item, Item::Turn(t) if t.next_hop == next_hop);
if theirs {
dropped = dropped.saturating_add(1);
}
# M-b: the room drop_turns frees is not refilled
--- a/toyos-net-ip/src/egress.rs
+++ b/toyos-net-ip/src/egress.rs
@@ -143,7 +143,6 @@
};
self.items.retain(&mut keep);
self.waiting.retain(&mut keep);
- self.refill();
dropped
}
# M-c: a queued request does not keep a drained entry
--- a/toyos-net-ip/src/nud.rs
+++ b/toyos-net-ip/src/nud.rs
@@ -438,7 +438,7 @@
/// datagrams, which goes once nothing of it waits there.
fn drained(i: &mut Interface, cx: &mut Cx<'_>, addr: Ipv4Addr) {
let Some(n) = i.neighbours.get(&addr) else { return };
- let waits = n.state.releasing() || n.queued;
+ let waits = n.state.releasing();
let idle = matches!(n.state, Nud::Stale(_) | Nud::Unreachable(_)) && !waits && cx.timers.get(timer(cx, addr)).is_none();
if idle {
remove(i, cx, addr);SEND BACK |
Ruling 2 deletes an entry whose idle lifetime passed while its queue held released datagrams at drain. `drained` also kept it while a request of its waited in the control queue (`|| n.queued`), which gave the tree a second idle rule beside `fire`'s, which already deletes an idle entry whose request still waits. The term goes, and with it the only reason `request_leaves` called `drained`. A request that outlives its entry finds none when it leaves and sends nothing, as it does after `fire`. NUD-30's third arm now takes 1 and 2 out alone and asserts that B is gone as 2 leaves, before the request behind them is taken out. NUD-29 gains two arms for `Control::drop_turns`, which no test pinned: - RTE-004's overlapping prefixes put X on both interfaces. X on if1 holds 1 and 2 whose turns wait when X on if0 ends its FAILED hold-down; if1's datagrams must still leave to X on if1. This pins the `t.iface == iface` filter. - B resolves while the control queue has room, so its turns are inside it; 62 replies fill it behind them and a defence waits. Evicting B frees their room, which is the defence's: it must leave ahead of the new entry's request. This pins the `refill` in `drop_turns`. The two arms and the three earlier ones share their setup through `b_and_511`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
|
Review of #635 at c1474cd, round 7. Scope: Ready for review.
PROVENANCE: CLEAN for this round's delta (72 added lines).
Round 6's BLOCKERs.
Round 6's NOTE: done.
Growth.
BLOCKERNone. NOTE
REMOVENone. LAND AFTER NAMED CHANGES |
`drained` had one caller, `leave`, which already holds the entry. Its condition moves into `leave` over that borrow, so the second map lookup and the doc comment go, and `fire`'s comment names `leave` instead. Behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
`b_and_511` stated a count that `limits::nud::TABLE_MAX` owns: its loop reads the limit, and the name and its doc comment went false the moment the limit moved. `b_in_a_full_table` and its doc name what it builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
`remove` dropped an entry's turns but left its queued `Item::Request`. `fire`, eviction and `leave` each delete an entry whose request may still wait, and a later INCOMPLETE entry at the same address sets `queued` again. The orphan then left as that entry's request, from its own earlier slot in the FIFO, and the new entry's item sent nothing. Count, form and spacing still came from the live entry; ip.md §5.4's order did not. `Control::drop_turns` becomes `purge_entry`: it drops the entry's requests with its turns, and still returns only the turns, since a request never counted toward `held`. No item outlives its entry. NUD-30's third arm goes on past B's deletion: a host asks for A, which queues a reply, and then a datagram to B makes a new entry there. The reply must leave first and the new entry's request after it. Without this change the orphan leaves as B's request ahead of the reply, and the test fails at the reply's assertion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…toyos-noredlist #588 landed first, and its `usb_stick_left` arms `usb-transport-break`, `usb-reset-moves`, `usb-reset-moves-after` and `usb-reset-moves-configured` again, its T14 row (`usbbreak`) arms `usb-transport-break`, and its metal record carries the `usbbreak` rows. Those stay, with `msc::transport_break`, `msc::reset_moves`, `RECOVERING` and `Quiet::Staged`. Everything else this branch deleted with `usb_transport_break` (90ecefe, b5c59cb, 402107b, 9ebf080) stays deleted: no test on either side arms it. Conflicts, every hunk: - `kernel/src/actuator.rs`, two hunks. First: main's eight USB rows. Kept `usb_transport_break`, `usb_reset_moves`, `usb_reset_moves_after` and `usb_reset_moves_configured`, with `usb_reset_moves`' doc losing its citation of the deleted `usb_transport_break`; dropped `usb_transport_offline`, `usb_reset_break`, `usb_transport_break_owed` and `usb_transport_break_flushed`, armed only by the deleted `usb_transport_break`, `log_flush_retry` and `partition_claim_departure`. Second: dropped `watch_window` (`blocking_read_window`, deleted in dc62efc), kept #634's `handler_post`. - `kernel/src/drivers/xhci/wait/msc.rs`, fourteen hunks, misaligned by #588's rewrite of the file. Resolved as main's file with this branch's deletions applied to it: `Asks` and `bot`'s `asks` argument at its five call sites; `transport_break`'s owed and flushed half (`WROTE`, `FLUSHED`, `AFTER_A_FLUSH`, `wrote`, `flushed`, their calls in `MscDevice::wrote` and the flush, and `arm`'s argument), so `arm` reads `usb-transport-break` alone; `return_silent` and its hook in `served`; `reset_break` and the wrapper around `climb`, whose body `climb_until_in_step` carries again; `short_read` and its hold and release around the data phase; `staged` and its checks in `bot`, the bad signature, the withheld CBW, the gone port and the unanswered wait; the bind's `slow_return` stall and INQUIRY staging, and `slow_return` itself; `read_serial`'s short ask. `reset_moves`, its three holds and `RECOVERING` are main's as they stand. - `kernel/src/watch.rs`, one hunk: dropped the `window` module (`watch-window`), kept #634's `handler_post` module. - `toyos-sched/src/watch.rs`, one hunk: the same, `window` dropped and `handler_post` kept. - `src/lib.rs`, one hunk: kept #652's `pub mod n2`, dropped `pub mod redlist`. - `tests/common/power.rs`, one hunk: main's only change there renamed `transport_break_chain`'s doc to `usb_stick_left`; its one caller is the deleted `usb_transport_break`, so it stays deleted. - `tests/common/usb.rs`, one hunk holding main's whole `usb_transport_break` region. Kept #588's `Held`, `usb_stick_left`, `a_stick_that_left_under_its_rung`, `transport_break_on_metal`, `transport_break_recovered` (which `tests/checks/usb.rs` judges) and `broke_on`; the rest is `usb_transport_break`'s and stays deleted: `a_read_whose_first_wait_spent_its_budget_goes_out_again`, `serial_short_is_not_read`, `cpu_of`, `line_with`, `Moved`, `a_stick_its_reset_moved_carries_on`, `abandoned_write_is_taken_offline`, `no_command_was_refused`, `port_gone_is_left_to_the_teardown`, `control_requests`, `command_blocks`, `every_reset_is_followed_by_a_test_unit_ready`, `is_a_rungs_configuration`, `every_port_reset_is_followed_by_a_test_unit_ready`, `every_reset_is_followed_by_both_clears` and `transport_gives_up`. - `tests/toyos.rs`, five hunks. `MACHINE_TESTS`: kept `usb_stick_left` and #634's `handler_post_without_a_pass`; dropped `usb_transport_break`, `xhci_full_speed_device` (5e42235), `blocking_read_window` and `user_copy_races_munmap` (7ea6be1). `METAL`: kept #588's `usb_stick_left` row. Dispatch: kept `usb_stick_left` and `handler_post_without_a_pass`; dropped `usb_transport_break`, `xhci_full_speed_device` and `smp_failed_ap_leaves_no_hole` (aedcf17). Outside the conflicts: - `kernel/src/drivers/xhci/wait/mod.rs`: `Quiet::Staged` and its line come back, which `transport_break::take` answers with; the auto-merge had taken 9ebf080's deletion. The `reset_break` and `return_silent` hooks stay deleted. - `src/metal.rs`: `FLASHABLE`'s `usb-transport-break` row comes back (b5c59cb took it), since `usb_stick_left`'s T14 arm flashes it. - `issues/kernel/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md`: records `4f2bea143`, this merge's second parent, as the tree holding #588's version of `usb_transport_break`, in place of the instruction to record it. - `issues/boot-media/logd-ends-the-boots-log-on-one-refused-create-and-nothing-durable-says-so.md`: drops its citation of `power::transport_break_chain`, deleted in 90ecefe. The `rust` gitlink is main's. On this tree: `cargo run -- --build-only` exit 0; the kernel's `cargo check` for x86-64 and AArch64, bare, with `boot-actuators` and with `boot-actuators,test-actuators`, exit 0 each; `cargo test --test toyos-build --no-run` exit 0, no warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Stage 3 of ToyOS's own network stack (
issues/design-debt/toyos-has-its-own-network-stack.md). Three crates, built clean-room fromip.md,udp-dhcp.mdand the RFCs. Liketoyos-net-wireandtoyos-net-tcp, they are pure:no_stdwithalloc,forbid(unsafe_code), and clippy forbids arithmetic side effects, indexing, unwrap, expect, panic andasoutside tests. They read no clock, draw no randomness and do no I/O. Nothing in the image uses them yet: netd moves over at stage 5.What changed
toyos-net-ip: per-interface addressing and host routing; ARP under one reachability machine (RFC 4861 NUD with RFC 7048's UNREACHABLE, a typestate enum whose states hold only their own fields); RFC 5227 conflict detection with defence (b); ICMP echo, errors, suppression and the two-level error limiter; the IGMPv3 any-source host (RFC 9776) with IGMPv2 compatibility mode; frame dispatch and the IPv4 input policy; pull egress.toyos-net-udp: one socket per port (U-1), RFC 6056 Algorithm 1 ephemeral ports (U-2), the demultiplexer, receive and send queues of 16 datagrams or 64 KiB, broadcast permission (U-4), send-to-self and loopback refused (U-8), ICMP errors for connected sockets only, and a bounded hold for what closed sockets had accepted.toyos-dhcp: the RFC 2131 client with RFC 3396 long options, RFC 4039 rapid commit, an RFC 4361 client identifier a server may echo (RFC 6842), and conflict detection throughtoyos-net-ip. Its draws come in a fixed order: the xid first, then any jitter.toyos-net-wirenow also holds what every net crate shares: the caller'sInstant, SipHash-2-4 (moved fromtoyos-net-tcpwith its bytes unchanged), thecounters!macro withRefusalLog, which admits one log line per rule per 10 s, and the IPv4 address classes and prefixes (addr:is_host,is_martian,Cidr). Every net crate reads the address arithmetic from there and keeps no copy of it. The IGMPv3 report builder takesGroupRecord, whose IS_IN carries its sources for §10.5's group-and-source answer, andMulticastAddrisOrd.toyos-net-tcp:connectrefuses a remote thataddr::is_hostrefuses, and its ownunicast()is gone (decision 17).dhcp.renew-unroutable, FRA, PMTU), the three places this stage departs from them, and stage 3's missing independent oracle, each with an exit.Cargo.lockgains only the three crates.Decisions
toyos-net-ipholds for resolution spends no credit, so the socket's next datagram does not wait behind it. A full send queue refuses the sender (udp.tx-queue-full) and the caller keeps its datagram.ip.md§5.4, §6.2 and §6.5 as the owner amended them. REACHABLE, STALE, DELAY, PROBE and UNREACHABLE each carry a pending queue that only drains (Released:pop, nopush). INCOMPLETE'sPendingis the only queue withpush, and resolution moves it into the resolved state. NUD-28's doctests push into each resolved queue (compile_fail) beside a twin that pushes into INCOMPLETE's and drains every resolved one. The control queue keeps only the order: resolution gives each datagram a turn there, which counts against its 64 frames. A turn that finds it full waits and enters as room appears, ahead of any frame queued later (OUT-08). When the turn comes, the oldest datagram in the entry's queue leaves, to the MAC the entry holds at that moment (NBR-08). An entry whose queue holds any is evicted after every other candidate, and then they are dropped asnb.pending-evictedwith their turns, and each sender is told (NUD-29). Its idle lifetime cannot end while it holds any: an entry whose lifetime passed meanwhile goes when the last one leaves, and one whose lifetime has not passed stays (NUD-29; NUD-30, for STALE and quiescent UNREACHABLE). However an entry goes, its turns and its queued requests go with it, so no item in the control queue outlives its entry. A later entry at the same address therefore sends its request from its own place in the queue (NUD-30).Ip::receive(wire.md§3.3), so the shard hands raw frames to it. The nine[shell]dispatch scenarios (ETH-10, 11, 12, 14, 19, 22–25) are therefore tested here.eth.not-for-usbeforetoyos-net-ipsees the datagram. The test checks both layers; the departure is in the track.wire.md's scenario asip.md§16.3 rules.ip.event-overflow,udp.event-overflow,dhcp.event-overflow), as intoyos-net-tcp. MOD-01, US-20 and DH-38 leave 1,025 logged refusals undrained and find 1,024 and one overflow.nb.pending-droppedis added: each held datagram dropped at FAILED or link-down is counted, beside the "host unreachable" its sender is told.dhcp.renew-unroutableis netd's, at stage 5 (udp-dhcp.md§D2.3, [shell][5]): the client never learns oftoyos-net-udp's refusal, which counts it asudp.no-route, and keeps its schedule. The track owes it.toyos-net-ipannounces the held address itself (IP-D6, C-4). DH-64 still names an announce request; the departure is in the track.limits::CLOSED_DATAGRAMS, 16); a close past it refuses the restudp.tx-discarded-on-close, since nobody is left to refuse them to. A closed socket's turn leaves with it, and the closed sockets' datagrams take one turn among the sockets rather than going first.udp-dhcp.md§U9 (3) lets every one leave; the departure is in the track.toyos-net-tcp'sconnectrefuses whattoyos_net_wire::addr::is_hostrefuses. HS-35's three addresses stay refused, and 0/8 and 127/8 remotes are refused too (MUST-46).For the spec owner
ip.control-queue-fullstays 0 until the queue drains; then the probes leave on the schedule, and only then is the address verified. It fills the queue again when a defence is due, and asserts that one defence waits.Scenario coverage
Every test names its scenario id, and every scenario has one test; a scenario with several inputs is one test that checks each.
ip.md: 215 scenarios. 189 carry[5], and 188 of those are tested. NUD-28 is covered by compile-fail doctests, each beside a twin that compiles. OUT-07 ([5][shard]) is deferred to stage 4: it needs a TCP segment and the shard's scheduler choosing between a flow andtoyos-net-ip's own frames. The 26 that carry only[9](FRA-01–20, PMTU-01–06: fragments and path MTU) belong to IP hardening. ACD-18, a proposed id, is tested too.udp-dhcp.md: 133 scenarios (US-01–57, DH-01–76), 132 tested. US-57 ([shell], the pipe ABI's mapping of refusals) is deferred to stage 5, where netd does that mapping.wire.md: all 53 scenarios tagged[ip]or[udp]are tested, as are the nine[shell]dispatch scenarios and ETH-32.Negative control: one checked mutation per rule
Each mutation is a patch. It was checked with
git apply --checkand applied; the mutated tree was shown to build (cargo test -p toyos-net-ip -p toyos-net-udp -p toyos-dhcp -p toyos-net-wire -p toyos-net-tcp --no-run, exit 0 for every one) and run with the same packages and--no-fail-fast; the patch was then reversed withgit apply -R, andgit status --porcelainwas empty after each. The baseline on 754ce5e exits 0, 1,045 passed. All 71 below were run on 754ce5e, and every one exits 101. Each goes red on the same tests as on c1474cd, and Md is new. The first run hit its time limit during R01. Its half-applied patch was reversed and the tree was checked clean. That run had also found that N23 as written no longer builds, so N23 was re-expressed. N23 and R01–R20 then ran in a second run on the same head.The final review's mutation. Md reverts this round's request change onto the head, in source only:
purge_entryisdrop_turnsagain, and NUD-30's new arm stays.removeleaves its entry's queued requestsThe round-6 review's mutations, each the review's edit on the lines as they stand:
drop_turnsis nowpurge_entry, anddrained's condition is now inleave. Mc-r reverts round 6's NOTE fix. The term comes back inleave's condition, andrequest_leaves' drop branch again deletes a drained entry.purge_entryignores a turn's interfacepurge_entryfrees is not refilledThe round-5 review's mutations, each the review's edit on the lines as they stand. N24 (
request_leavesdrops a request without deleting a drained entry) has no site left:request_leavesdeletes nothing, and Mc-r restores that deletion. N23 also adds..topurge_entry's request pattern, which its new field needs to build.removeleaves the turns; nowpurge_entrykeeps them and still drops the requeststo: Option<MacAddr>fixed itRound 6's rules. N25 is the same edit on the line as it stands.
leave's idle condition ignores the released queue, so it deletes an entry whose lifetime passed while it still holds datagramsThe round-4 review's mutations. A1 is the same edit on the line as it stands.
Round 5's rules. N16 and N18 are the same edits on the lines as they stand.
Releasedgainspushconnectadmits 0/8 and 127/8, as the deletedunicast()didThe earlier rounds' mutations, run again. Where the code they mutate moved, each is the same edit on the lines as they stand: M02r, N04r (a turn that finds the queue full is dropped, so its datagram never leaves), N05r, N07r, N08r (now: no released datagram's leaving deletes anything), R07r and R08r. R02p, R05p, R15p, M04p and M05p are round 3's re-expressions. N06 ("an evicted entry's released datagrams still leave"), N17 ("a turn names only its address") and N20 ("an eviction takes its turns out of the interface's count") have no site left: turns carry no id, an evicted entry's datagrams and turns go with it, and N21 and N22 mutate that.
dhcp.bootp-replynot loggedicmp.source-quenchnot loggeddhcp.no-subnet-masknot loggeddhcp.xid-mismatchloggedudp.no-source-addressip.acquisition-admittednot countedIp::transmitignores its creditM22 and M23 go red only in
toyos-net-tcp, which tests the shared log policy and SipHash's reference vectors for every crate that uses them.Independent oracle
toyos-net-ipandtoyos-net-udpand compared as a whole frame: DISCOVER, REQUEST, RENEW, REBIND, REBOOT and DECLINE.toyos-net-wire's.toyos-net-wire.Gates, on 754ce5e
cargo run -- --ci host: EXIT=0 (55 steps, all green, workspace clippy with warnings denied among them).cargo test -p toyos-net-ip -p toyos-net-udp -p toyos-dhcp -p toyos-net-wire -p toyos-net-tcp --no-fail-fast: EXIT=0, 1,045 passed.cargo clippy -p toyos-net-ip -p toyos-net-udp -p toyos-dhcp --all-targets -- -D warnings: EXIT=0.Clean room
No other stack's source was opened while writing this branch. That includes smoltcp, lwIP, netstack3, FreeBSD, Linux and netd's glue beyond the pipe ABI.
🤖 Generated with Claude Code
https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L