aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/api.md10
-rw-r--r--docs/architecture.md5
-rw-r--r--docs/dashboard.md99
-rw-r--r--docs/development.md5
-rw-r--r--docs/example.toml33
-rw-r--r--docs/measurements.md6
-rw-r--r--docs/openapi.yaml11
-rw-r--r--docs/operations.md68
-rw-r--r--docs/peers.md5
-rw-r--r--docs/trust.md7
10 files changed, 214 insertions, 35 deletions
diff --git a/docs/api.md b/docs/api.md
index 5b4dac4..cdc722e 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -31,8 +31,9 @@ curl -u alice:secret -X POST 'localhost:3000/store/stats/visits?increment=1' #
* Bodies are JSON (`application/json`, the default), text (`text/plain`, stored as a
string), or a value of a type of its own (Types); answers are JSON, or as `Accept`
asks. Numbers in JSON are stored as integers where they fit, as floats otherwise.
-* Every request needs a user from the node's `--htpasswd` (basic authentication). Users
- are all alike: each can read and write everything but protocol data (below).
+* Every request needs a user from the node's `--htpasswd` (basic authentication), or
+ named by a reverse proxy in front that authenticated it (`--user-header`;
+ [`operations.md`](operations.md), Behind a reverse proxy). Users are all alike: each can read and write everything but protocol data (below).
* A key with `/` in it, or an empty key, can be written inside a value at its parent,
but not named in a path: read it through the parent.
@@ -193,6 +194,9 @@ node or by sync:
{"key":"settings/theme","deleted":true}
```
+While nothing changes, a blank line every 30 s (`--stream-heartbeat`) keeps proxies in
+front from taking the stream for dead; skip blank lines.
+
An event carries the value as a read would return it then. Changes in quick succession
may come as one event with the latest value: a stream tells the value, not every write.
A change that leaves the value as it was sends nothing. The connection stays open; after
@@ -222,7 +226,7 @@ Load balancers can send requests to any ready node: `/ready` is the check.
Every change records the node that committed it (as its Automerge author) and, for a
write through the API, the user who wrote it (in its message, as a trailer like Git's
`Signed-off-by:`: `User: alice`), with when (by the writer's clock). User names are
-each node's own (`--htpasswd`), so who wrote it is that user at that node: `alice` on
+each node's own (`--htpasswd`, or its proxy's), so who wrote it is that user at that node: `alice` on
two nodes is one person only where their operators make it so, which nothing checks.
`GET /admin/changes` lists them,
newest first, with the paths each touched; `GET /admin/changes/settings` those that
diff --git a/docs/architecture.md b/docs/architecture.md
index e0e2c94..9b9db71 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -225,14 +225,15 @@ catch-up source. The phases, what each serves and how the files come in: [`lifec
the write lock, stamped with the clock) → `publish_change` (bumps the version, emits
`DocumentChanged`) → the sync worker generates a message per neighbour → each
neighbour applies it, relays its new heads to *its* neighbours, which pull what they
-lack. In a full mesh (up to `k + 1` members) the writer reaches everyone directly.
+lack. In a full mesh (up to `k + 1` members) the writer reaches everyone directly, but
+those on none of its transports, to whom the nodes between announce.
**An inbound message.** Consumer → `handle_message`: drop our own and stale-run
replies, admit (swarm key), mark the peer seen, record its epoch (ignoring late ones),
then by type: HELLO answered, HELLO_REPLY establishes membership and discovers peers,
sync queued on its lane if on our epoch (held back from everyone but the source while catching
up), STATUS answered with a STATUS_REPLY and compared for divergence, blobs served or collected,
-LINKS answered with the node's links if the asker is a member, and again on each change while it subscribes (`swarm::links`, for the dashboard);
+LINKS answered with the node's links if the asker is a member, and again on each change while it subscribes, or with other members' links it asks for the asker, which can't reach them (`swarm::links`, for the dashboard);
PING and PONG (the paths layer answered the PING already) only mark the peer seen, and
after a PONG the peer's round-trip time is its path's again.
diff --git a/docs/dashboard.md b/docs/dashboard.md
index b7e3069..25bcfe8 100644
--- a/docs/dashboard.md
+++ b/docs/dashboard.md
@@ -14,10 +14,10 @@ cargo build --release --features mycelium,dashboard
```
It is at `/admin/dashboard` on the node's HTTP API, by default
-`http://127.0.0.1:3000/admin/dashboard`, and asks for a user from `--htpasswd`, as the
-rest of the API does. The API listens on localhost unless `--bind-addr` says otherwise;
+`http://127.0.0.1:3000/admin/dashboard`, and asks for a user from `--htpasswd` (or one
+a reverse proxy names, `--user-header`), as the rest of the API does. The API listens on localhost unless `--bind-addr` says otherwise;
to look at a node on another machine, forward the port (`ssh -L 3000:localhost:3000
-<machine>`) or put a TLS proxy in front ([`trust.md`](trust.md)).
+<machine>`) or put a TLS proxy in front ([`operations.md`](operations.md), Behind a reverse proxy).
Every node has its own dashboard, and shows the swarm as that node sees it: what it
knows of the others, what they report to it, and its own figures.
@@ -100,16 +100,19 @@ else the fresher. Then:
* **Narrow, over a red band**: either end holds the other dormant, or the path in use
is down (no path to the peer answers; `/peers` shows its connection as `null`).
-Messages this node exchanges run along its links as dots, from sender to receiver.
+Messages this node exchanges run along its links as dots, from sender to receiver, all
+at one speed: over a longer link they arrive later.
They come from the per-peer message counts, which arrive every 2 s: what was sent and
received since is spread over the next 2 s, at most 6 dots per link and direction,
shared out by message kind, each kind in its colour (named in the legend; the console
colours them alike). A dot is as large as its kind's messages were on average
-(by the size's logarithm: a ping small, a large sync message large); the slider right
-after their legend makes all dots larger or smaller, kept in the browser. A picture
+(by the size's logarithm: a ping small, a large sync message large); the `size` slider
+right after their legend makes all dots larger or smaller, and the `speed` slider after
+it makes them run faster or slower (0.1 to 0.9 map units per millisecond, 0.3 at
+first: a link of 200 in about 0.7 s); both are kept in the browser. A picture
of the traffic, not a record of it; links between other members show none.
-`messages: on` after the slider hides them (`messages: off` shows them again; the
-legend and the slider greyed meanwhile); the choice is kept in the browser.
+`messages: on` after the sliders hides them (`messages: off` shows them again; the
+legend and the sliders greyed meanwhile); the choice is kept in the browser.
A label on the map that changes, a round trip or a node's name, breathes once: it
glows around its new text and fades (not where the system asks for reduced motion).
@@ -120,8 +123,10 @@ every 30 s, which costs the swarm fewer messages and leaves the map up to 30 s b
(this node's own links stay current). It's the node's choice, for every page open on
it, until clicked again or the node restarts.
-The legend above the map names the transports that appear; `hide legend` beside the
-box's title gives its room to the map (`show legend` brings it back).
+The legend, over the top of the map, names the transports that appear; its contents
+are on an underlay of their own shape, as the map's labels, so they read over what is
+under them, and the map is dragged through it as anywhere else. `hide legend` beside
+the box's title hides it (`show legend` brings it back); the map stays as it is.
`layout: static`, beside it, is the default: nodes move only when the map is laid out
(below). A click switches to `layout: dynamic`: each measured link is as
@@ -146,10 +151,23 @@ measured.
* **Drag a node**: move it. On a dynamic map, the others follow at once, and once
let go of, it moves back among them; on a static map it stays where it is put until
the map is laid out again.
-* **Double-click a node**: pin it where it is (a dot in its middle): the layout leaves
- it there. A double-click whose second press drags the node pins it where it's
- dropped. Double-clicked again, it's let go, and moves to its place among the others
- (on a static map, only it moves).
+* **Double-click a node**: open its dashboard, in this page, where the node publishes
+ one (`--dashboard-url`, in the member list; e.g. behind a reverse proxy,
+ [`operations.md`](operations.md)). The other dashboard asks for its own credentials
+ where its proxy or node does. It opens as this one was left: the map's nodes where
+ they were (on a dynamic map they then move to their places among the other node's
+ links, smoothly; new ones are placed among them), the view, the pins, what is in
+ focus, the legend shown or hidden, what is open in the figures and in the document,
+ `links: live` or `every 30 s` (the other node's choice, for every page open on it),
+ and this browser's settings for the page (sizes, collapsed boxes, the layout, the
+ messages' size and speed, the console's filter), which are per host otherwise.
+ They travel in the URL's fragment, which no request carries, and the page removes it
+ at once. Links are drawn as the other node sees them. Where it publishes none, its
+ halo flashes red and "no dashboard" rises from it, fading out.
+* **Middle-click a node**: pin it where it is (a dot in its middle): the layout leaves
+ it there. Dragged with the middle button, it is pinned where it's dropped.
+ Middle-clicked again, it's let go, and moves to its place among the others (on a
+ static map, only it moves).
The map is laid out when the page opens, and again when the window or the box is
resized. A member that comes later is placed among the others, which stay where they
@@ -224,7 +242,8 @@ The messages this node exchanged with its peers, one by one: when (on the 24-hou
clock, to the millisecond), which way (`→` sent, `←` received, `↛` refused: the
transport wouldn't send it), with whom, the type (`HELLO`, `SYNC`, `STATUS_REPLY`,
`LINKS`, …, in its kind's colour, as on the map) and the size. The latest 500, the
-newest at the bottom; scrolled along while at the bottom. They arrive with every update
+newest at the bottom; scrolled along while at the bottom, which `following…` beside the
+title says. `clear` empties it; what comes from then on fills it again. They arrive with every update
(every 2 s). The PINGs the paths layer sends on its own, and the PONGs it answers
PINGs with, are listed too: under the node at the address they went to, where it is
known, else under the address. Not listed: WHO and ME, which stay in the layers
@@ -242,6 +261,29 @@ requests and answers, from whichever peers (by the blob), and a delta offered in
with the blob it is fetched as. A STATUS and its reply carry nothing that ties them, nor
does an ordinary SYNC.
+The map marks the message's two nodes and the link between them in the grey its line
+has in the console, each in its own shape: a circle around each node, and the link's
+lines as they bow. The message shown and the one pointed at are both marked, each in
+its line's grey; where they mark the same node or link, the shown one's wins.
+
+The other way round, the messages exchanged with a node, or over one of this node's
+links, are tinted in the colour of its halo on the map: lightly while it is pointed at
+there, stronger while it is in focus (clicked there or in the peers); where both tint a
+message, in focus wins.
+
+`filter selected`, beside the console's title, shows only the messages with the node or
+over the link in focus (clicked on the map or in the peers; pointing at one doesn't
+count), and all of them while nothing is. Where there are none to show (a link between
+two other nodes, a node this one exchanges nothing with), it says so. Whenever what it
+shows changes (turned on or off, another focus, or none), the console goes to the newest
+message and follows from there. The choice is kept in the browser.
+
+At the right of the console's title, `msg/s` gives the messages per second sent and received, as load averages
+do: over the last 1, 5 and 15 minutes (exponentially weighted), since the page was
+opened, from the message counts each update brings; below it, the bytes per second
+alike, in `B/s`, `KiB/s` or `MiB/s`, whichever the largest of the three needs. With the filter on, they are those
+with the node or over the link in focus; `–` where there are none.
+
## While you read
Updates don't pull things from under you: while text is selected in a box, that box
@@ -254,7 +296,9 @@ are done. The map isn't redrawn under the pointer: what you point at stays put.
built with `dashboard` (404), or the user isn't in `--htpasswd` (401).
* **A hollow node**: no links report from it. If it is also dormant here, it is
probably down or unreachable; if not, it may not take this node for a member yet
- (it hasn't registered it), or its answers don't arrive.
+ (it hasn't registered it), or its answers don't arrive. A node that just joined
+ through others shows as soon as one of them reports a link to it, hollow until its
+ registration reaches this node and it is asked for its links (within 30 s).
* **Only dashed grey between two nodes**: the member list wants them as neighbours, but
neither reports a link to the other: they haven't reached each other (yet).
* **A dotted link**: one end has stopped reporting; the link is as it was last
@@ -290,6 +334,25 @@ older than 90 s is left out. A member keeps 64 subscribers at most. With `links:
and the members are only asked every 30 s, for every page open on this node, until
asked otherwise or the node restarts.
+A member this node can't reach (on none of its transports: one on Tor alone, from a
+node on libp2p alone) is asked through a peer that can: an active member it reaches
+that, by the member list, shares a transport with the member, else one on a transport
+this node isn't on (the lowest pair score of several). The LINKS names the members to
+ask (`of`): the peer asks them on this node's behalf, itself or again through a peer
+of its own (three nodes at most), for as long as this node asked, and passes their
+links on in its replies (`relayed`, each with its age), several together (64 members'
+at most, and 256 KiB as encoded but for one member's alone): it holds what arrives a
+while. The asker's window (250 ms, at most 5 s) is for the whole way:
+each hop holds its share of what is left, the window divided among it and the hops
+that may follow (at least 10 ms), and passes the rest on, so links reach the asker
+within the window however long the chain. Such a subscription isn't ended by a message: every hop's runs
+out unless renewed, so it ends within 90 s of the page closing or the node going away.
+A hop renews its own as soon as it is renewed, for the same time, so the hops run in
+step, each a message's delay behind the one before (it never runs out first), and a
+renewal keeps its nonce, so what is on its way under the last one still counts. A
+renewal lost on the way leaves the hops behind it to run out at the old time: with 90 s
+renewed every 30 s, one lost costs nothing.
+
The links a member reports: its neighbours and the peers it holds dormant (a neighbour
that goes dormant is one no more, but stays listed until it is heard from again), by
which transport, whether only through a relay, the round trip (as the paths layer
@@ -297,7 +360,9 @@ measures it), whether it holds the peer dormant or the path is down, the peer's
transports with a path up (each with its fastest path's round trip, and whether it goes
only through a relay), and the peer's clock against the member's ([`measurements.md`](measurements.md),
Per peer, `clock`). A member answers a LINKS from members only, at most every 5 s, and
-every build answers, with or without the feature (`swarm::links`).
+asks for each peer that has it relay at most 512 members at once and 32 a second on
+average (the rest of a request is dropped); every build answers, with or without the
+feature (`swarm::links`).
**Clocks.** The clocks in the links chain every member's clock to this node's, along
the narrowest bounds, and the map takes each against their median: a member more than
diff --git a/docs/development.md b/docs/development.md
index 93726b1..d7f6068 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -50,3 +50,8 @@ take as long as the protocol's timeouts make them, however fast the machine, and
busy machine some fail now and then (a node's timer fires before the work it waits for
is done). One failing alone (`cargo sim <name>`) and passing then is that, not a
regression; one failing again and again is a regression.
+
+A node on several transports is a node on several simulated nets, one per scheme
+(`Sim::start_on`, under the identity layer): nodes on different nets can't reach each
+other, as one on Tor alone and one on libp2p alone can't, so a swarm whose transports
+don't all meet is simulated with the simulated transport alone.
diff --git a/docs/example.toml b/docs/example.toml
index 95b6f52..00bfed3 100644
--- a/docs/example.toml
+++ b/docs/example.toml
@@ -37,6 +37,12 @@ node-key-file = "/var/lib/continuum/node.key"
# member list; not unique, never an identity.
# alias = "node-${env:HOSTNAME}"
+# Where people reach this node's dashboard (http:// or https://, e.g. behind a reverse
+# proxy). Published in the member list: the other nodes' dashboards open it on a
+# double-click on this node. A build without the dashboard feature warns and publishes
+# none (removing one published before).
+# dashboard-url = "https://node-1.example.com/admin/dashboard"
+
# Only relay (libp2p circuit relay), without being a node: no document, no swarm, no
# node key. Needs libp2p alone (libp2p's enable, no other), its listen and
# key-file; options about a swarm or a document (node-key-file, seeds, genesis,
@@ -47,14 +53,33 @@ node-key-file = "/var/lib/continuum/node.key"
# --- The HTTP API ------------------------------------------------------------------------
-# Address of the HTTP API. Plain HTTP: to expose it, put a TLS proxy in front.
-# bind-addr = "127.0.0.1:3000"
+# Where the HTTP API listens: TCP addresses, and Unix sockets (unix:PATH, made readable
+# and writable by the node's user and group only), as many as given. Plain HTTP: to
+# expose it, put a TLS proxy in front.
+# bind-addr = ["127.0.0.1:3000", "unix:/run/continuum/api.sock"]
# Users of the HTTP API (an htpasswd file with Argon2 hashes). Required (but with
-# relay-only: without it, no HTTP API).
+# user-header, or relay-only: without either, no HTTP API).
htpasswd = "/etc/continuum/htpasswd"
-# HTTP API users (from htpasswd) who may write protocol data under /store/_continuum
+# A request header naming the user, set by a reverse proxy in front of the HTTP API that
+# authenticated the request itself (e.g. X-Remote-User, from nginx's $remote_user).
+# Believed only from trusted-proxies; other requests need htpasswd's basic
+# authentication. The proxy must set or clear it on every request.
+# user-header = "X-Remote-User"
+
+# How often GET /stream sends a blank line while nothing changes, so proxies in front
+# keep it open: below their read timeout (nginx's proxy_read_timeout, 60 s by default).
+# 0: never.
+# stream-heartbeat = "30s"
+
+# Where the reverse proxies whose user-header is believed connect from: IP addresses (or
+# ranges, as ADDRESS/BITS), and unix for the API's Unix sockets (bind-addr). On a host
+# with other users, leave out the addresses and have the proxy use a Unix socket: others
+# can connect over loopback too.
+# trusted-proxies = ["127.0.0.1", "::1", "unix"]
+
+# HTTP API users (from htpasswd or user-header) who may write protocol data under /store/_continuum
# (remake records, members, forget records), e.g. to repair a stuck remake. Such writes
# act on the whole swarm and are logged. Epoch metadata and the swarm ID are never
# writable.
diff --git a/docs/measurements.md b/docs/measurements.md
index 2adc0f5..fb01de0 100644
--- a/docs/measurements.md
+++ b/docs/measurements.md
@@ -168,9 +168,9 @@ clocks"); setting them is the operating system's job (NTP).
Messages and payload bytes by direction (`sent`, `received`, `refused`) and kind
(`hello`, `helloReply`, `sync`, `status`, `statusReply`, `blob`, `identity` for WHO and
-ME, counted under the address they went to or came from, so only in the totals, and
-`paths` for PING and PONG: per peer, but the paths layer's own PINGs under the address,
-so only in the totals). Payload bytes are what a transport's message carries (the wire
+ME, and `paths` for PING and PONG). The layers below the node count WHO and ME, and the
+paths layer's own PINGs and PONGs, under the address they went to or came from; a peer
+(`/peers`) has those of its addresses too, where it is known to be at them. Payload bytes are what a transport's message carries (the wire
encoding): not the transport's framing (for Mycelium, its topic and the base64 of its
HTTP API). `refused` is what the transports wouldn't accept to send (no path to the
peer), not counted as sent.
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
index 78912fa..701cdf9 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -7,7 +7,8 @@ info:
as JSON by path, that syncs over Mycelium, libp2p or Tor.
Every endpoint requires HTTP basic authentication (users from `--htpasswd`,
- Argon2 hashes), except `/health`, `/ready`, the Swagger UI (`/swagger-ui`) and
+ Argon2 hashes), or a user named by a trusted reverse proxy in front
+ (`--user-header`), except `/health`, `/ready`, the Swagger UI (`/swagger-ui`) and
this description (`/openapi.yaml`).
`/debug/*` exists only with `--debug-api`.
@@ -328,7 +329,8 @@ paths:
description: |
Newline-delimited JSON: one event with the current value, then one whenever
the resolved value changes (by a local write or by sync). The connection stays
- open.
+ open; while nothing changes, a blank line every `--stream-heartbeat` (30 s)
+ keeps proxies from closing it: skip blank lines.
responses:
'200':
description: Event stream.
@@ -747,8 +749,9 @@ paths:
description: |
The dashboard's latest snapshot: `node` (`id`, `alias`), `page` (the page's
version, a hash: a page showing another reloads), `stats` (as
- `/admin/stats`), `peers` (as `/peers`) and `graph`: `members` (by node ID:
- `alias`, `self`), `intended` (by node ID: the neighbours the member list gives
+ `/admin/stats`), `peers` (as `/peers`) and `graph`: `members` (by node ID,
+ those of the member list, and any a link reported goes to: `alias`, `self`,
+ `dashboard`: its `--dashboard-url`), `intended` (by node ID: the neighbours the member list gives
it, as this node computes them), `measured` (by node ID: `secsAgo` and
`links`, each with `peer`, `transport`, `relayed`, `down`, `rttMs`, `dormant` and
`others` (the peer's other transports: `transport`, `rttMs`, `relayed`), as the member
diff --git a/docs/operations.md b/docs/operations.md
index fe09537..fae3b6c 100644
--- a/docs/operations.md
+++ b/docs/operations.md
@@ -103,6 +103,74 @@ One node at a time, restarting each with its files. What to know:
switching ([`remakes.md`](remakes.md), Limits). Don't remake while the swarm runs two versions.
* **The dashboard** reloads by itself once its node serves another version of it.
+## Behind a reverse proxy
+
+The HTTP API speaks plain HTTP on `127.0.0.1:3000`; a reverse proxy in front adds TLS,
+and can authenticate users itself (single sign-on, client certificates, its own
+password file). It then names the user in a header, which the node believes from where
+the proxy connects alone (`--user-header`, `--trusted-proxies`); requests from
+elsewhere still need `--htpasswd`, which becomes optional. The proxy must set or clear
+that header on every request, or a client could set it itself.
+
+Where the proxy connects decides who can pose as it. Every user of a host connects
+from its loopback addresses, so trusting those (as `--trusted-proxies` does by
+default) trusts everyone on the host. A Unix socket is better: `--bind-addr` takes
+`unix:PATH` too, as many places as given, and makes the socket readable and writable
+by the node's user and group only, so only members of that group (the proxy's user,
+added to it) can connect at all. Trust it alone with `--trusted-proxies unix`, and keep
+a TCP address for the rest (a command-line tool, with `--htpasswd`):
+
+```sh
+continuum --bind-addr 127.0.0.1:3000,unix:/run/continuum/api.sock \
+ --user-header X-Remote-User --trusted-proxies unix --htpasswd htpasswd ...
+```
+
+```nginx
+location / {
+ auth_basic "continuum";
+ auth_basic_user_file /etc/nginx/continuum.htpasswd;
+ proxy_set_header X-Remote-User $remote_user;
+ proxy_pass http://unix:/run/continuum/api.sock;
+}
+```
+
+The directory of the socket must exist and let the proxy's user through (with systemd,
+`RuntimeDirectory=continuum` and `RuntimeDirectoryMode=0750`, the proxy's user in the
+node's group). A socket left behind by an earlier run is replaced, and the node removes
+its own when it stops. With a proxy on another host, trust its address instead, and
+have the node listen where only that host reaches it.
+
+Where a socket won't do, the host's firewall can keep everyone but the proxy's user
+off the API's port, as Linux filters local connections by the user that opens them.
+With nftables, and nginx running as `www-data`:
+
+```
+table inet continuum {
+ chain output {
+ type filter hook output priority 0;
+ oifname "lo" tcp dport 3000 meta skuid != "www-data" reject
+ }
+}
+```
+
+(with iptables: `-A OUTPUT -o lo -p tcp --dport 3000 -m owner ! --uid-owner www-data -j
+REJECT`). The loopback addresses can then stay trusted; root can still get past it, as it
+can past a socket's permissions. Every address of the API on loopback is trusted alike,
+so a command-line tool of another user goes through the proxy then.
+
+The names the proxy gives are the users of `--protocol-writers` and of who wrote what
+([`api.md`](api.md)).
+
+What streams (`/stream`, the dashboard's events) says `X-Accel-Buffering: no`, so nginx
+passes it on as it comes, and says it's alive while nothing changes: the dashboard's
+events every 15 s, `/stream` every 30 s (a blank line, `--stream-heartbeat`), within
+nginx's `proxy_read_timeout` (60 s). With a proxy that gives up sooner, lower
+`--stream-heartbeat` below its timeout.
+
+The API may also be served under a path of its own (`location /continuum/ {
+proxy_pass http://unix:/run/continuum/api.sock:/; }`): its redirects (`Location`), the dashboard's
+requests and the Swagger UI's are relative, so they stay under it.
+
## What to alert on
From the metrics (`GET /admin/stats` as Prometheus, [`measurements.md`](measurements.md)):
diff --git a/docs/peers.md b/docs/peers.md
index 95b4dab..c63cec1 100644
--- a/docs/peers.md
+++ b/docs/peers.md
@@ -284,7 +284,10 @@ the current pull is answered, if they still have something it lacks. The regular
STATUS (every third keepalive) triggers the same pull, so gaps close within a few
keepalive intervals. A node
that just caught up announces its heads once. In a full mesh nothing is announced: the
-writer reaches everyone (while a node doesn't know its neighbours yet, it announces).
+writer reaches everyone (while a node doesn't know its neighbours yet, it announces);
+but to the neighbours the writer can't reach, those on none of its transports (by the
+member list, the writer's neighbours as this node computes them): a node on Tor and
+libp2p announces to one on Tor alone what came from one on libp2p alone.
## Keepalive and self-healing
diff --git a/docs/trust.md b/docs/trust.md
index 521a175..165fc9b 100644
--- a/docs/trust.md
+++ b/docs/trust.md
@@ -34,7 +34,12 @@ secret like the swarm key: readable by its owner only (a node warns otherwise; a
it creates is).
**The HTTP API.** Every endpoint but the API description needs a user from
-`--htpasswd`. It listens on `127.0.0.1:3000` by default (`--bind-addr`) and speaks
+`--htpasswd`, or one a reverse proxy names (`--user-header`): the node believes that
+header from `--trusted-proxies` alone, but from those blindly, so whoever can send
+requests from there can be any user. With the default loopback addresses, that is every
+user of the host; with only the API's Unix socket (`--trusted-proxies unix`), those its
+file's permissions let in: the node's user and group ([`operations.md`](operations.md), Behind a
+reverse proxy). It listens on `127.0.0.1:3000` by default (`--bind-addr`) and speaks
plain HTTP: to expose it, put a TLS proxy in front. All users may read and write user
data and use the admin endpoints (remakes, forgetting peers, exports, log filters).
There are no read-only users: whoever may open the dashboard may also edit and delete