The guest gets a network, not a verb
Superseded by one-connect-door-not-four-gates, which keeps this page’s core claim — the guest reaches the network itself rather than a bespoke per-protocol verb — but replaces the architecture named below as what makes that safe. DNS answered locally, TCP accepted only for a minted address, and an intercepting proxy on 80/443 are gone; one explicit CONNECT-only proxy at an exact-hostname allowlist, pinned to a single host-side resolution, replaces all three. Read the new page for what is enforced now; this page’s four-gate account of why the network is safe to expose no longer holds.
A userland as rich as synod’s §7 is half wasted if nothing in it can
reach anything, so the guest gets a real interface — a tun whose only peer
is a user-mode TCP/IP stack in a host process we wrote — and the single
fetch-url verb that used to be the entire egress surface is retired
outright, not deprecated beside it. Every protocol a package manager, a
build tool, or a document workflow actually speaks becomes reachable the way
it is reached everywhere else — DNS, TCP, TLS — rather than a bespoke host
enquiry per tool (apt, then pip, then npm, then git, then crates.io, each a
smaller worse version of the network it stands in for). What makes this safe
to ship broadly is egress’s four gates, terminating every
connection host-side before deciding whether it goes anywhere: DNS answered
locally, TCP accepted only for an address DNS itself minted, 80/443
intercepted so a host grant reads as a verb grant, everything else refused by
the absence of a listener.
Rejected alternatives
- No network device, one verb per protocol. The design this decision
replaces. Coherent on paper and does not scale: every new protocol a real
toolchain needs (
git,pip,npm,apt,crates.io, …) becomes a new host-side enquiry, hand-written and re-reviewed, forever behind whatever the guest’s own userland can already speak natively. - The macOS-only hypervisor frame tap.
VZFileHandleNetworkDeviceAttachmentdelivers Ethernet frames over a datagram socket for free, no entitlement, no guest cooperation — but Windows has no equivalent this cheap, and a design that runs one way on one platform and a structurally different way on the other is two designs wearing one name. Rejected for the same reasonsynodis one guest with two backends everywhere else: the guest must not be able to tell which hypervisor booted it. - The HDV device model on Windows (
HdvInitializeDeviceHost, aFlexibleIovPCI device slot the guest enumerates as a real NIC) — declined on theHdvCreateGuestMemoryApertureseam, not on availability. The device model itself is free (OpenVMM’s MITvirtio/virtio_net), and at least one third party has already driven it from HDV — the door is open. What is ours to write is the seam: a device model earns its keep by following physical addresses the guest authored into a shared mapping, so implementing guest memory overHdvCreateGuestMemoryApertureis the most adversary-facing code the design could contain, the platform’s own aperture semantics are reported incoherent with no invalidation notice to subscribe to, and a byte-stream parser in the pump’s host side is a different, smaller class of exposure that no amount of good dependency code changes. CONNECTwithout interception. Answer the tunnel request, dial the named host, never look inside. Loses precisely becausesynodships to arbitrary machines and makes a security claim on all of them: there is no IT department downstream curating a tight per-site allowlist, so what ships must be broad enough to be useful on day one — and a broad allowlist checked only at hostname depth is barely a boundary, since every allowed host that accepts writes is a full-bandwidth way out (allowinggithub.comwould allow pushing the granted folder to it). Interception is what turns a broad default into a verb grant instead of a host grant.- Per-domain consent cards. Worse than nothing for the non-technical user
synodactually ships to: a person is trained to click “always allow” and is left with a control that only looks like one. A blocked request instead fails once, in plain English, naming the host.
Consequences
- Host-mode exarch loses its web door.
fetch-urlwas reachable from a bare exarch session with no guest underneath it; the four gates described in egress exist only insideguest-net, so a host-mode session that wants the network now needs a guest to run it through. Search is unaffected —searchstays a policy bit onNetPolicy, clamping the provider’s own hosted web search, which never touchedfetch-url’s machinery to begin with. - Caps were resized, not just renamed.
fetch-url’s defaults were sized for one enquiry at a time; a guest with a real network runs a real install, many requests in a burst, somax-bytesandrate-per-minuteboth moved up rather than surviving as the old verb’s numbers under a new name. - The ledger records the method.
exarch::egress::AuditLog’sRecordgrew a discriminant per gate (Name,Connect,Request) instead of one shape for every access, because the three gates check different things at different moments and collapsing them into one record shape was how the earlier design’s DNS/accept split got lost. git cloneis off the shipped list. The defaultreadallowlist grantsGET/HEADonly, and cloning over HTTPS needs aPOSTtogit-upload-packeven for a read-only fetch — so it is refused by the shipped default’s emptywritelist until a fleet’s own policy names it, the same way every other write-shaped protocol is.- The jargon guard moved. The plain-English check that used to guard
fetch-url’s refusal text (fleet::desk’sFETCH_URL_JARGON) is retired with the verb it guarded and rebuilt besideguest_net::refusal::RefusalasREFUSAL_JARGON, checked against every refusal variant exhaustively rather than against strings a socket had to be opened to produce.
Open questions
- The jail-vs-install collision. §5’s fresh-UID spawn jail means a
model-spawned
aptfails on privilege inside the guest, sopip3 --useris the one install path the network’s allowlist actually admits —curlrides beside it for whateverpip3cannot reach directly. Whether a networked guest ever wants a real package-manager install path, and what that would need from the jail, is recorded and not resolved. - The rate cap is a guess.
rate-per-minute: 240is sized off onepip install, not a measurement. A number to move once real traffic is observed, not a boundary anyone has tested against. - Path depth.
AuditLog’sRequestrecord carries a URL whole, query string included — the honest half of egress’s residual. Whether a review surface built on the ledger should ever truncate a logged path for readability, and how much of one that could do before the audit stops being trustworthy evidence of what actually crossed, is open. - Request headers relay by a named allowlist.
guest_net::proxyfirst shipped keeping onlyHostandContent-Lengthfrom the guest’s request, which broke real traffic silently — noRangeforaptto resume a partial download, noAcceptfor PyPI’s JSON API, noUser-Agentfor hosts that 403 a client with none. Resolved: a named allowlist (Accept,Accept-Encoding,Accept-Language,Range, theIf-*conditionals,User-Agent,Authorization,Content-Type) is relayed upstream, matched case-insensitively; every hop-by-hop header and everything else unnamed is dropped. The same egress residual that already covers the query string covers this list too — see its “honest residual” section. - The upstream client has no idle-read timeout to bound a stall.
reqwest’s blocking client exposestimeout(a total-request cap, incompatible with a legitimately slow multi-hundred-megabyte wheel) andconnect_timeout, but noread_timeout— there is no knob for “abandon a connection that stopped sending bytes but never closed.” What bounds a stalled transfer today is the connect timeout plusNetPolicy’smax-bytescap, which stops a slow-but-finite response but not a peer that opens a connection and then goes silent forever without closing it. Whether that gap is worth a hand-rolled read-progress watchdog is not yet decided.
Standing beside this decision
The engine protocol’s enquiry channel still carries the
agent, schedule, and reply families, none of which this decision touches:
losing one enquiry class (fetch-url) is not a supersession of the channel
that carried it.