Files
MangaCircus/docs/peering.md
T

139 lines
8.1 KiB
Markdown

# Server peering (v1)
Status: implemented, tested in isolated local fixtures, and enabled for a local
two-instance pilot on 2026-10-10. It has **not** been deployed to the VM. See the broader design and
future ideas in [`vision/server-peering.md`](../vision/server-peering.md).
Peering is an opt-in, pull-only transfer between MangaCircus servers. It does not
exchange accounts, reading history, ratings, notes, or scraper credentials. The
publishing gateway reads a library mount, and only the existing scraper publishes
received chapters. No database server or second library writer is introduced.
## Roles and direction
The admin UI is **Settings → Peering**, and all users can open the **Peers** catalog
page. Each server may have many peers. For every peer, its admin independently sets:
- **Serve approved manga**: expose only individually checked library titles to that
peer. Off is the default and invalidates the current serving grant.
- **Allow downloads**: queue and pull chapters from that peer. Off is the default;
a verified pairing and imported grant still allow browsing its approved catalog.
- **Let readers queue**: ordinary readers may request transfers; otherwise only an
admin may queue. Browsing a permitted catalog does not disclose the local user to
the remote server.
- Upload streams, bytes/s and bytes/day, and receiving pages in flight and bytes/s.
Defaults are two in-flight pages and 2 MiB/s in each direction per peer. The
publisher's caps remain authoritative. A gateway-wide four-stream ceiling applies.
Serving and receiving are separate directions. Pairing by itself exposes no title.
## Deployment
The optional `peer-gateway` Compose profile publishes port 8093 on **127.0.0.1** by
default. It uses the same image as reader/scraper, a read-only library mount, a
read-only peering policy/identity mount, and its own writable usage-counter mount.
The gateway needs an external-facing Docker network for its host port to work on
Docker Desktop; it also joins the private backend. It has no reader account database,
scraper queue, Docker socket, or writable library mount. It must never be scaled
past one instance for the same usage-counter directory.
1. Build the image and start the normal stack as documented in
[`compose-deployment.md`](compose-deployment.md). Generate the local certificate
in **Settings → Peering**. Keep `/data/peering/identity.key` private and backed up.
2. Start the optional service with `docker compose --profile peering up -d
peer-gateway`. Verify `docker compose --profile peering ps` shows healthy and
`https://127.0.0.1:8093/health` responds. `/health` is deliberately unauthenticated
and contains only `{ "ok": true }`.
3. Give the other server a reachable HTTPS endpoint for port 8093. A private
Tailscale TCP forward is recommended for the pilot; the default loopback binding
is not remotely reachable. Do not expose port 8093 on the public internet merely
to make pairing work. Use a stable tailnet DNS name, not a changing IP. Keep
scraper (8091) and Sentinel (8092) private.
The `PEER_BIND`, `PEER_PORT`, `PEER_DATA_PATH`, and `PEER_USAGE_PATH` Compose variables
control the bind and persistent paths. The gateway's internal port remains 8093.
OpenSSL is bundled in the app image for one-time P-256 self-signed certificate
generation. Each certificate is valid for one year. Expired/replaced identities
require administrator-supervised re-pairing; there is no automatic key rotation yet.
## Pairing and transfer
1. Each admin generates an identity, creates a 48-hour invitation, and exchanges the
JSON outside the app. The JSON contains a public certificate, SHA-256 fingerprint,
URL, invitation ID and one-use code—**not** the private key. Compare the
fingerprints through a separate trusted channel.
2. Both admins import the other's invitation and press **Test & verify pairing**.
The HTTPS handshake presents client and server certificates, checks their exact
pins and dates, and consumes the one-use invitation code. Both sides must probe.
3. A publisher checks specific titles under that peer, enables **Serve**, issues a
90-day random grant, and sends the token privately to the receiving admin. The
receiving admin imports the grant and enables **Allow downloads**. Reverse
direction needs its own share selection and grant. The server stores only a hash
of a grant it issued; the receiver stores its imported token server-side.
4. A local user browses the catalog, selects missing/newest/last-five or an exact
chapter, and queues it through the normal scraper. The peer adapter downloads up
to the configured pages in flight, validates size and SHA-256, resumes a partial
page with HTTP Range, checkpoints between batches, and atomically publishes each
complete chapter into the ordinary library. Existing-chapter policy is applied
before download. Partial transfers remain in the hidden scraper work folder.
The publisher sees the receiving **server** certificate and transfer requests, not
the human reader. It can still observe requested titles, times and byte counts.
## Peer v1 HTTP interface
All content endpoints require TLS 1.3 client certificate pinning, a paired/serving
peer, and a currently valid certificate-bound bearer grant. There is no insecure
HTTP fallback and no path-based file browsing.
| Request | Response |
| --- | --- |
| `GET /peer/v1/hello` with one-use invitation headers | Protocol version and server identity fingerprint; no catalog |
| `GET /peer/v1/catalog?cursor=N` | Up to 50 approved opaque title IDs, title/year/status, next cursor |
| `GET /peer/v1/manga/{id}?cursor=N` | Up to 100 complete chapter IDs and light metadata |
| `GET /peer/v1/manga/{id}/chapters/{id}` | Ordered page IDs, MIME, byte size, SHA-256 and ETag |
| `GET /peer/v1/blobs/{id}` | One image, optional `Range: bytes=N-` and `If-Match`; 206 on resume |
Unknown/unshared IDs return 404. Revoked serving returns 403; bad/missing grants
return 401; caps return 429 and `Retry-After`. No source-site URL, local filesystem
path, or user record is sent in protocol responses. Individual complete chapter
manifests are discovered from final `chapter.json` folders; an unfinished folder is
not shared. Catalog and chapter lists are short-lived in-memory caches. Turning off
serving blocks subsequent requests immediately, though an already streaming image
may finish its current chunk/response.
The initial implementation sends original image bytes; it does not transcode them.
See [`vision/server-peering-compression-test.md`](../vision/server-peering-compression-test.md)
for the measured reason. The transport accepts a small amount of controlled page
parallelism, not unbounded connections.
## Tests and safe operation
`npm run test:peer` creates disposable A/B identities, gateways and libraries and
tests mTLS pairing, no-cert/bad-grant rejection, private catalog, per-title sharing,
manifest hashes, a resumed range response, scraper publication, duplicate skip and
revocation. `npm test` includes it. An isolated Compose acceptance is:
```powershell
$env:APP_IMAGE = 'mangacircus:local' # replace with the image just built
$env:QA_PORT = '8188'
$env:QA_PEER_PORT = '8193'
node tools/test-compose.js --peer-gateway --peer-ui
```
It makes disposable library/data, four containers, a certificate and a browser
session, then tears them down. It never mounts the live library/queue.
For live rollout, wait for a scraper checkpoint, back up the library and data
(including the peering identity, policy, outbound grants, gateway usage and hidden
work folders), and deploy reader/scraper/gateway from one immutable image digest.
There must remain exactly one queue-owning scraper and one reader database writer.
Peering is optional: stop the `peer-gateway` profile to close incoming service
without affecting reading or existing local downloads. Do not delete the identity
to recover from a network error; that would break every peer pin.
Current limits: no automatic certificate renewal, invitation cancellation UI,
peer cover-image endpoint, multi-peer performance pilot, VM deployment, or public
internet exposure review. The gateway has no automated hoster-interaction logic;
source scraping remains subject to the existing source-safety policy.