pmtiles-swarm 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,40 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.13.0
11
+ ### ✨ Features and improvements
12
+ - **The publisher's DHT socket is bound explicitly, on a configurable port.** It was left to bind
13
+ implicitly on its first send, which works but takes an unpredictable ephemeral port and reports
14
+ nothing — so there was no way to forward it, and no way to tell which one it had. `mutable.dhtPort`
15
+ now sets it (`0`, ephemeral, by default) and the port is logged at startup.
16
+
17
+ Publishing needs no forward either way: a put is outbound, and the replies come back on the same
18
+ socket the way any UDP client's do. Pinning and forwarding one makes this a *reachable* DHT node
19
+ instead, which earns a better routing table and contributes back — worth having on a node that
20
+ runs continuously.
21
+
22
+ It must not collide with an engine's port. Each seeding engine runs a DHT of its own, so a node
23
+ with both has three UDP participants and only this one is placed here; two sockets cannot hold
24
+ one port and the node would fail to start.
25
+
26
+ ### 📚 Documentation
27
+ - **[docs/running-as-a-service.md](docs/running-as-a-service.md) covers the publisher key**:
28
+ generating it as the service account so ownership is right, `chmod 400` because nothing ever
29
+ writes it back, and why it must be backed up off the machine — losing it breaks every style
30
+ pointing at that public key, permanently, with no reissue. Also what happens under HA config
31
+ sync: the configuration replicates to the standby and the key does not, so the standby logs
32
+ `not publishing: ENOENT` and serves on, which is the intended outcome rather than a fault. And
33
+ how to confirm it works, including that `nodes: 0` in the log means nobody stored the record
34
+ however healthy the rest of the line looks.
35
+ - **The ports table lists the DHT port**, which had only been described in the publisher section —
36
+ not where anyone looks when deciding what to forward.
37
+ - **Why publishing does not reuse an engine's DHT** is recorded in
38
+ [src/publisher.js](src/publisher.js), since it will be asked again. libtorrent's is unreachable:
39
+ the 2.x Python bindings expose neither `dht_put_item` nor `dht_get_item`, though the alerts are
40
+ bound, so the C++ side supports BEP 44 and there is simply no method to call. WebTorrent's *is*
41
+ `bittorrent-dht` and could be reused to save a socket; that is a deliberate choice rather than an
42
+ oversight, taken to keep one code path that behaves the same whichever engine is configured.
43
+
10
44
  ## 0.12.0
11
45
  ### ✨ Features and improvements
12
46
  - **A category can now be addressed without a server at all.** A node that builds can publish a
@@ -354,6 +354,116 @@ What the running service actually has:
354
354
  systemctl show -p ReadWritePaths -p UMask -p SupplementaryGroups pmtiles-swarm
355
355
  ```
356
356
 
357
+ ## The publisher key
358
+
359
+ Only needed if this node publishes BEP 46 records — the signed DHT entries that
360
+ let a style point at a category rather than at a build that goes stale. Skip
361
+ this section entirely if it does not build archives.
362
+
363
+ **Generate it as the service account**, so the file is owned by the user that
364
+ has to read it:
365
+
366
+ ```sh
367
+ sudo -u pmtiles-swarm -H /var/lib/pmtiles-swarm/node_modules/.bin/pmtiles-swarm publisher-key > /etc/pmtiles-swarm/publisher.pem
368
+ sudo chown pmtiles-swarm:pmtiles-swarm /etc/pmtiles-swarm/publisher.pem
369
+ sudo chmod 400 /etc/pmtiles-swarm/publisher.pem
370
+ ```
371
+
372
+ The PEM goes to stdout and the public key to stderr, so the redirect above
373
+ captures only the key material and you still see the public half on the
374
+ terminal. Write that public key down — it is what subscribers point at, and it
375
+ is the one part you will want later.
376
+
377
+ `400` rather than `600`: the service only ever reads this. Nothing in the
378
+ product writes it back, so removing write permission costs nothing and means a
379
+ compromised process cannot quietly replace it.
380
+
381
+ Then in `/etc/pmtiles-swarm/swarm.config.json`:
382
+
383
+ ```json
384
+ {
385
+ "mutable": {
386
+ "publish": true,
387
+ "keyPath": "/etc/pmtiles-swarm/publisher.pem"
388
+ }
389
+ }
390
+ ```
391
+
392
+ `/etc/pmtiles-swarm` is already in `ReadWritePaths` because the console rewrites
393
+ the configuration there, so the unit needs no change.
394
+
395
+ ### Back it up, off this machine
396
+
397
+ Losing this file breaks **every style pointing at that public key, permanently**
398
+ — there is no recovery, no reissue, and no way to prove to a subscriber that a
399
+ new key is you. It is not like an API key you can rotate.
400
+
401
+ Back it up somewhere that is not this disk, and treat the backup as seriously as
402
+ the original: whoever holds it can publish a signed record telling your
403
+ subscribers that any archive is the current build, and clients will believe it
404
+ because the signature checks out.
405
+
406
+ ### Exactly one publisher
407
+
408
+ Two nodes publishing under one key fight over the sequence number, each
409
+ overwriting the other's claim about what is current. So the PEM belongs on the
410
+ build node and nowhere else.
411
+
412
+ **This matters if you run HA config sync.** The configuration will replicate to
413
+ the standby, `publish: true` and all — but the PEM will not, because you are
414
+ not going to copy it. The standby then logs
415
+
416
+ ```
417
+ [mutable] not publishing: ENOENT: no such file or directory
418
+ ```
419
+
420
+ on every start and carries on serving normally. That is the intended outcome
421
+ rather than a fault: the config syncing is harmless, and the key not syncing is
422
+ the point. If the noise bothers you, set `mutable.publish` to `false` in the
423
+ standby's config after the sync.
424
+
425
+ ### It does not need a port forwarded
426
+
427
+ The DHT socket is separate from the seeding engine's, and by default takes an
428
+ ephemeral UDP port (`mutable.dhtPort: 0`). That is enough to publish: a put is
429
+ outbound — find nodes, then send — and the replies come back on the same
430
+ socket the way any UDP client's do, which NAT handles without help.
431
+
432
+ Setting a fixed port and forwarding it makes this a *reachable* DHT node, which
433
+ means better lookups and contributing back to the network. Worth doing if this
434
+ node is long-lived, but nothing here requires it.
435
+
436
+ **Do not reuse the libtorrent engine's port.** That engine runs a DHT of its own
437
+ on `libtorrent.listen`, and two sockets cannot hold one port — the node would
438
+ fail to start.
439
+
440
+ The port in use is reported at startup:
441
+
442
+ ```
443
+ [mutable] DHT on UDP 63213
444
+ ```
445
+
446
+ ### Confirming it works
447
+
448
+ ```sh
449
+ journalctl -u pmtiles-swarm | grep mutable
450
+ ```
451
+
452
+ A healthy publisher says which categories it is announcing and under which key
453
+ at startup, then one line per category whenever a build moves:
454
+
455
+ ```
456
+ [mutable] publishing 3 categories as 7680dc95248eb807… every 30m
457
+ [mutable] openmaptiles -> 4813a0e68e4b (seq 1786108931, 8 nodes)
458
+ ```
459
+
460
+ `nodes` is how many DHT peers stored the record. **Zero means nobody did**, and
461
+ the record does not exist however healthy the log looks otherwise — check that
462
+ UDP is not blocked and that the DHT is reachable.
463
+
464
+ The first publish waits about fifteen seconds after start, because a put into a
465
+ DHT that has not finished bootstrapping reaches nobody.
466
+
357
467
  ## The sidecar
358
468
 
359
469
  The libtorrent engine runs Python as a child process, so the service user needs
@@ -373,16 +483,33 @@ rather than exiting. Name the interpreter explicitly when the service user's
373
483
 
374
484
  ## Ports
375
485
 
376
- Four listeners, and only the peer ports want a firewall rule. See
486
+ Five listeners, and only the peer ports want a firewall rule. See
377
487
  [ports and reachability](engines.md#ports-and-reachability) for the detail.
378
488
 
379
489
  | | |
380
490
  | --- | --- |
381
491
  | `libtorrent.listen` — 6881, TCP and UDP | forward it |
382
492
  | `webtorrent.clientOptions.torrentPort` — pin it, or it changes every start | forward it |
493
+ | `mutable.dhtPort` — UDP, ephemeral by default | optional; see below |
383
494
  | `port` — 8090 | your proxy or CDN |
384
495
  | `adminPort` — 8091, bound to `127.0.0.1` | nothing; that is the point |
385
496
 
497
+ `mutable.dhtPort` is the odd one. Publishing works without any forward, because
498
+ a put is outbound and the replies come back on the same socket the way any UDP
499
+ client's do. Pin it and forward it only if you want this to be a *reachable*
500
+ DHT node — which earns a better routing table and contributes back, and is
501
+ worth having on a node that runs continuously.
502
+
503
+ Pin it to something free: **not** `libtorrent.listen`, which is a DHT of its
504
+ own, and not `torrentPort`. `6883` sits clear of both.
505
+
506
+ ```json
507
+ { "mutable": { "dhtPort": 6883 } }
508
+ ```
509
+
510
+ Note that each engine runs its own DHT as well, so a node with both engines has
511
+ three UDP participants. Only this one is yours to place.
512
+
386
513
  ## Updating
387
514
 
388
515
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "BitTorrent distribution for PMTiles map archives: create torrents, watch folders, publish and subscribe to RSS feeds, and seed through qBittorrent or an embedded client",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/config.js CHANGED
@@ -425,6 +425,19 @@ const DEFAULTS = {
425
425
  keyPath: undefined,
426
426
  /** How often to republish, in seconds. */
427
427
  republishSeconds: 1800,
428
+ /**
429
+ * UDP port for this node's DHT socket. 0 takes an ephemeral one.
430
+ *
431
+ * Publishing does not need a forwarded port: a put is outbound, and the
432
+ * replies come back on the same socket the way any UDP client's do, which
433
+ * NAT handles. Setting a fixed port and forwarding it makes this a
434
+ * reachable DHT node, which gives better lookups and contributes back —
435
+ * but nothing here requires it.
436
+ *
437
+ * Do not reuse the libtorrent engine's port. That engine runs a DHT of its
438
+ * own, and two sockets cannot hold one port.
439
+ */
440
+ dhtPort: 0,
428
441
  },
429
442
  /**
430
443
  * What this node has served, at `GET /api/stats`.
package/src/index.js CHANGED
@@ -273,6 +273,15 @@ PMTILES_SWARM_PUBLIC_URL
273
273
  ]);
274
274
  const pem = await fs.readFile(config.mutable.keyPath, 'utf8');
275
275
  const dht = new DHT();
276
+ // Bound explicitly rather than left to bind implicitly on first send,
277
+ // so the port is predictable and can be forwarded if you want this to
278
+ // be a reachable DHT node. 0 takes an ephemeral one, which is all
279
+ // publishing needs.
280
+ await new Promise((resolve, reject) => {
281
+ dht.once('error', reject);
282
+ dht.listen(config.mutable.dhtPort ?? 0, resolve);
283
+ });
284
+ console.log(`[mutable] DHT on UDP ${dht.address().port}`);
276
285
  publisher = new MutablePublisher({
277
286
  catalog,
278
287
  dht,
package/src/publisher.js CHANGED
@@ -21,6 +21,30 @@
21
21
  * so republishing on a timer is not an optimisation here — it is the feature.
22
22
  */
23
23
 
24
+ /*
25
+ * Why this uses bittorrent-dht rather than an engine's own DHT.
26
+ *
27
+ * Both seeding engines run a DHT already, so a third one looks redundant.
28
+ * It is not, for two different reasons:
29
+ *
30
+ * libtorrent - its DHT lives in the Python sidecar, and the 2.x Python
31
+ * bindings do not expose dht_put_item or dht_get_item at all.
32
+ * The alerts are bound (dht_mutable_item_alert, dht_put_alert)
33
+ * so the C++ side supports BEP 44, but there is no method to
34
+ * start one. Checked against 2.0.13; worth re-checking if a
35
+ * later binding adds them, because a sidecar op would then be
36
+ * the tidier answer for a libtorrent-only node.
37
+ *
38
+ * webtorrent - its DHT *is* bittorrent-dht, reachable as `client.dht`, so
39
+ * reusing it is possible and would save a socket. Not done,
40
+ * to keep one code path that behaves the same whichever
41
+ * engine is configured.
42
+ *
43
+ * The dependency itself costs nothing: webtorrent already depends on the same
44
+ * version, and npm dedupes them to one install. Declaring it directly only
45
+ * removes the reliance on a transitive.
46
+ */
47
+
24
48
  import { mutableMagnet, publishInfoHash } from './mutable.js';
25
49
 
26
50
  /** Republish well inside the ~2h a DHT keeps an item. */