Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion issues/toyos-has-its-own-network-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ netd runs smoltcp. Its replacement is ToyOS's own stack, built clean-room: reade
- Then multi-core, netring (blocked on the owner's ABI ruling), TCP and IP hardening, IPv6, offloads, soak.
- This track owns the T14's outbound rows, the machine reaching its router and the internet on the I219, judged from the stick: they arrive on this stack and not on smoltcp (owner: "no smoltcp."), and until they do no T14 row reads the wired card (`issues/the-host-cannot-reach-the-t14-while-it-runs-toyos.md`).

Stage 5 lands in slices, the orchestrator's cut under the owner's words "i want smoltcp out as fast as possible": netd's decisions go into `toyos-net-node` (`userland/netstack/node`), pure and host-tested and shipped in nothing, and then one change moves netd onto it and deletes smoltcp. In the tree: the node, its DHCP lease, its clients' datagram sockets and its mDNS name. Still to build on it: the resolver, streams, listeners; then the move.
Stage 5 lands in slices, the orchestrator's cut under the owner's words "i want smoltcp out as fast as possible": netd's decisions go into `toyos-net-node` (`userland/netstack/node`), pure and host-tested and shipped in nothing, and then one change moves netd onto it and deletes smoltcp. In the tree: the node, its DHCP lease, its clients' datagram sockets, its mDNS name and its resolver. Still to build on it: streams, listeners; then the move.

What the node does not yet meet:

Expand All @@ -30,6 +30,10 @@ What the node does not yet meet:
- The machine's name is told to the node twice, as `toyos_dhcp::HostName` in `Node::new` and as `toyos_mdns::Host` in `Node::answer_as`: the first owns its bytes and the second borrows them. Exit: one type carries the label to both; due at the move if handing `answer_as` its `&'static str` takes a leak.
- The node's name is not probed for or defended (RFC 6762 §8.1, §9), as `toyos-mdns` records of itself. It is not announced again when the link comes back, which §8.3 asks and netd does today, where link-up resets its DHCP client and the address is new again: here the lease outlives the link. Exit: `toyos-mdns` does both, on the node's `link`.
- An answer to a query from a 169.254/16 source leaves in a frame to the router: `toyos-mdns` takes a link-local source for one on the link, and [ip]'s route lookup has no way to 169.254/16 on a leased interface but the gateway, where RFC 3927 §2.6.2 sends it on the link and §7 never to a router. `an_answer_to_a_link_local_asker_leaves_by_the_router` holds the present state. Exit: [ip] sends to 169.254/16 on the link, and that test finds the answer in a frame to the asker's own link address.
- A query that does not reach its resolver is waited out for `toyos_dns::WAIT_MS` like one nobody answered, whether [udp] refused it for want of a route (`node.query-unsent`) or the network reported it back, its resolver unreachable or its port refused (`node.query-failed`): `toyos_dns::Lookup` has no word for either. Exit, before the move: `Lookup` is told and asks the next resolver at once, `a_query_udp_refuses_is_counted_holds_no_port_and_is_waited_out` finds its answer at 0 ms, and `a_query_the_network_reports_unreachable_is_counted_and_waited_out` finds its lookup ended at 3 s, [ip]'s first report of its one resolver, and not at `ROUNDS` × `WAIT_MS`.
- Lookups started together at a resolver whose link address is not yet known lose all but the newest `PENDING_PER_NEIGHBOUR` queries to [ip]'s queue for an unresolved next hop (`nb.pending-overflow`, RFC 4861 §7.2.2), and each lost one costs its lookup a `WAIT_MS`; smoltcp kept each query in its own socket until the neighbour answered. `a_burst_at_a_resolver_not_yet_resolved_leaves_ips_queue_of_queries_and_the_rest_a_wait_later` holds it as it stands. It is a regression against what ships and would ship at the move. Exit, before the move: that test finds every query of the burst on the wire once the resolver answers ARP.
- `toyos_dns::Lookup::on_datagram` still takes a reply's source address and port, which the node's connected sockets have already matched and the node hands it from its own record of the query: the two parameters are read only by netd's resolver on smoltcp. Exit, at the move: they go with that resolver.
- No test of `toyos-net-ip` holds that a next hop which never answers ARP holds up no datagram to another, which the node's resolver rests on when the lease's first resolver is down (`issues/netstack-resolver-asks-a-dead-primary-first-on-every-lookup.md`, its second cost). Exit, before the move: a scenario in `toyos-net-ip/tests/nud.rs` sends to a silent neighbour and to an answering one and finds the second datagram on the wire at once.
- `node.address-refused` has no test: `toyos-dhcp` accepts no address or prefix [ip] refuses, by the same `toyos-net-wire` checks in both, so the refusal cannot be reached from the wire. Exit: the client hands [ip] a type that carries the check, and the counter goes.

The listener defects are this track's: `issues/a-handshake-nobody-finishes-holds-a-listeners-port-shut.md` and `issues/a-connect-between-two-accepts-is-reset.md`, on smoltcp until stage 5, and `issues/an-accept-that-never-reaches-netstack-strands-its-listener.md`, in std's accept.
Expand Down
31 changes: 21 additions & 10 deletions toyos-dns/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! IPv4 addresses, which datagram answers that question, what the answer says,
//! and when the question is asked again. Pure: `core` and `alloc`, no
//! `unsafe`, no I/O, and no clock or randomness of its own. The caller hands in
//! the time, every query ID and every datagram with its source.
//! the time, the draw of every query's ID and every datagram with its source.
//!
//! **A reply has crossed a trust boundary.** Anyone on the path can send a
//! datagram to the port a query left from, so a reply is read only if it
Expand Down Expand Up @@ -471,8 +471,10 @@ struct Sent {

/// One name's lookup, from its first query to its answer.
///
/// **Each query carries an ID the caller drew for it**, which the caller takes
/// from a source an off-path sender cannot predict (RFC 5452 §9.2). A reply to
/// **Each query carries an ID the caller draws for it**, from a source an
/// off-path sender cannot predict (RFC 5452 §9.2). Every call that may send a
/// query takes the draw, and calls it once for a query it sends and not at all
/// otherwise. A reply to
/// 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.
Expand All @@ -499,8 +501,8 @@ pub struct Lookup {

impl Lookup {
/// A lookup of `name` through `servers`, and its first query, sent at
/// `now` with ID `id`. `None` when there is no server to ask.
pub fn start(name: Name, servers: &[[u8; 4]], now: u64, id: u16) -> Option<(Self, Step)> {
/// `now` with the ID `id` draws. `None` when there is no server to ask.
pub fn start(name: Name, servers: &[[u8; 4]], now: u64, id: impl FnOnce() -> u16) -> Option<(Self, Step)> {
if servers.is_empty() {
return None;
}
Expand Down Expand Up @@ -530,7 +532,7 @@ impl Lookup {
}

/// The next query for `name`, or the end where every one has been sent.
fn ask(&mut self, now: u64, id: u16) -> Step {
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),
Expand All @@ -541,23 +543,32 @@ impl Lookup {
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
/// is due. `id` is for the query this may send.
pub fn on_time(&mut self, now: u64, id: u16) -> Step {
/// is due. `id` draws the ID of the query this sends.
pub fn on_time(&mut self, now: u64, id: impl FnOnce() -> u16) -> Step {
if now < self.due {
return Step::Wait;
}
self.ask(now, id)
}

/// `msg` arrived at `now` from `port` of `from`, on the port `asked` was
/// sent from. `id` is for the query this may send.
pub fn on_datagram(&mut self, asked: Asked, from: [u8; 4], port: u16, msg: &[u8], now: u64, id: u16) -> Step {
/// sent from. `id` draws the ID of the query this sends.
pub fn on_datagram(
&mut self,
asked: Asked,
from: [u8; 4],
port: u16,
msg: &[u8],
now: u64,
id: impl FnOnce() -> u16,
) -> Step {
let Some(carried) = msg.get(..2).map(|b| u16::from_be_bytes([b[0], b[1]])) else {
return Step::Wait;
};
Expand Down
Loading
Loading