diff --git a/nodejs/bin/www b/nodejs/bin/www index 3b91099..1923c65 100644 --- a/nodejs/bin/www +++ b/nodejs/bin/www @@ -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'); diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 0e8c84f..ff7c0ef 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -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", diff --git a/nodejs/package.json b/nodejs/package.json index fe5f4e8..ef13812 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -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", diff --git a/nodejs/services/mdns_announce.js b/nodejs/services/mdns_announce.js new file mode 100644 index 0000000..934701f --- /dev/null +++ b/nodejs/services/mdns_announce.js @@ -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 };