@quolu/lattice 0.12.28 → 0.12.30

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/README.md CHANGED
@@ -119,7 +119,8 @@ dashboardはmanifestのfile identityが変わらない間のstable store readを
119
119
  最近のsession activityが期限切れでも、Lattice storeの`active_set`が非空なprojectは一覧へ残ります。
120
120
  長時間の外部処理中にCLI呼出しが途切れても進行中projectを休眠扱いしません。
121
121
  LANや外部reverse proxyから閲覧するoptional bridgeは既定で無効です。明示したIPにだけbindする初回設定、
122
- 再設定、停止方法は[bridge setup](docs/bridge-setup.md)を参照してください。
122
+ 再設定、停止方法は[bridge setup](docs/bridge-setup.md)を参照してください。reverse proxy hostへsshで
123
+ 到達できる場合は、LANへbindせずloopbackだけを逆トンネルで公開する構成も選べます。
123
124
  工程図の既定表示は、後続に作業中・未着手が残っていない完了工程を図から除きます。まとめnodeも置かないため、
124
125
  完走したplanは図の場所を取りません。除いた工程は凡例の件数、右ペインの「全工程」一覧、各工程の詳細から
125
126
  辿れ、詳細の前提・後続は除外前の依存関係を示します。総数・進捗・最長依存鎖は除外前の全工程で数えます。
@@ -10,7 +10,7 @@ portを省略するか`auto`にすると、49152–65535から候補を重複な
10
10
  実際のexclusive bindとhealth確認に成功したportだけを保存する。
11
11
 
12
12
  ```bash
13
- lattice bridge setup --listen 192.168.1.102 --port auto --dashboard --allow-host lattice.kitepon.dev --json
13
+ lattice bridge setup --listen 192.168.1.50 --port auto --dashboard --allow-host lattice.example.com --json
14
14
  ```
15
15
 
16
16
  `--dashboard`は現在のlocal dashboard descriptorをrequestごとに解決するため、dashboard再起動でportが変わっても
@@ -20,7 +20,7 @@ listen IPは常に許可Hostへ入り、reverse proxyで公開するhostnameは`
20
20
 
21
21
  ```bash
22
22
  lattice bridge status --json
23
- lattice bridge reconfigure --listen 192.168.1.102 --port auto --dashboard --json
23
+ lattice bridge reconfigure --listen 192.168.1.50 --port auto --dashboard --json
24
24
  lattice bridge disable --json
25
25
  ```
26
26
 
@@ -33,6 +33,38 @@ fileを除去し、JSON結果の`recovery`へ処置を明示する。その後
33
33
  自動化・隔離testではabsoluteな`LATTICE_CONFIG_DIR`で設定rootを変更できる。無効な設定、低いport、
34
34
  使用中の明示port、危険なrequest target、到達不能upstreamはsilent fallbackせずtyped errorを返す。
35
35
 
36
+ ## listen IPがDHCPで動く場合
37
+
38
+ 設定したlisten IPがホストから消えると、古いsocketは死んだアドレスへ取り残され、LANから到達できなくなる。
39
+ daemonは各reconcileで実効アドレスを解決し直し、同一subnet(IPv4 /24、IPv6 /64)に生きたアドレスがあれば
40
+ そこへbindし直す。VPNや別NICなど異なるnetworkのアドレスは採用せず、候補が無ければ
41
+ `BRIDGE_LISTEN_ADDRESS_ABSENT`で公開socketをfail-closedにする。再bind先は許可Hostへ自動で加わる。
42
+
43
+ `LATTICE_BRIDGE_REGISTRAR_SSH_HOST`と`LATTICE_BRIDGE_REGISTRAR_SCRIPT`を両方設定すると、新しいbindingを
44
+ 張るたびに`ssh <host> <script> <port>`でreverse proxy hostへ自己登録する。アドレスは送らず、remote側が
45
+ ssh送信元から決めるため、各hostは自分自身しか登録できない。登録の失敗はbridgeを落とさずstderrへ
46
+ typedに報告する。この配線が無いと、Caddy等が持つリテラルはlease変更のたびに黙って陳腐化する。
47
+
48
+ ## reverse proxyへ逆トンネルで繋ぐ(LAN bindを使わない)
49
+
50
+ reverse proxy hostへsshで到達できるなら、LANへbindせずloopbackだけで公開できる。bridgeが動くhostから
51
+ 接続しに行くため、そのhostのLAN addressはreverse proxyのどこにも現れず、追従も自己登録も不要になる。
52
+
53
+ ```bash
54
+ lattice bridge setup --listen 127.0.0.1 --port 53939 --dashboard --allow-host lattice.example.com --json
55
+ ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 \
56
+ -R 172.18.0.1:53939:127.0.0.1:53939 proxy-host
57
+ ```
58
+
59
+ reverse proxyはこの固定endpointだけを見る。転送口のbind先は、reverse proxyが到達できるaddressにする。
60
+ Docker上のreverse proxyでは、containerの`127.0.0.1`はcontainer自身のloopbackでありhostのそれではないため、
61
+ hostのloopbackへ開いた口には届かない。対象networkのgateway(`docker network inspect`の`Gateway`)へbindする。
62
+
63
+ sshdは既定の`GatewayPorts no`だと`127.0.0.1`にしかbindできない。`clientspecified`にすると、clientが明示した
64
+ addressだけにbindする(`yes`と違い全interfaceへは晒さない)。host firewallがINPUTをDROPしている場合は、
65
+ その1 portだけを許可する。`ExitOnForwardFailure=yes`は、転送口を開けないまま接続だけ生かす状態を防ぐ。
66
+ 常駐はprocess supervisorのKeepAliveに任せ、切断時は張り直す。
67
+
36
68
  ## Docker Caddy/Cloudflare Tunnelへ接続する
37
69
 
38
70
  bridgeを有効化したMacとreverse proxy hostの間で、まず許可Hostを付けたLAN到達を確認する。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.12.28",
3
+ "version": "0.12.30",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -308,6 +308,7 @@ export function bridgeRuntimeController({
308
308
  env = process.env, instanceToken = null,
309
309
  register = registerBridgeUpstream,
310
310
  report = (line) => process.stderr.write(`${line}\n`),
311
+ readInterfaces = () => networkInterfaces(),
311
312
  } = {}) {
312
313
  let active = null;
313
314
  let fingerprint = null;
@@ -329,23 +330,36 @@ export function bridgeRuntimeController({
329
330
  async reconcile() {
330
331
  const config = await readBridgeConfig({ env });
331
332
  const next = config === null || !config.enabled ? null : JSON.stringify(config);
332
- if (next === fingerprint) return active;
333
+ // The config is not the only thing that can change under a live binding.
334
+ // A DHCP lease change removes the bound address from the host without
335
+ // touching a byte of config, and the socket lingers on an address nothing
336
+ // can route to. Resolve the effective address on every pass, from the
337
+ // interfaces as they are now, so the binding follows the host rather than
338
+ // only the file. One snapshot serves both the comparison and the bind, so
339
+ // a lease that changes mid-reconcile cannot land them on different
340
+ // addresses.
341
+ const interfaces = next === null ? null : readInterfaces();
342
+ const effective = next === null ? null
343
+ : resolveBridgeListenAddress({ configured: config.listen.address, interfaces }).effective;
344
+ if (next === fingerprint && (active === null || active.address === effective)) return active;
333
345
  if (next === null) {
334
346
  await active?.close();
335
347
  active = null;
336
348
  fingerprint = null;
337
349
  return null;
338
350
  }
339
- // Compare against the CONFIGURED address: a rebound binding still serves
340
- // the same configuration, and comparing the effective address would tear
341
- // the server down and rebuild it on every reconcile.
351
+ // The binding is reused while it still serves the same configuration AND
352
+ // still sits on the address that configuration resolves to today. The
353
+ // effective address is stable for an unchanged host, so this compares
354
+ // equal on every quiet pass and only forces a rebind when the ground has
355
+ // actually moved.
342
356
  if (active !== null && active.configured_address === config.listen.address
343
- && active.port === config.listen.port) {
357
+ && active.port === config.listen.port && active.address === effective) {
344
358
  active.updateConfig(config);
345
359
  fingerprint = next;
346
360
  return active;
347
361
  }
348
- const replacement = await startBridgeServer({ config, env, instanceToken });
362
+ const replacement = await startBridgeServer({ config, env, instanceToken, interfaces });
349
363
  const previous = active;
350
364
  active = replacement;
351
365
  fingerprint = next;