Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const serverExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExp
}),
configureFromSettings: () => Effect.die("unexpected configureFromSettings"),
setMode: () => Effect.die("unexpected setMode"),
setPreferredLanInterfaceName: () => Effect.die("unexpected preferred LAN interface change"),
setTailscaleServeEnabled: () => Effect.die("unexpected setTailscaleServeEnabled"),
getAdvertisedEndpoints: Effect.succeed([]),
} satisfies DesktopServerExposure.DesktopServerExposure["Service"]);
Expand Down
180 changes: 180 additions & 0 deletions apps/desktop/src/backend/DesktopServerExposure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@ const tailnetNetworkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces = {
],
};

const multiHomedNetworkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces = {
docker0: [
{
address: "172.17.0.1",
family: "IPv4",
internal: false,
},
],
en0: [
{
address: "192.168.1.20",
family: "IPv4",
internal: false,
},
],
en1: [
{
address: "192.168.1.21",
family: "IPv4",
internal: false,
},
],
};

function mockSpawnerLayer(statusJson = "{}") {
return Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
Expand Down Expand Up @@ -195,6 +219,7 @@ describe("DesktopServerExposure", () => {
mode: "network-accessible",
endpointUrl: "http://192.168.1.20:4173",
advertisedHost: "192.168.1.20",
preferredLanInterfaceName: null,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
});
Expand All @@ -209,6 +234,159 @@ describe("DesktopServerExposure", () => {
),
);

it.effect("advertises one endpoint per physical interface and skips virtual bridges", () =>
withHarness(
multiHomedNetworkInterfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* settings.setServerExposureMode("network-accessible");
yield* serverExposure.configureFromSettings({ port: 4173 });

const state = yield* serverExposure.getState;
assert.equal(state.advertisedHost, "192.168.1.20");

const endpoints = yield* serverExposure.getAdvertisedEndpoints;
assert.deepEqual(
endpoints
.filter((endpoint) => endpoint.id.startsWith("desktop-lan:"))
.map((endpoint) => [endpoint.label, endpoint.httpBaseUrl, endpoint.interfaceName]),
[
["Local network", "http://192.168.1.20:4173/", "en0"],
["Local network — en1 (192.168.1.21)", "http://192.168.1.21:4173/", "en1"],
],
);
}),
),
);

it.effect("treats Windows virtual adapters with parentheses as virtual", () =>
withHarness(
{
"vEthernet (Default Switch)": [{ address: "172.25.32.1", family: "IPv4", internal: false }],
...multiHomedNetworkInterfaces,
},
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* settings.setServerExposureMode("network-accessible");
yield* serverExposure.configureFromSettings({ port: 4173 });

const endpoints = yield* serverExposure.getAdvertisedEndpoints;
assert.isFalse(endpoints.some((endpoint) => endpoint.httpBaseUrl.includes("172.25.32.1")));
}),
),
);

it.effect("advertises every physical interface without truncation", () =>
withHarness(
Object.fromEntries(
Array.from({ length: 10 }, (_, index) => [
`en${index}`,
[{ address: `10.0.0.${index + 2}`, family: "IPv4", internal: false }],
]),
),
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* settings.setServerExposureMode("network-accessible");
yield* serverExposure.configureFromSettings({ port: 4173 });

const endpoints = yield* serverExposure.getAdvertisedEndpoints;
const lanEndpoints = endpoints.filter((endpoint) => endpoint.id.startsWith("desktop-lan:"));
assert.equal(lanEndpoints.length, 10);
}),
),
);

it.effect("re-resolves the default endpoint when interfaces change after startup", () => {
const interfaces: Record<string, DesktopNetworkInterfaces.NetworkInterfaces[string]> = {
...multiHomedNetworkInterfaces,
};
return withHarness(
interfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* settings.setServerExposureMode("network-accessible");
yield* serverExposure.configureFromSettings({ port: 4173 });

const preferred = yield* serverExposure.setPreferredLanInterfaceName({ name: "en0" });
assert.equal(preferred.state.advertisedHost, "192.168.1.20");

// The harness network service returns the fixture object by
// reference, so removing en0 here simulates it disappearing after
// the exposure state was resolved at startup.
delete interfaces.en0;
const endpoints = yield* serverExposure.getAdvertisedEndpoints;
const defaultEndpoint = endpoints.find(
(endpoint) => endpoint.id.startsWith("desktop-lan:") && endpoint.isDefault === true,
);
assert.equal(defaultEndpoint?.httpBaseUrl, "http://192.168.1.21:4173/");
assert.isFalse(endpoints.some((endpoint) => endpoint.httpBaseUrl.includes("192.168.1.20")));
const refreshed = yield* serverExposure.getState;
assert.equal(refreshed.advertisedHost, "192.168.1.21");
}),
);
});

it.effect("downgrades to loopback when only virtual interfaces exist", () =>
withHarness(
{
docker0: [{ address: "172.17.0.1", family: "IPv4", internal: false }],
},
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* settings.setServerExposureMode("network-accessible");
yield* serverExposure.configureFromSettings({ port: 4173 });

const state = yield* serverExposure.getState;
assert.equal(state.mode, "local-only");
assert.equal(state.endpointUrl, null);
}),
),
);

it.effect("honors a preferred LAN interface", () =>
withHarness(
multiHomedNetworkInterfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* settings.setServerExposureMode("network-accessible");
yield* serverExposure.configureFromSettings({ port: 4173 });
const change = yield* serverExposure.setPreferredLanInterfaceName({ name: "en1" });

assert.equal(change.state.advertisedHost, "192.168.1.21");
assert.equal(change.state.preferredLanInterfaceName, "en1");
assert.equal(change.requiresRelaunch, false);

const endpoints = yield* serverExposure.getAdvertisedEndpoints;
const defaultEndpoint = endpoints.find(
(endpoint) => endpoint.id.startsWith("desktop-lan:") && endpoint.isDefault === true,
);
assert.equal(defaultEndpoint?.httpBaseUrl, "http://192.168.1.21:4173/");
}),
),
);

it.effect("falls back to automatic when the preferred interface disappears", () =>
withHarness(
multiHomedNetworkInterfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* settings.setServerExposureMode("network-accessible");
yield* serverExposure.configureFromSettings({ port: 4173 });
const change = yield* serverExposure.setPreferredLanInterfaceName({ name: "en9" });

assert.equal(change.state.preferredLanInterfaceName, "en9");
assert.equal(change.state.advertisedHost, "192.168.1.20");
}),
),
);

it.effect("persists tailscale serve preferences atomically and reports no-op updates", () =>
withHarness(
emptyNetworkInterfaces,
Expand Down Expand Up @@ -252,6 +430,7 @@ describe("DesktopServerExposure", () => {
load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
setMainWindowBounds: () => Effect.die("unexpected main window bounds update"),
setServerExposureMode: () => Effect.fail(settingsFailure),
setPreferredLanInterfaceName: () => Effect.fail(settingsFailure),
setTailscaleServe: () => Effect.fail(settingsFailure),
setUpdateChannel: () => Effect.die("unexpected update channel change"),
setWslBackendEnabled: () => Effect.die("unexpected WSL backend toggle"),
Expand Down Expand Up @@ -425,6 +604,7 @@ describe("DesktopServerExposure", () => {
{
id: "desktop-lan:http://192.168.1.20:3773",
label: "Local network",
interfaceName: "en0",
provider: {
id: "desktop-core",
label: "Desktop",
Expand Down
Loading
Loading