1import type * as NodeOS from "node:os";
2
3import { describe, expect, it } from "@effect/vitest";
4import * as NetAddress from "effect/net/NetAddress";
5
6import {
7 acceptsPrivateNetworkConnection,
8 loopbackUrlHostForAddress,
9 resolveHeadlessConnectionString,
10} from "./startupAccess.ts";
11
12const ipv4 = (address: string, internal = false): NodeOS.NetworkInterfaceInfo => ({
13 address,
14 netmask: "255.255.255.0",
15 family: "IPv4",
16 mac: "00:00:00:00:00:00",
17 internal,
18 cidr: `${address}/24`,
19});
20
21it("resolves wildcard hosts to a concrete external interface when one is available", () => {
22 const connectionString = resolveHeadlessConnectionString("0.0.0.0", 3773, {
23 interfaces: { en0: [ipv4("192.168.1.42")], lo0: [ipv4("127.0.0.1", true)] },
24 });
25
26 expect(connectionString).toBe("http://192.168.1.42:3773");
27});
28
29it("skips public and container addresses for a private-network listener", () => {
30 const interfaces = {
31 eth0: [ipv4("203.0.113.7")],
32 docker0: [ipv4("172.17.0.1")],
33 bridge200: [ipv4("192.168.64.1")],
34 eth1: [ipv4("10.0.0.5")],
35 };
36
37 expect(resolveHeadlessConnectionString("0.0.0.0", 3773, { interfaces })).toBe(
38 "http://203.0.113.7:3773",
39 );
40 expect(
41 resolveHeadlessConnectionString("0.0.0.0", 3773, { interfaces, privateNetworkOnly: true }),
42 ).toBe("http://10.0.0.5:3773");
43 expect(
44 resolveHeadlessConnectionString("0.0.0.0", 3773, {
45 interfaces: { eth0: [ipv4("203.0.113.7")] },
46 privateNetworkOnly: true,
47 }),
48 ).toBe("http://localhost:3773");
49});
50
51it("prefers a LAN address over a tailnet one for a private-network pairing URL", () => {
52 expect(
53 resolveHeadlessConnectionString("0.0.0.0", 3773, {
54 interfaces: { tailscale0: [ipv4("100.101.102.103")], en0: [ipv4("192.168.1.42")] },
55 privateNetworkOnly: true,
56 }),
57 ).toBe("http://192.168.1.42:3773");
58});
59
60it("targets the bound interface when the listener binds a specific address", () => {
61 const hostFor = (ip: string) =>
62 loopbackUrlHostForAddress(NetAddress.inetAddressFromIpStringUnsafe(ip, 3773));
63
64 expect(hostFor("0.0.0.0")).toBe("127.0.0.1");
65 expect(hostFor("::")).toBe("127.0.0.1");
66 expect(hostFor("::1")).toBe("[::1]");
67 expect(hostFor("127.0.0.1")).toBe("127.0.0.1");
68 expect(hostFor("127.0.0.2")).toBe("127.0.0.2");
69 expect(hostFor("100.101.102.103")).toBe("100.101.102.103");
70});
71
72describe("acceptsPrivateNetworkConnection", () => {
73 const accepts = (localAddress: string | undefined, remoteAddress: string | undefined) =>
74 acceptsPrivateNetworkConnection({ localAddress, remoteAddress });
75
76 it("serves loopback, LAN, and tailnet clients", () => {
77 expect(accepts("127.0.0.1", "127.0.0.1")).toBe(true);
78 expect(accepts("::ffff:192.168.1.20", "::ffff:192.168.1.31")).toBe(true);
79 expect(accepts("100.101.102.103", "100.90.1.2")).toBe(true);
80 });
81
82 it("closes connections from or to a public address", () => {
83 // A cloud VM's public address is NATed to a private interface address.
84 expect(accepts("172.31.4.10", "203.0.113.7")).toBe(false);
85 expect(accepts("203.0.113.7", "192.168.1.31")).toBe(false);
86 expect(accepts("169.254.10.1", "169.254.10.2")).toBe(false);
87 expect(accepts(undefined, "192.168.1.31")).toBe(false);
88 });
89});