youros

How it worksUnderneath

Sandbox

The sandbox is a service on a node that makes boxes and is the only way anything gets one. A box is a set of Linux namespaces (user, mount, pid, network, ipc, uts), a seccomp filter and a cgroup, made by the node as the user it runs as. The node holds the box's network end, so it is the gate, and writes the audit. A browser profile (Browsing) and an extension instance are both boxes; nothing else in the node makes one.

The box

The user namespace is what lets an ordinary user make the rest, and the node maps only its own uid into it. The mount namespace starts empty: a read-only image for the box's programs, a folder of its own, and whatever folders it was granted, bound in. Nothing of the node's state is mounted, so its key isn't hidden from the box, it is absent. Seccomp refuses what no box needs (loading modules, ptrace outside the box, new keyrings, bpf). Limits come from a cgroup of the node's own, which systemd delegates to a user service; where there is no systemd, limits are what setrlimit gives, and the node says so.

A browser box also gets a display of its own and the GPU's render node, and keeps Chrome's own sandbox on inside it (the prototype measured this: namespaces and seccomp-BPF report "adequately sandboxed" in a box, and WebGL runs on the real GPU). A box's programs arrive like the node's own: from /dl/, checked against hashes in the signed release, never from a package manager, which would need root.

The gate

A box's network namespace has loopback and one TUN device, and the node holds the other end of that device. Everything the box sends is a packet handed to the node, which runs a user-space TCP/IP stack on it (the same idea as slirp and pasta, in our code so the node decides each flow).

  • Each new flow is checked against the box's reach, by the address, after DNS, at the moment of the dial. Reach is a short vocabulary: internet (any public address), local (the node and its own networks), named hosts, or nothing. A flow that is allowed is dialled by the node as an ordinary socket; one that isn't is refused, and the box sees a connection refused, not a timeout.
  • DNS is the gate's. The box's resolver is the node, so every name a box asked for is known, and a flow is audited by the name that led to it as well as its address. A box that dials an address it never looked up is audited as such.
  • UDP is a flow like TCP, so a browser's WebRTC reaches STUN and TURN on the internet and not a printer on the LAN, which is what the prototype's HTTP proxy could not do.
  • Nothing is decrypted. The gate sees addresses, names, ports, sizes and timing, never content; a TLS connection's name comes from DNS and its SNI. Seeing inside would mean a CA of ours trusted by the box, which is what an inspection box does and what browser.md treats as an attack.

The audit is a doc per box, written by the node (D-22: device state, on the node that ran it), one entry per flow and per refusal, kept for a window and thinned after it. It syncs sealed to the account's devices like any doc (sandbox/audit-reaches-you), and the shell shows it beside the pane or extension it belongs to.

Raising, not only logging

Rules, not a model: refused dials past a rate, any flow to the node's own networks from a box allowed only the internet, a new host for an extension whose manifest names its hosts, a box sending far more than it receives, connections at a fixed interval to one host. Each is a notice to the account's devices with the entries that raised it, and the person can stop the box from the notice. A rule is cheap to add once the audit exists; the audit is the part that has to be right first.

Extensions in boxes

An extension is a service (D-4) nobody vouches for. Its manifest names the folders, hosts and verbs it needs; installing it is a grant of those (D-12), shown to a person, and the box is made from the grant, so what the manifest says and what the box allows are the same list. Each use of an extension that is given different folders is an instance, a box of its own. A small extension that only computes is better as a WASM component (D-4's "later"): it has no system calls to filter, and every file and socket it opens is a call into the node, so the box costs nothing. A box is for what must be native, a browser first.

The rest of the node

A box is watched because the node made it and holds its network. The rest of a node (your own programs, a daemon you installed) isn't, and watching it is a different job with a different price:

  • Without root, a node can see but not stop. /proc/net/tcp, tcp6, udp and udp6 list every socket on the machine with its owner's uid, readable by anyone, and the process behind each is known for the node's own user. Read every second, that is a list of who talks to whom that misses short connections and blocks nothing. It is enough for a notice: "something of yours is listening on a port the internet can reach".
  • Stopping, or seeing every flow, needs root: an nftables table or an eBPF program on the cgroups, which only root or CAP_NET_ADMIN on the host can load. That is a separate helper, installed with sudo by someone who asked for it, doing only that and speaking to the node over a socket; never part of the one line.
  • A watcher on a node is blind to a node that has fallen. Root malware edits what the watcher reads. The view that can't be lied to is from outside: a node that is the network's router or resolver sees the others' traffic. That is later, and needs no root on the machines it watches.

Where it breaks:

  • Ubuntu restricts unprivileged user namespaces. Since 23.10, AppArmor lets an unconfined program make one but takes its capabilities inside, unless the program has a profile allowing it, and adding a profile needs root. The prototype's boxes were made by a daemon running as root, so this is unmeasured for ours. If it holds, the choices are a box made by a program that already has a profile (bwrap where installed), a one-time privileged step, or no browser box on that machine, and the node says which.
  • A user-space network stack costs. Every packet of a busy browser crosses it; pasta reaches gigabits, a stack of ours has to be measured against a video call before it carries one.
  • Containers inside boxes. An extension that wants Docker can't have it: the daemon is root.
  • A kernel bug is a way out. Namespaces and seccomp share the node's kernel; a box is as strong as that kernel. A microVM (Firecracker, gVisor) is stronger and needs /dev/kvm or more of a runtime; later, for nodes that have it.