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.

8 changes: 7 additions & 1 deletion issues/toyos-has-its-own-network-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@ netd runs smoltcp. Its replacement is ToyOS's own stack, built clean-room: reade
- Stage 5: netd on one shard, pipe ABI unchanged; smoltcp leaves netd, `Cargo.toml` and `Cargo.lock` in the same PR.
- Then multi-core, netring (blocked on the owner's ABI ruling), TCP and IP hardening, IPv6, offloads, soak.

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 and its DHCP lease. Still to build on it: datagram sockets and mDNS, 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 and its mDNS name. Still to build on it: the resolver, streams, listeners; then the move.

What the node does not yet meet:

- Of what a third party wrote, only slirp's recorded OFFER and ACK have reached it (`userland/netstack/node/tests/slirp.rs`); every other frame it has answered is the tests' own, from the RFCs' layouts. Exit: netd runs on it against slirp in a guest and a router on the T14.
- It counts a DHCP message [udp] refused as `node.dhcp-unsent`, whatever the rule; the `dhcp.renew-unroutable` scenario owed above is not written. Exit: that scenario names the counter, or the node counts the renewal apart.
- With the link down the DHCP client keeps its timers: every 4 to 64 s the node has a deadline, builds a DISCOVER that [udp] refuses, and counts it in `dhcp.tx.discover` and `node.dhcp-unsent`, from `Node::new` on. Exit: `toyos-dhcp`'s client is told the link went down and waits for it, and the node's first DISCOVER is the one that leaves.
- The word each of [udp]'s refusals is answered in on the pipe ABI is `Refused` in `userland/netstack/node/src/datagram.rs`, tested by `each_refusal_is_answered_in_the_pipes_word_for_it` against no reader's scenario; `udp.no-ephemeral-port` has a word and no test that reaches it. Exit: the scenario owed below exists, and the test names its id.
- A client's datagram to a broadcast address is refused `udp.broadcast-not-permitted` and answered invalid input, where smoltcp sends it: the pipe ABI has no request that permits it, and std's `set_broadcast` (`sdk/std/sys/net/connection.rs`) answers `Ok` to a permission nothing grants. No program in the tree sends to a broadcast address or sets the option; the sources of the third-party C programs an image builds from a recipe have not been searched. The owner's ruling: "Of course carry the permission why would you even ask. The premise is and always has been to do things properly". The move of netd onto the node does not land with this open. Exit, both before the move, as a stage of their own: that search, for `SO_BROADCAST` and a `sendto` to a broadcast address, is posted with its command and output; and the pipe ABI carries the permission from std's and libc's setters to `Udp::set_broadcast`, a send refused for want of it answered in a word that says permission and not input.
- A refusal [udp] logs against a client's call waits in the stack until the node's next `receive`, `fire` or `link`, because only those pass over the stack's reports. Exit, at the move and not after it: the datagram calls end in that pass, or netd drains it once per turn of its loop.
- 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.
- `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: 30 additions & 1 deletion toyos-net-shard/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ use toyos_net_tcp::{ConnId, Endpoint, Hop, IcmpError, IcmpKind, ListenerId, Outg
use toyos_net_udp::{Sender, SocketId, Udp, Verdict};
use toyos_net_udp::Served as UdpServed;
use toyos_net_wire::ethernet::{FrameBuilder, IndividualMac, MacAddr};
use toyos_net_wire::ipv4::{Ipv4Builder, Ipv4Source, TrafficClass, Ttl};
use toyos_net_wire::ipv4::{Ipv4Builder, Ipv4Source, MulticastAddr, TrafficClass, Ttl};
use toyos_net_wire::siphash::Key;
use toyos_net_wire::{Instant, Port};

Expand Down Expand Up @@ -218,6 +218,10 @@ impl Shard {
self.tcp.counters()
}

pub fn udp_counters(&self) -> &toyos_net_udp::Counters {
self.udp.counters()
}

/// Log lines and address results since the last call.
pub fn drain_events(&mut self) -> alloc::vec::Drain<'_, Event> {
self.events.drain(..)
Expand Down Expand Up @@ -444,6 +448,13 @@ impl Shard {
removed
}

/// Joins `group` on the interface: frames to its link address are taken from here on, and
/// [ip] reports the membership whenever the link can carry the report. Nothing to settle:
/// a join makes no flow eligible and no event.
pub fn join(&mut self, now: Instant, group: MulticastAddr) -> Result<(), toyos_net_ip::Counter> {
self.ip.join(now, self.iface, group)
}

// ---- TCP ----

/// An active open from the source [ip]'s route lookup picks (RFC 9293 MUST-44), to a peer the
Expand Down Expand Up @@ -529,6 +540,24 @@ impl Shard {
self.udp.recv(id, out)
}

/// The port a socket holds.
pub fn udp_port(&mut self, id: SocketId) -> Result<Port, toyos_net_udp::Error> {
self.udp.binding(id).map(|(_, port)| port)
}

/// The TTL of a socket's datagrams to one host, and of those to a group.
pub fn udp_set_ttl(&mut self, id: SocketId, unicast: Ttl, multicast: Ttl) -> Result<(), toyos_net_udp::Error> {
self.udp.set_ttl(id, unicast, multicast)
}

/// Closes a socket: its port is free at once, and what it had accepted leaves as [udp]'s
/// closed sender, in the round from this call.
pub fn udp_close(&mut self, now: Instant, id: SocketId) -> Result<(), toyos_net_udp::Error> {
let closed = self.udp.close(id);
self.settle(now);
closed
}

/// The DHCP client's socket, on port 68: the one socket that may send from 0.0.0.0 and name
/// its source, and the one the acquisition exception delivers to (RFC 2131 §4.1).
pub fn acquisition(&mut self) -> Result<SocketId, toyos_net_udp::Error> {
Expand Down
3 changes: 2 additions & 1 deletion userland/netstack/node/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "toyos-net-node"
description = "Netstack's decisions, pure: one interface of ToyOS's own stack and its DHCP lease, frames and time in, frames out against the device's credit."
description = "Netstack's decisions, pure: one interface of ToyOS's own stack, its DHCP lease, its clients' datagram sockets and its mDNS name, frames and time in, frames out against the device's credit."
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
Expand All @@ -11,6 +11,7 @@ doctest = false

[dependencies]
toyos-dhcp = { path = "../../../toyos-dhcp" }
toyos-mdns = { path = "../mdns" }
toyos-net-ip = { path = "../../../toyos-net-ip" }
toyos-net-shard = { path = "../../../toyos-net-shard" }
toyos-net-tcp = { path = "../../../toyos-net-shard/tcp" }
Expand Down
122 changes: 122 additions & 0 deletions userland/netstack/node/src/datagram.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//! A client's datagram sockets on [udp]: bind, send, receive and close. A socket is named by a
//! [`DatagramId`], which only [`Node::udp_bind`] makes, so no call here names the DHCP client's
//! socket or the responder's; their ports are taken like any other.
//!
//! **Refusals.** [udp] refuses by rule and counts each under its own name. A client is answered
//! in one of the pipe ABI's words ([`Refused`]): the shell writes the word's code and decides
//! nothing. A destination, a port or a length is the client's number and is refused, never
//! trusted; a datagram off the wire reaches a socket only through [udp]'s demultiplexer.
//!
//! A refusal [udp] logs comes out of [`Node::drain_events`] after the node's next `receive`,
//! `fire` or `link`.

use core::net::Ipv4Addr;

use toyos_net_udp::{Counter, Error, SocketId};
use toyos_net_wire::{Instant, Port};

use crate::Node;

/// One client socket. An id outlives its socket only as a refusal: [udp] never hands a closed
/// socket's id to another.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DatagramId(SocketId);

/// Why a call was refused, in the pipe ABI's words (`toyos::net`'s `ERR_*`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Refused {
/// The port is another socket's, or no ephemeral port is free.
AddrInUse,
/// The id names no socket, or the machine has no address or route to send from.
NotConnected,
/// The call names what no datagram of this socket may carry: an address that is not this
/// machine's to bind, a destination nothing is sent to, port 0, more than one frame holds.
InvalidInput,
/// The socket holds all it may until some of it has left: the same call succeeds later.
ResourceExhausted,
}

/// A datagram [`Node::udp_recv_from`] handed over.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Datagram {
/// Bytes written: the payload, cut to the buffer.
pub len: usize,
pub source: Ipv4Addr,
/// Absent when the peer sent port 0: it cannot be answered.
pub source_port: Option<Port>,
}

/// [udp]'s refusal as the client hears it. A client's socket is bound and never connected, and
/// never names its source, so the rules of those calls, and the counters that are no refusal,
/// are not its to meet.
fn refused(error: Error) -> Refused {
let rule = match error {
Error::NoSuchSocket => return Refused::NotConnected,
Error::Refused(rule) => rule,
Error::Failed(_) => unreachable!("[udp] reported a network error against a socket that is not connected"),
};
match rule {
Counter::PortInUse | Counter::NoEphemeralPort => Refused::AddrInUse,
Counter::NoSourceAddress | Counter::NoRoute | Counter::SourceAddressNotAssigned => Refused::NotConnected,
Counter::BindAddressNotLocal
| Counter::SendLoopback
| Counter::SendToSelf
| Counter::SendInvalidDestination
| Counter::SendPortZero
| Counter::SendUnspecifiedDestination
| Counter::BroadcastNotPermitted
| Counter::ExceedsMtu => Refused::InvalidInput,
Counter::TxQueueFull => Refused::ResourceExhausted,
Counter::ConnectUnspecified
| Counter::ConnectGroup
| Counter::ConnectPortZero
| Counter::ConnectedDestinationMismatch
| Counter::NotConnected
| Counter::SourceNotPermitted
| Counter::Rx
| Counter::RxDelivered
| Counter::RxNoSocket
| Counter::RxNoSocketGroup
| Counter::RxQueueFull
| Counter::RxNoChecksum
| Counter::RxSrcPortZero
| Counter::RxTruncated
| Counter::RxDroppedOnConnect
| Counter::RxDiscardedOnClose
| Counter::TxDiscardedOnClose
| Counter::Tx
| Counter::IcmpErrorDelivered
| Counter::IcmpErrorSoft
| Counter::IcmpErrorUnconnected
| Counter::IcmpErrorNoSocket
| Counter::EventOverflow => unreachable!("[udp] refused a client's bind or send as {}", rule.name()),
}
}

impl Node {
/// Binds a socket to `addr`, 0.0.0.0 meaning every address the machine holds, and to `port`
/// or, with none named, an ephemeral one, which spends the one draw. Returns the socket and
/// the port it holds.
pub fn udp_bind(&mut self, addr: Ipv4Addr, port: Option<Port>, draw: impl FnOnce() -> u32) -> Result<(DatagramId, Port), Refused> {
let (id, port) = self.stack.bind(addr, port, draw).map_err(refused)?;
Ok((DatagramId(id), port))
}

/// Queues `payload` for `destination:port`; it leaves in a later [`Node::transmit`].
pub fn udp_send_to(&mut self, now: Instant, id: DatagramId, destination: Ipv4Addr, port: u16, payload: &[u8]) -> Result<(), Refused> {
self.stack.send_to(now, id.0, destination, port, payload).map_err(refused)
}

/// The oldest datagram that reached the socket, in `out`, or `None` when none waits. One
/// longer than `out` is cut to it and the rest is gone, as [udp] counts.
pub fn udp_recv_from(&mut self, id: DatagramId, out: &mut [u8]) -> Result<Option<Datagram>, Refused> {
let received = self.stack.recv_from(id.0, out).map_err(refused)?;
Ok(received.map(|received| Datagram { len: received.len, source: received.source, source_port: received.source_port }))
}

/// Closes the socket: its port is free at once, what it received is gone, and what it had
/// accepted still leaves, to [udp]'s bound.
pub fn udp_close(&mut self, now: Instant, id: DatagramId) -> Result<(), Refused> {
self.stack.close(now, id.0).map_err(refused)
}
}
35 changes: 33 additions & 2 deletions userland/netstack/node/src/lease.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ use core::net::Ipv4Addr;

use toyos_dhcp::{Lease, Transmission};
use toyos_net_shard::{Config, Event, Refusal, Shard};
use toyos_net_udp::SocketId;
use toyos_net_udp::{Received, SocketId};
use toyos_net_wire::ethernet::MacAddr;
use toyos_net_wire::Instant;
use toyos_net_wire::ipv4::{MulticastAddr, Ttl};
use toyos_net_wire::{Instant, Port};

/// The port a DHCP server listens on (RFC 2131 §4.1).
const SERVER_PORT: u16 = 67;
Expand Down Expand Up @@ -130,6 +131,36 @@ impl Stack {
Some((out.get(..received.len)?, received.source))
}

// ---- datagram sockets: [udp]'s calls passed on, none of which writes the lease ----

/// Binds a socket to `addr`, 0.0.0.0 meaning any, and to `port` or one `draw` picks. Returns
/// it and the port it holds.
pub(crate) fn bind(&mut self, addr: Ipv4Addr, port: Option<Port>, draw: impl FnOnce() -> u32) -> Result<(SocketId, Port), toyos_net_udp::Error> {
let id = self.shard.bind(addr, port, draw)?;
Ok((id, self.shard.udp_port(id)?))
}

/// Queues a datagram; accepted means queued, not sent.
pub(crate) fn send_to(&mut self, now: Instant, id: SocketId, destination: Ipv4Addr, port: u16, payload: &[u8]) -> Result<(), toyos_net_udp::Error> {
self.shard.send_to(now, id, destination, port, payload)
}

pub(crate) fn recv_from(&mut self, id: SocketId, out: &mut [u8]) -> Result<Option<Received>, toyos_net_udp::Error> {
self.shard.recv_from(id, out)
}

pub(crate) fn close(&mut self, now: Instant, id: SocketId) -> Result<(), toyos_net_udp::Error> {
self.shard.udp_close(now, id)
}

pub(crate) fn set_ttl(&mut self, id: SocketId, unicast: Ttl, multicast: Ttl) -> Result<(), toyos_net_udp::Error> {
self.shard.udp_set_ttl(id, unicast, multicast)
}

pub(crate) fn join(&mut self, now: Instant, group: MulticastAddr) {
agreed(self.shard.join(now, group));
}

// ---- the lease ----

/// The shard's next event, read against the state.
Expand Down
19 changes: 16 additions & 3 deletions userland/netstack/node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
//! interface holds is `lease`'s to write and nobody else's: see that module for the three rules
//! its types keep.
//!
//! A client's datagram sockets are `datagram`'s and the machine's `<host>.local` name is `name`'s:
//! both read the lease and write none of it.
//!
//! **Untrusted input.** A received frame is never read here: every byte goes through
//! `toyos-net-wire`'s parsers inside the shard, and a DHCP payload through the client's. What
//! either refuses is counted where it was refused and, where it is a log line, comes out of
Expand All @@ -29,7 +32,9 @@

extern crate alloc;

mod datagram;
mod lease;
mod name;

use alloc::vec;
use alloc::vec::Vec;
Expand All @@ -38,12 +43,14 @@ use toyos_dhcp::{AddressRequest, Client, HostName, Lease, Output};
use toyos_net_shard::{Config, Shard};
use toyos_net_wire::Instant;

pub use datagram::{Datagram, DatagramId, Refused};
use lease::{Report, Stack, Verified};

toyos_net_wire::counters! {
DhcpUnsent = "node.dhcp-unsent";
AddressRefused = "node.address-refused";
RouterRefused = "node.router-refused";
NameUnsent = "node.name-unsent";
}

/// A line for the log.
Expand All @@ -57,6 +64,8 @@ pub enum Event {
pub struct Node {
stack: Stack,
client: Client,
/// The responder for the machine's name, once [`Self::answer_as`] started it.
name: Option<name::Name>,
counters: Counters,
events: Vec<Event>,
/// Room for the largest message the client accepts.
Expand All @@ -73,6 +82,7 @@ impl Node {
Ok(Self {
stack,
client,
name: None,
counters: Counters::default(),
events: Vec::new(),
datagram: vec![0; usize::from(toyos_dhcp::limits::MAX_MESSAGE)],
Expand Down Expand Up @@ -130,7 +140,8 @@ impl Node {
// ---- the clock ----

pub fn next_deadline(&self) -> Option<Instant> {
self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).min()
let name = self.name.as_ref().and_then(name::Name::next_deadline);
self.stack.next_deadline().into_iter().chain(self.client.next_deadline()).chain(name).min()
}

/// Every deadline at or before `now`; the frames they make due wait for [`Self::transmit`].
Expand All @@ -143,7 +154,8 @@ impl Node {
}

/// Hands the client what the shard reported and what reached its socket, and carries out
/// what it answers, until neither has more.
/// what it answers, until neither has more; then the name is served, against the lease as
/// that left it.
fn settle(&mut self, now: Instant, draw: &mut impl FnMut() -> u32) {
loop {
let (out, verified) = if let Some(report) = self.stack.report() {
Expand All @@ -159,10 +171,11 @@ impl Node {
} else if let Some((payload, from)) = self.stack.recv(&mut self.datagram) {
(self.client.receive(now, payload, from, &mut *draw), None)
} else {
return;
break;
};
self.carry_out(now, out, verified, draw);
}
self.serve_name(now);
}

/// What one call of the client's asked for: the lease first, so a message leaves from the
Expand Down
Loading
Loading