WordPress put objstore.public_url, the clearnet api.seaweedfs.s3 host, into S3_UPLOADS_BUCKET_URL, so the onion pass of WordPress#0 (CI run 40979252521) loaded the site logo from clearnet and the leak spec failed; the /objstore rebase only fired on onion-only consumers and never on a multi-network node. lookup('objstore', app, 'relative_url') now returns /_infinito/objstore/<bucket> for a shared consumer with a public bucket and an API domain, and locations.conf.j2 serves that path on every vhost of the consumer, clearnet and onion alike, by proxying over the loopback to the provider's own vhost.
The route passes GET and HEAD only, answers 404 for a path ending in / (no ListObjects), drops the query string (no S3 subresources or presigned queries) and the Authorization and Cookie headers, so only the public bucket's anonymous read applies, and adds nosniff plus a sandbox CSP; private and embedded buckets get no route. Only web-app-wordpress switches to relative_url: mastodon, pixelfed, baserow, bookwyrm, snipe-it, listmonk, mediawiki, shopware and peertube need absolute URLs for federation, mail or server-side rendering, and decidim and funkwhale sign URLs, which the route deliberately refuses. The prefix moves from /objstore to /_infinito/objstore.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9.2 KiB
Network Modes 🌈
How a node decides which networks it serves each role on, and how vhosts, TLS, cross-app URLs, checks and CI follow that decision. For general documentation rules such as links, writing style, RFC 2119 keywords, and Sphinx behavior, see documentation.md. For the Tor provider itself see svc-net-tor.
Registry 🗂️
reachability.py holds one Network entry per network. Every other module asks it (network_of, sibling_domain, network(name)) instead of testing a domain suffix of its own.
| Network | Label | Provider | Suffix | TLS | Browser proxy |
|---|---|---|---|---|---|
clearnet |
clearnet |
none | none | yes | no |
tor |
onion |
svc-net-tor |
.onion |
no | yes |
handshake is listed in RESERVED_NETWORKS; network('handshake') raises until it is implemented.
Node Modes 🧭
A node runs in one NETWORK_MODE:
| Mode | Networks served |
|---|---|
clearnet 🌐 |
clearnet |
tor 🧅 |
tor |
multi 🌈 |
clearnet and tor |
NETWORK_MODE defaults to empty in 08_networks.yml. Empty resolves to multi when svc-net-tor is deployed with a node onion address and to clearnet otherwise. tor or multi on a node without the provider fails the deploy.
Role Reachability 🎚️
roles/<role>/meta/networks.yml is the single point of truth for the networks one role may be served on:
reachability:
modes: [clearnet, tor]
single_mode: true # nocheck: network-reachability <why the role answers under one domain only>
handover: true # nocheck: network-reachability <why the role answers under its canonical URL only>
modeslists networks from the registry. An empty list or no key means every network.multiis a node mode, not a list value.single_mode: trueserves the role ontorwhen the node runs Tor and onclearnetotherwise, never on both.handover: truekeeps every network sibling, but each sibling outside the canonical network answers302to the same path on the canonical URL and advertises noOnion-Location. The webserver health check expects that302, the CSP crawl skips those siblings and Playwright tests only the canonical domain.single_mode: true,handover: trueand amodeslist below every network each carry# nocheck: network-reachabilityplus a reason on or directly above their line (suppression.md).
test_network_reachability.py enforces the schema and the marker. meta/services.yml keeps only the Tor service bond (bond, enabled, shared).
The effective networks of a role are the node's networks that the role allows, where tor also needs the role's services.tor.enabled to render true, reduced to one network by single_mode. A deployed role with no effective network fails the deploy. A role without a tor service keeps its clearnet domains on every node.
Domains and Vhosts 🌍
get_merged_domainsin domains.py expands every canonical domain of a role into one sibling per effective network, canonical network first. Lists gain the sibling after the clearnet entry; named dicts gain a<key>_onionentry.lookup('network_siblings', domain)returns the vhosts one domain is served under. sys-stk-front-proxy renders one vhost per sibling, and a non-canonical sibling handed in directly renders nothing, so no vhost is rendered twice.- TLS is decided per vhost with
lookup('tls', domain, ...): onion vhosts listen on port 80 in plain HTTP, clearnet vhosts followTLS_MODE. - Every app-id term of the
tlslookup is aligned to the network of the vhost being rendered (align_domain_to_consumerin tls_common.py), in both directions. The SSO provider is never aligned: the node publishes one issuer, the canonical one. - Injected snippets (sys-front-inj-all) and the CSP header (content_security_policy.conf.j2) are rendered per vhost. Clearnet vhosts of a role served on more than one network send
Onion-Location. - Redirect vhosts redirect inside the network of their source domain.
Network-Aligned Values 🧭
A value that names an own or peer host MUST point to the network of the vhost that serves it. Pick the form by how the value is consumed:
| Value | Form | How |
|---|---|---|
Allowlist a peer checks (OIDC redirect and post-logout URIs, web origins, Collabora aliasgroupN, trusted domains, CSP sources) |
Union of every sibling, rendered once | lookup('network_urls', url) for a URL, a loop over lookup('network_siblings', domain) when each entry needs its own scheme or port |
| Asset a browser loads (logo, icon, manifest icon, CDN file) | Same-origin path | lookup('design', ...).relative_urls, lookup('cdn', ...).relative_urls; /_infinito/cdn/ is served on every vhost |
| Object-store file a browser loads (uploads, media in a public bucket) | Same-origin path | lookup('objstore', application_id, 'relative_url'), which is /_infinito/objstore/<bucket> for a shared consumer with public: true; locations.conf.j2 serves it on every vhost of that consumer with GET and HEAD only, no query string and no path ending in / |
| Pointer a browser follows from a snippet rendered per vhost (injected script, proxy config) | Rendered per vhost | lookup('tls', '<app-id>', 'url.base') with domain in scope, which aligns the term to the vhost's network |
| Own public URL an application keeps in one server-side setting | Request-derived or one instance per network | Let the application read X-Forwarded-Host and X-Forwarded-Proto, which every vhost sends; otherwise run one instance per network as web-svc-logout does with network_instances |
- A role MUST NOT write an onion host by hand or rewrite hosts in its own proxy config.
- The SSO issuer is the one host that stays canonical on every network.
- Every plain-HTTP vhost strips
Securefrom upstream cookies withproxy_cookie_flags ~ nosecure(nginx_ssl_header.j2). A location that sets its ownproxy_cookie_flagsreplaces this rule for that location. - Logout: Keycloak holds one front-channel URL per client, the canonical logout conductor. On a vhost outside the canonical network the injected logout patch adds
client_idandpost_logout_redirect_uriset to that network's conductor, and Keycloak registers every conductor sibling as post-logout redirect URI next to+. An external issuer (OIDC_OVERRIDE.URL) keeps the bare logout URL. - test_network_allowlist_sinks.py fails when a role template names an allowlist sink without the sibling union. Suppress a single-network peer with
# nocheck: network-allowlistand a reason.
Checks 🩺
- sys-ctl-hlth-webserver probes every vhost with that vhost's scheme.
- sys-ctl-hlth-csp crawls the
httpand thehttpsvhosts in one run; hosts under the Tor suffix go through the Tor SOCKS proxy.
CI 🚀
- network.py derives the node modes a matrix row can take from the role's Tor bond,
reachabilityand whether its deploy pulls the Tor provider in; thenetworkinput (auto,clearnet,tor,multi) narrows them. See README.md. - A row hands its mode to the deploy as the
networkvariable, which provisioning writes into the inventory asNETWORK_MODE(network_mode.py).make compose-deploy apps=<role> network=tordoes the same locally. - test-e2e-playwright runs the role's suite once, on the vhost of one network sibling. On a node serving the role on several networks the sibling is picked at random and kept for the flake retry, and network-leak.spec.js joins the run with the guest persona. The leak spec fails when a document requests a host of another network or the browser blocks mixed content; the SSO issuer is the one host every network may reach.
Adding a Network ➕
- Add a
Networkentry toNETWORKSin reachability.py with its provider role, suffix, TLS policy, browser-proxy flag and probe timeout, and remove its name fromRESERVED_NETWORKS. - Add the node modes that serve it to
NODE_MODES. - Extend
sibling_domainwith the way the network derives a host from a clearnet one. - Gate the network on its provider's service bond in
allowed_networks, astoris gated onservices.tor.enabled. - Add a glyph for each new node mode to symbol_glossary.py.