FN-8202: prevent mDNS hostname conflicts
Prevent self-conflicting mDNS advertisements and allow automatic LAN discovery to be disabled. - Advertise a Fusion-owned DNS-SD hostname derived from the node ID. - Add a global automatic LAN discovery opt-out for dashboard and serve. - Cover hostname isolation and discovery startup behavior with tests and documentation. Files changed: .changeset/fn-8202-mdns-hostname-fix.md | 7 +++++ docs/settings-reference.md | 1 + docs/shared-mesh-protocol.md | 2 +- .../cli/src/commands/__tests__/dashboard.test.ts | 24 ++++++++++++++++ packages/cli/src/commands/__tests__/serve.test.ts | 12 ++++++++ packages/cli/src/commands/dashboard.ts | 24 +++++++++++----- packages/cli/src/commands/serve.ts | 24 +++++++++++----- packages/core/src/__tests__/node-discovery.test.ts | 32 +++++++++++++++++++++- .../core/src/__tests__/settings-defaults.test.ts | 8 ++++++ packages/core/src/node-discovery.ts | 31 +++++++++++++++++++++ packages/core/src/settings-schema.ts | 5 ++++ packages/core/src/types.ts | 2 ++ 12 files changed, 156 insertions(+), 16 deletions(-) Fusion-Task-Id: FN-8202 Fusion-Task-Lineage: c1d6e36b-bda7-4968-b40c-c14e16ebda28 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -3002,6 +3002,30 @@ describe("runDashboard — mesh lifecycle ownership", () => {
|
||||
dispose();
|
||||
});
|
||||
|
||||
it("skips automatic discovery when local network discovery is disabled", async () => {
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const startDiscovery = vi.fn().mockResolvedValue(undefined);
|
||||
mockGlobalSettingsGetSettings.mockResolvedValue({ localNetworkDiscoveryEnabled: false });
|
||||
|
||||
(CentralCore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
getProjectByPath: vi.fn().mockResolvedValue({ id: "project-1" }),
|
||||
listProjects: vi.fn().mockResolvedValue([{ id: "project-1", path: process.cwd() }]),
|
||||
listNodes: vi.fn().mockResolvedValue([{ id: "node-local", type: "local", status: "offline" }]),
|
||||
updateNode: vi.fn().mockResolvedValue(undefined),
|
||||
startDiscovery,
|
||||
stopDiscovery: vi.fn(),
|
||||
}));
|
||||
|
||||
const { dispose } = await runDashboard(0, { open: false });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(startDiscovery).not.toHaveBeenCalled();
|
||||
dispose();
|
||||
});
|
||||
|
||||
it("stops peer exchange and discovery during shutdown", async () => {
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const { PeerExchangeService } = await import("@fusion/engine");
|
||||
|
||||
@@ -1786,6 +1786,18 @@ describe("runServe — Peer exchange and discovery", () => {
|
||||
await triggerSignal("SIGINT");
|
||||
});
|
||||
|
||||
it("skips automatic discovery when local network discovery is disabled", async () => {
|
||||
mocks.globalSettingsGetSettings.mockResolvedValue({ localNetworkDiscoveryEnabled: false });
|
||||
|
||||
await runServe(4040, {});
|
||||
|
||||
const nodeCentral = mocks.centralInstances.find((instance) => instance.listNodes.mock.calls.length > 0);
|
||||
expect(nodeCentral).toBeDefined();
|
||||
expect(nodeCentral.startDiscovery).not.toHaveBeenCalled();
|
||||
|
||||
await triggerSignal("SIGINT");
|
||||
});
|
||||
|
||||
it("starts discovery with port 5050 when port 0 is requested", async () => {
|
||||
await runServe(0, {});
|
||||
|
||||
|
||||
@@ -2738,13 +2738,23 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
//
|
||||
if (centralCoreForMesh) {
|
||||
try {
|
||||
await centralCoreForMesh.startDiscovery({
|
||||
broadcast: true,
|
||||
listen: true,
|
||||
serviceType: "_fusion._tcp",
|
||||
port: actualPort,
|
||||
staleTimeoutMs: 300_000,
|
||||
});
|
||||
const globalSettings = await store.getGlobalSettingsStore().getSettings();
|
||||
/*
|
||||
* FNXC:NodeDiscovery 2026-07-17-12:00:
|
||||
* FN-8202 requires dashboard boot to respect the global LAN discovery
|
||||
* opt-out. The explicit discovery API remains an operator override.
|
||||
*/
|
||||
if (globalSettings.localNetworkDiscoveryEnabled === false) {
|
||||
logSink.warn("LAN discovery disabled by localNetworkDiscoveryEnabled setting", "dashboard");
|
||||
} else {
|
||||
await centralCoreForMesh.startDiscovery({
|
||||
broadcast: true,
|
||||
listen: true,
|
||||
serviceType: "_fusion._tcp",
|
||||
port: actualPort,
|
||||
staleTimeoutMs: 300_000,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
logSink.warn(`Failed to start mDNS discovery: ${message}`, "dashboard");
|
||||
|
||||
@@ -1075,13 +1075,23 @@ export async function runServe(
|
||||
//
|
||||
if (sharedCentralCore) {
|
||||
try {
|
||||
await sharedCentralCore.startDiscovery({
|
||||
broadcast: true,
|
||||
listen: true,
|
||||
serviceType: "_fusion._tcp",
|
||||
port: actualPort,
|
||||
staleTimeoutMs: 300_000,
|
||||
});
|
||||
const globalSettings = await store.getGlobalSettingsStore().getSettings();
|
||||
/*
|
||||
* FNXC:NodeDiscovery 2026-07-17-12:00:
|
||||
* FN-8202 makes automatic LAN discovery opt-outable at daemon boot;
|
||||
* explicit POST /api/discovery/start requests intentionally still work.
|
||||
*/
|
||||
if (globalSettings.localNetworkDiscoveryEnabled === false) {
|
||||
console.warn("[serve] LAN discovery disabled by localNetworkDiscoveryEnabled setting");
|
||||
} else {
|
||||
await sharedCentralCore.startDiscovery({
|
||||
broadcast: true,
|
||||
listen: true,
|
||||
serviceType: "_fusion._tcp",
|
||||
port: actualPort,
|
||||
staleTimeoutMs: 300_000,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.warn(`[serve] Failed to start mDNS discovery: ${message}`);
|
||||
|
||||
Reference in New Issue
Block a user