From 60f21eb9d2c31fdc03dcba58b91ff211625937d0 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 5 Jun 2026 09:44:17 +0100 Subject: [PATCH] detect loopback and coturn option --- .github/workflows/cli-deno-tests.yml | 5 +++++ src/apps/cli/testdeno/helpers/docker.ts | 9 ++++++--- src/apps/cli/testdeno/helpers/net.ts | 19 +++++++++++++++++++ src/apps/cli/testdeno/helpers/p2p.ts | 2 +- .../testdeno/test-p2p-peers-local-relay.ts | 9 +++++++-- src/apps/cli/testdeno/test-p2p-sync.ts | 9 +++++++-- .../testdeno/test-p2p-three-nodes-conflict.ts | 15 ++++++++++----- 7 files changed, 55 insertions(+), 13 deletions(-) diff --git a/.github/workflows/cli-deno-tests.yml b/.github/workflows/cli-deno-tests.yml index 4d3790c..46a5e7a 100644 --- a/.github/workflows/cli-deno-tests.yml +++ b/.github/workflows/cli-deno-tests.yml @@ -34,6 +34,10 @@ on: description: 'Enable verbose and debug logging' type: boolean default: false + use_coturn: + description: 'Enable local coturn container for P2P tests' + type: boolean + default: false permissions: contents: read @@ -140,6 +144,7 @@ jobs: LIVESYNC_CLI_RETRY: 3 LIVESYNC_CLI_DEBUG: ${{ inputs.enable_debug == true && '1' || '0' }} LIVESYNC_CLI_VERBOSE: ${{ inputs.enable_debug == true && '1' || '0' }} + LIVESYNC_USE_COTURN: ${{ inputs.use_coturn == true && '1' || '0' }} run: | TASK="${{ matrix.task }}" echo "[INFO] Running Deno task: $TASK" diff --git a/src/apps/cli/testdeno/helpers/docker.ts b/src/apps/cli/testdeno/helpers/docker.ts index c20ccd0..3c0e6ce 100644 --- a/src/apps/cli/testdeno/helpers/docker.ts +++ b/src/apps/cli/testdeno/helpers/docker.ts @@ -625,7 +625,7 @@ export async function startP2pRelay(): Promise { } export function isLocalP2pRelay(relayUrl: string): boolean { - return relayUrl === "ws://localhost:4000" || relayUrl === "ws://localhost:4000/"; + return relayUrl.includes("localhost") || relayUrl.includes("127.0.0.1") || relayUrl.includes("[::1]"); } // --------------------------------------------------------------------------- @@ -648,7 +648,10 @@ export async function startCoturn( console.log("[INFO] stopping leftover Coturn container if present"); await stopCoturn().catch(() => {}); - console.log("[INFO] starting local Coturn container"); + const { getOptimalLoopbackIp } = await import("./net.ts"); + const externalIp = await getOptimalLoopbackIp(); + + console.log(`[INFO] starting local Coturn container with external-ip ${externalIp}`); await dockerOrFail( "run", "-d", @@ -660,7 +663,7 @@ export async function startCoturn( `${port}:${port}/udp`, COTURN_IMAGE, "--log-file=stdout", - "--external-ip=127.0.0.1", + `--external-ip=${externalIp}`, `--user=${user}:${pass}`, `--realm=${realm}` ); diff --git a/src/apps/cli/testdeno/helpers/net.ts b/src/apps/cli/testdeno/helpers/net.ts index 362c70d..fa5debd 100644 --- a/src/apps/cli/testdeno/helpers/net.ts +++ b/src/apps/cli/testdeno/helpers/net.ts @@ -47,3 +47,22 @@ export async function waitForPort(hostname: string, port: number, options: WaitF (lastError ? ` (last error: ${String(lastError)})` : "") ); } + +export async function getOptimalLoopbackIp(): Promise { + const ipv4 = "127.0.0.1"; + const ipv6 = "::1"; + + try { + const l = Deno.listen({ hostname: ipv4, port: 0 }); + l.close(); + return ipv4; + } catch { + try { + const l = Deno.listen({ hostname: ipv6, port: 0 }); + l.close(); + return ipv6; + } catch { + return ipv4; // fallback to default + } + } +} diff --git a/src/apps/cli/testdeno/helpers/p2p.ts b/src/apps/cli/testdeno/helpers/p2p.ts index f3e0574..a04b080 100644 --- a/src/apps/cli/testdeno/helpers/p2p.ts +++ b/src/apps/cli/testdeno/helpers/p2p.ts @@ -97,7 +97,7 @@ export async function stopLocalRelayIfStarted(started: boolean): Promise { } export async function maybeStartCoturn(turnServers: string): Promise { - if (turnServers.includes("localhost") || turnServers.includes("127.0.0.1")) { + if (turnServers.includes("localhost") || turnServers.includes("127.0.0.1") || turnServers.includes("[::1]")) { await startCoturn(); return true; } diff --git a/src/apps/cli/testdeno/test-p2p-peers-local-relay.ts b/src/apps/cli/testdeno/test-p2p-peers-local-relay.ts index f1c481f..36dbbf8 100644 --- a/src/apps/cli/testdeno/test-p2p-peers-local-relay.ts +++ b/src/apps/cli/testdeno/test-p2p-peers-local-relay.ts @@ -9,16 +9,21 @@ import { maybeStartCoturn, stopCoturnIfStarted, } from "./helpers/p2p.ts"; +import { getOptimalLoopbackIp } from "./helpers/net.ts"; Deno.test("p2p-peers: discovers host through local relay", async () => { - const relay = Deno.env.get("RELAY") ?? "ws://localhost:4000/"; + const loopbackIp = await getOptimalLoopbackIp(); + const loopbackHost = loopbackIp === "::1" ? "[::1]" : loopbackIp; + + const relay = Deno.env.get("RELAY") ?? `ws://${loopbackHost}:4000/`; const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`; const passphrase = Deno.env.get("PASSPHRASE") ?? "test"; const timeoutSeconds = Number(Deno.env.get("TIMEOUT_SECONDS") ?? "8"); const nonce = `${Date.now()}-${crypto.randomUUID().slice(0, 8)}`; const hostPeerName = Deno.env.get("HOST_PEER_NAME") ?? `p2p-host-${nonce}`; const clientPeerName = Deno.env.get("CLIENT_PEER_NAME") ?? `p2p-client-${nonce}`; - const turnServers = Deno.env.get("TURN_SERVERS") ?? "turn:127.0.0.1:3478"; + const useCoturn = Deno.env.get("LIVESYNC_USE_COTURN") !== "0"; + const turnServers = Deno.env.get("TURN_SERVERS") ?? (useCoturn ? `turn:${loopbackHost}:3478` : "none"); await using workDir = await TempDir.create("livesync-cli-p2p-peers-local-relay"); const hostVault = workDir.join("vault-host"); diff --git a/src/apps/cli/testdeno/test-p2p-sync.ts b/src/apps/cli/testdeno/test-p2p-sync.ts index 9772232..53cefda 100644 --- a/src/apps/cli/testdeno/test-p2p-sync.ts +++ b/src/apps/cli/testdeno/test-p2p-sync.ts @@ -10,9 +10,13 @@ import { stopCoturnIfStarted, } from "./helpers/p2p.ts"; import { runCli } from "./helpers/cli.ts"; +import { getOptimalLoopbackIp } from "./helpers/net.ts"; Deno.test("p2p-sync: discovers peer and completes sync", async () => { - const relay = Deno.env.get("RELAY") ?? "ws://localhost:4000/"; + const loopbackIp = await getOptimalLoopbackIp(); + const loopbackHost = loopbackIp === "::1" ? "[::1]" : loopbackIp; + + const relay = Deno.env.get("RELAY") ?? `ws://${loopbackHost}:4000/`; const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`; const passphrase = Deno.env.get("PASSPHRASE") ?? "test"; const peersTimeout = Number(Deno.env.get("PEERS_TIMEOUT") ?? "12"); @@ -20,7 +24,8 @@ Deno.test("p2p-sync: discovers peer and completes sync", async () => { const nonce = `${Date.now()}-${crypto.randomUUID().slice(0, 8)}`; const hostPeerName = Deno.env.get("HOST_PEER_NAME") ?? `p2p-host-${nonce}`; const clientPeerName = Deno.env.get("CLIENT_PEER_NAME") ?? `p2p-client-${nonce}`; - const turnServers = Deno.env.get("TURN_SERVERS") ?? "turn:127.0.0.1:3478"; + const useCoturn = Deno.env.get("LIVESYNC_USE_COTURN") !== "0"; + const turnServers = Deno.env.get("TURN_SERVERS") ?? (useCoturn ? `turn:${loopbackHost}:3478` : "none"); await using workDir = await TempDir.create("livesync-cli-p2p-sync"); const hostVault = workDir.join("vault-host"); diff --git a/src/apps/cli/testdeno/test-p2p-three-nodes-conflict.ts b/src/apps/cli/testdeno/test-p2p-three-nodes-conflict.ts index ba6ed1a..718367d 100644 --- a/src/apps/cli/testdeno/test-p2p-three-nodes-conflict.ts +++ b/src/apps/cli/testdeno/test-p2p-three-nodes-conflict.ts @@ -10,19 +10,24 @@ import { stopCoturnIfStarted, } from "./helpers/p2p.ts"; import { jsonStringField, runCliOrFail, runCliWithInputOrFail, sanitiseCatStdout } from "./helpers/cli.ts"; +import { getOptimalLoopbackIp } from "./helpers/net.ts"; Deno.test("p2p: three nodes detect and resolve conflicts", async () => { - const relay = Deno.env.get("RELAY") ?? "ws://localhost:4000/"; - const roomId = `${Deno.env.get("ROOM_ID_PREFIX") ?? "p2p-room"}-${Date.now()}`; - const passphrase = `${Deno.env.get("PASSPHRASE_PREFIX") ?? "p2p-pass"}-${Date.now()}`; - const appId = Deno.env.get("APP_ID") ?? "self-hosted-livesync-cli-tests"; + const loopbackIp = await getOptimalLoopbackIp(); + const loopbackHost = loopbackIp === "::1" ? "[::1]" : loopbackIp; + + const relay = Deno.env.get("RELAY") ?? `ws://${loopbackHost}:4000/`; + const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`; + const passphrase = Deno.env.get("PASSPHRASE") ?? "test"; + const appId = "self-hosted-livesync-cli-tests"; const peersTimeout = Number(Deno.env.get("PEERS_TIMEOUT") ?? "10"); const syncTimeout = Number(Deno.env.get("SYNC_TIMEOUT") ?? "15"); const nonce = `${Date.now()}-${crypto.randomUUID().slice(0, 8)}`; const hostPeerName = Deno.env.get("HOST_PEER_NAME") ?? `p2p-host-${nonce}`; const peerNameB = Deno.env.get("PEER_NAME_B") ?? `p2p-client-b-${nonce}`; const peerNameC = Deno.env.get("PEER_NAME_C") ?? `p2p-client-c-${nonce}`; - const turnServers = Deno.env.get("TURN_SERVERS") ?? "turn:127.0.0.1:3478"; + const useCoturn = Deno.env.get("LIVESYNC_USE_COTURN") !== "0"; + const turnServers = Deno.env.get("TURN_SERVERS") ?? (useCoturn ? `turn:${loopbackHost}:3478` : "none"); await using workDir = await TempDir.create("livesync-cli-p2p-3nodes"); const vaultA = workDir.join("vault-a");