import * as NodeServices from "@effect/platform-node/NodeServices"; import { it as effectIt } from "@effect/vitest"; import type * as NodeOS from "node:os"; import * as Effect from "effect/Effect"; import * as Deferred from "effect/Deferred"; import * as Fiber from "effect/Fiber"; import * as Ref from "effect/Ref"; import * as Layer from "effect/Layer"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import * as HttpClient from "effect/http/HttpClient"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; import { describe, expect, it } from "vite-plus/test"; import * as ServerConfig from "../config.ts"; import * as DirectEndpoints from "./DirectEndpoints.ts"; import { resolveBoundEndpoints } from "./DirectEndpoints.ts"; const INTERFACES: ReturnType = { lo0: [ { address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8", }, ], en0: [ { address: "192.168.1.10", netmask: "255.255.255.0", family: "IPv4", mac: "aa:bb:cc:dd:ee:ff", internal: false, cidr: "192.168.1.10/24", }, { address: "fe80::1", netmask: "ffff:ffff:ffff:ffff::", family: "IPv6", mac: "aa:bb:cc:dd:ee:ff", internal: false, cidr: "fe80::1/64", scopeid: 4, }, ], en1: [ { address: "203.0.113.20", netmask: "255.255.255.0", family: "IPv4", mac: "aa:bb:cc:dd:ee:00", internal: false, cidr: "203.0.113.20/24", }, ], utun4: [ { address: "100.101.102.103", netmask: "255.255.255.255", family: "IPv4", mac: "00:00:00:00:00:00", internal: false, cidr: "100.101.102.103/32", }, { address: "fd7a:115c:a1e0::1", netmask: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", family: "IPv6", mac: "00:00:00:00:00:00", internal: false, cidr: "fd7a:115c:a1e0::1/128", scopeid: 0, }, ], }; const vpnInterface = (address: string) => [ { address, netmask: "255.255.255.255", family: "IPv4" as const, mac: "00:00:00:00:00:00", internal: false, cidr: `${address}/32`, }, ]; const virtualInterface = (address: string, mac = "02:42:ac:11:00:01") => [ { address, netmask: "255.255.0.0", family: "IPv4" as const, mac, internal: false, cidr: `${address}/16`, }, ]; describe("resolveBoundEndpoints", () => { it("lists every external IPv4 address for a wildcard bind, tagging the tailnet one", () => { expect( resolveBoundEndpoints({ platform: "linux", host: "0.0.0.0", port: 3773, interfaces: INTERFACES, }), ).toEqual([ { kind: "lan", httpBaseUrl: "http://192.168.1.10:3773/" }, { kind: "tailnet", httpBaseUrl: "http://100.101.102.103:3773/" }, ]); }); it("skips container and VM networks, which only this machine reaches", () => { const interfaces = { ...INTERFACES, docker0: virtualInterface("172.17.0.1"), "br-3f2a1b": virtualInterface("172.18.0.1"), virbr0: virtualInterface("192.168.122.1"), "vEthernet (WSL)": virtualInterface("172.24.0.1"), bridge100: virtualInterface("192.168.64.1"), vmbr0: virtualInterface("192.168.1.20"), }; expect( resolveBoundEndpoints({ platform: "linux", host: "0.0.0.0", port: 3773, interfaces }), ).toEqual([ { kind: "lan", httpBaseUrl: "http://192.168.1.10:3773/" }, { kind: "tailnet", httpBaseUrl: "http://100.101.102.103:3773/" }, { kind: "lan", httpBaseUrl: "http://192.168.1.20:3773/" }, ]); }); it("omits Windows host-only adapters without excluding guest NICs or explicit binds", () => { const interfaces = { ...INTERFACES, "VirtualBox-Host-Only-Netzwerk": virtualInterface("192.168.56.1", "0A:00:27:00:01:01"), "Ethernet 2": virtualInterface("192.168.150.1", "00:50:56:c0:00:01"), "Renamed network": virtualInterface("192.168.151.1", "00:50:56:c0:00:08"), guest1: virtualInterface("192.168.1.30", "08:00:27:5e:1a:22"), guest2: virtualInterface("192.168.1.31", "00:50:56:2b:8c:01"), guest3: virtualInterface("192.168.1.32", "00:0c:29:4f:8e:35"), guest4: virtualInterface("192.168.1.33", "00:50:56:80:8c:01"), }; const input = { host: "0.0.0.0", port: 3773, interfaces }; const retained = [ { kind: "lan", httpBaseUrl: "http://192.168.1.10:3773/" }, { kind: "tailnet", httpBaseUrl: "http://100.101.102.103:3773/" }, ...[30, 31, 32, 33].map((last) => ({ kind: "lan", httpBaseUrl: `http://192.168.1.${last}:3773/`, })), ]; expect(resolveBoundEndpoints({ ...input, platform: "win32" })).toEqual(retained); for (const platform of ["linux", "darwin"] as const) { expect(resolveBoundEndpoints({ ...input, platform })).toHaveLength(retained.length + 3); } for (const host of ["192.168.56.1", "192.168.150.1", "192.168.151.1"]) { expect(resolveBoundEndpoints({ ...input, platform: "win32", host })).toEqual([ { kind: "lan", httpBaseUrl: `http://${host}:3773/` }, ]); } }); it("tags only addresses on Tailscale's interface as tailnet, not other VPNs in its range", () => { const interfaces = { en0: INTERFACES.en0, tailscale0: vpnInterface("100.70.1.2"), CloudflareWARP: vpnInterface("100.96.0.1"), utun7: vpnInterface("100.85.0.1"), }; expect( resolveBoundEndpoints({ platform: "linux", host: "0.0.0.0", port: 3773, interfaces }), ).toEqual([ { kind: "lan", httpBaseUrl: "http://192.168.1.10:3773/" }, { kind: "tailnet", httpBaseUrl: "http://100.70.1.2:3773/" }, { kind: "lan", httpBaseUrl: "http://100.96.0.1:3773/" }, { kind: "lan", httpBaseUrl: "http://100.85.0.1:3773/" }, ]); }); it("never reports a host name, which can resolve to another machine per client", () => { for (const host of ["server.local", "devbox", "devbox.home.arpa"]) { expect( resolveBoundEndpoints({ platform: "linux", host, port: 3773, interfaces: INTERFACES }), ).toEqual([]); } }); it("never reports a public address, which would carry the credential over plain HTTP", () => { expect( resolveBoundEndpoints({ platform: "linux", host: "203.0.113.20", port: 3773, interfaces: INTERFACES, }), ).toEqual([]); }); it("reports only the tailnet address on a cloud VM, whose private network no client shares", () => { expect( resolveBoundEndpoints({ platform: "linux", host: "0.0.0.0", port: 3773, interfaces: INTERFACES, onCloudVm: true, }), ).toEqual([{ kind: "tailnet", httpBaseUrl: "http://100.101.102.103:3773/" }]); }); it("never reports an IPv4-mapped host, which can name the client's own loopback", () => { expect( resolveBoundEndpoints({ platform: "linux", host: "::ffff:127.0.0.1", port: 3773, interfaces: INTERFACES, }), ).toEqual([]); }); }); const TAILSCALE_STATUS_JSON = JSON.stringify({ Self: { DNSName: "bb-1.tail1234.ts.net.", TailscaleIPs: ["100.64.1.2"] }, }); const layerTailscaleUp = Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.succeed( ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(1), exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)), isRunning: Effect.succeed(false), kill: () => Effect.void, unref: Effect.succeed(Effect.void), stdin: Sink.drain, stdout: Stream.make(new TextEncoder().encode(TAILSCALE_STATUS_JSON)), stderr: Stream.empty, all: Stream.empty, getInputFd: () => Sink.drain, getOutputFd: () => Stream.empty, }), ), ), ); const layerServeProbe = (status: number) => Layer.succeed( HttpClient.HttpClient, HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status }))), ), ); const layerServeConfig = (host = "127.0.0.1") => Layer.effect( ServerConfig.ServerConfig, Effect.map(ServerConfig.ServerConfig, (config) => ({ ...config, host, port: 3773, tailscaleServeEnabled: true, tailscaleServePort: 443, })), ).pipe( Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-direct-" })), Layer.provide(NodeServices.layer), ); const resolveWithServe = (probeStatus: number) => Effect.flatMap(DirectEndpoints.DirectEndpoints, (service) => service.resolve()).pipe( Effect.provide( DirectEndpoints.layer.pipe( Layer.provide( Layer.mergeAll(layerServeConfig(), layerTailscaleUp, layerServeProbe(probeStatus)), ), ), ), ); describe("DirectEndpoints Tailscale Serve", () => { effectIt.effect("lists the tailnet name once Serve answers for this server", () => Effect.gen(function* () { expect(yield* resolveWithServe(200)).toEqual([ { kind: "tailnet", httpBaseUrl: "https://bb-1.tail1234.ts.net/" }, ]); }), ); effectIt.effect( "returns bound endpoints during validation and reuses only a completed Serve result", () => Effect.gen(function* () { const status = yield* Ref.make(200); const finishProbe = yield* Deferred.make(); const probeStarted = yield* Deferred.make(); const service = yield* DirectEndpoints.make.pipe( Effect.provide( Layer.mergeAll( layerServeConfig("192.168.1.10"), layerTailscaleUp, Layer.succeed( HttpClient.HttpClient, HttpClient.make((request) => Deferred.succeed(probeStarted, undefined).pipe( Effect.andThen(Deferred.await(finishProbe)), Effect.andThen(Ref.get(status)), Effect.map((status) => HttpClientResponse.fromWeb(request, new Response(null, { status })), ), ), ), ), ), ), ); const resolving = yield* Effect.forkScoped(service.resolve()); yield* Deferred.await(probeStarted); const bound = { kind: "lan", httpBaseUrl: "http://192.168.1.10:3773/" }; expect(yield* service.resolveAvailable()).toEqual([bound]); yield* Deferred.succeed(finishProbe, undefined); const validated = yield* Fiber.join(resolving); expect(validated).toEqual([ bound, { kind: "tailnet", httpBaseUrl: "https://bb-1.tail1234.ts.net/" }, ]); expect(yield* service.resolveAvailable()).toEqual(validated); yield* Ref.set(status, 502); expect(yield* service.resolve()).toEqual([bound]); expect(yield* service.resolveAvailable()).toEqual([bound]); }).pipe(Effect.scoped), ); effectIt.effect("leaves the tailnet name out when Serve is not forwarding", () => Effect.gen(function* () { expect(yield* resolveWithServe(502)).toEqual([]); }), ); });