feat(mdns): announce this site's local presence for agent local-discovery
The announcer half of AGENT_LOCAL_DISCOVERY_SPEC.md / MULTI_SITE_SPEC.md Appendix B -- advertises which public hostnames this site fronts (and at what local IP) via mDNS, so a theta-agent on the same LAN segment with prefer_local_directory enabled can skip the relay/WAN path. Opt-in via THETA_LOCAL_DISCOVERY_HOSTS (comma-separated); no-op if unset, so this changes nothing for an install that doesn't configure it. Only ever advertises which hostnames map to which local IP -- no identity/ trust information -- consistent with the hard rule on the listening side (theta-agent) that local-discovery may change DNS resolution but must never touch certificate validation. Verified end-to-end against the real theta-agent Go binary: announce, discover, apply, and clean revert on disappearance all confirmed working over real multicast between two containers.
This commit is contained in:
@@ -19,6 +19,8 @@ require('@simpleworkjs/bao-conf').init({ path: 'jump-host', conf }).then(async (
|
||||
require('../models');
|
||||
const { bootstrapWireguard } = require('../services/wg_bootstrap');
|
||||
await bootstrapWireguard();
|
||||
const { startMdnsAnnounce } = require('../services/mdns_announce');
|
||||
await startMdnsAnnounce();
|
||||
|
||||
const app = require('../app');
|
||||
const middleware = require('../middleware/auth');
|
||||
|
||||
Generated
+54
@@ -19,6 +19,7 @@
|
||||
"@simpleworkjs/oidc-client": "^1.0.0",
|
||||
"@simpleworkjs/orm": "^0.2.8",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bonjour-service": "^1.4.4",
|
||||
"bootstrap": "^5.3.8",
|
||||
"compression": "^1.8.1",
|
||||
"ejs": "^3.1.10",
|
||||
@@ -63,6 +64,12 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@leichtgewicht/ip-codec": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
||||
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
@@ -500,6 +507,16 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/bonjour-service": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.4.tgz",
|
||||
"integrity": "sha512-jCZcVv7eoc4QesRscwEZtSROBen+6LpKAmBIsQYQrsAeVHLyMXWX/t6eIV5KiRZYNUBl8eVqImEEMQ8L5+c/Kw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"multicast-dns": "^7.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.8",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
|
||||
@@ -892,6 +909,18 @@
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dns-packet": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
|
||||
"integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@leichtgewicht/ip-codec": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/dottie": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz",
|
||||
@@ -1170,6 +1199,12 @@
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
@@ -1788,6 +1823,19 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/multicast-dns": {
|
||||
"version": "7.2.5",
|
||||
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
|
||||
"integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dns-packet": "^5.2.2",
|
||||
"thunky": "^1.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"multicast-dns": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
@@ -2898,6 +2946,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/thunky": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@simpleworkjs/oidc-client": "^1.0.0",
|
||||
"@simpleworkjs/orm": "^0.2.8",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bonjour-service": "^1.4.4",
|
||||
"bootstrap": "^5.3.8",
|
||||
"compression": "^1.8.1",
|
||||
"ejs": "^3.1.10",
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
// mDNS local-discovery announcer (MULTI_SITE_SPEC.md Appendix B). Advertises
|
||||
// this site's local presence so an agent on the same LAN segment (with
|
||||
// prefer_local_directory enabled -- see theta-agent's local_discovery.go)
|
||||
// can skip the relay/WAN path and talk to the local instance directly.
|
||||
//
|
||||
// What gets announced is deliberately just "which public hostnames does
|
||||
// this site front, and at what local IP" -- nothing about identity or
|
||||
// trust. The listening side never weakens certificate validation based on
|
||||
// this; it only ever changes DNS resolution (see the hard rule documented
|
||||
// in theta-agent's local_discovery.go).
|
||||
|
||||
const SERVICE_TYPE = 'theta-suite'; // -> _theta-suite._tcp, matches theta-agent's mdnsServiceName
|
||||
|
||||
function announcedHosts() {
|
||||
return (process.env.THETA_LOCAL_DISCOVERY_HOSTS || '')
|
||||
.split(',')
|
||||
.map((h) => h.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
let bonjourInstance = null;
|
||||
let publishedService = null;
|
||||
|
||||
async function startMdnsAnnounce() {
|
||||
const hosts = announcedHosts();
|
||||
if (hosts.length === 0) {
|
||||
console.log('[mdns-announce] THETA_LOCAL_DISCOVERY_HOSTS not set -- nothing to announce, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const { Bonjour } = require('bonjour-service');
|
||||
bonjourInstance = new Bonjour(undefined, (err) => {
|
||||
console.error('[mdns-announce] bonjour-service error:', err.message);
|
||||
});
|
||||
|
||||
publishedService = bonjourInstance.publish({
|
||||
name: `theta-suite-${process.env.SITE_SLUG || 'site'}`,
|
||||
type: SERVICE_TYPE,
|
||||
port: Number(process.env.PORT) || 80,
|
||||
txt: { hosts: hosts.join(','), site: process.env.SITE_SLUG || '' }
|
||||
});
|
||||
|
||||
console.log(`[mdns-announce] announcing on the local network: ${hosts.join(', ')}`);
|
||||
}
|
||||
|
||||
function stopMdnsAnnounce() {
|
||||
if (bonjourInstance) {
|
||||
bonjourInstance.destroy();
|
||||
bonjourInstance = null;
|
||||
publishedService = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { startMdnsAnnounce, stopMdnsAnnounce };
|
||||
Reference in New Issue
Block a user