fix: handle IPv6 loopback (::1) in LiveKit URL resolution

The previous implementation used serverHost.split(":")[0] to extract
the hostname, which fails for IPv6 addresses — "::1:7880".split(":")[0]
yields "" instead of "::1". Now handles three formats:

- Bracketed: [::1]:7880 → host = "::1"
- Bare IPv6: ::1 → host = "::1" (multiple colons detected)
- IPv4/hostname: example.com:443 → host = "example.com"

Also fixes ensureLiveKitProxy to wrap bare IPv6 in brackets and
correctly detect port presence in bracketed notation.
This commit is contained in:
jevb
2026-04-01 15:17:09 +02:00
parent 74b189fc49
commit ade414bedd
2 changed files with 29 additions and 18 deletions
+22 -2
View File
@@ -281,7 +281,17 @@ export class LiveKitSession {
private async resolveLiveKitUrl(proxyPath: string, directUrl?: string): Promise<string> {
if (this.serverHost !== null) {
const host = this.serverHost.split(":")[0] ?? "";
// Extract hostname, handling IPv6 bracket notation (e.g. "[::1]:7880")
// and bare IPv6 (e.g. "::1").
let host: string;
if (this.serverHost.startsWith("[")) {
host = this.serverHost.slice(1, this.serverHost.indexOf("]"));
} else if ((this.serverHost.match(/:/g) ?? []).length > 1) {
// Bare IPv6 address (multiple colons, no brackets) — use as-is
host = this.serverHost;
} else {
host = this.serverHost.split(":")[0] ?? "";
}
const isLocal = host === "localhost" || host === "127.0.0.1" || host === "::1";
if (isLocal && directUrl) {
log.debug("LiveKit URL resolved via direct (local)", { url: directUrl });
@@ -310,7 +320,17 @@ export class LiveKitSession {
// Ensure host:port format — default to 443 (standard HTTPS) when the
// server is behind a reverse proxy. Without an explicit port, the Rust
// proxy would default to 8443 which may not be exposed.
const hostWithPort = this.serverHost.includes(":") ? this.serverHost : `${this.serverHost}:443`;
// Handle IPv6: "[::1]:7880" has port, "[::1]" and bare "::1" do not.
let hostWithPort: string;
if (this.serverHost.startsWith("[")) {
// Bracketed IPv6 — check for "]:port" suffix
hostWithPort = this.serverHost.includes("]:") ? this.serverHost : `${this.serverHost}:443`;
} else if ((this.serverHost.match(/:/g) ?? []).length > 1) {
// Bare IPv6 (multiple colons) — wrap in brackets and add default port
hostWithPort = `[${this.serverHost}]:443`;
} else {
hostWithPort = this.serverHost.includes(":") ? this.serverHost : `${this.serverHost}:443`;
}
this.liveKitProxyPort = await invoke<number>("start_livekit_proxy", {
remoteHost: hostWithPort,
});
@@ -1378,27 +1378,18 @@ describe("LiveKitSession", () => {
expect(url).toBe("ws://127.0.0.1:7880/livekit");
});
it("returns directUrl when serverHost is ::1 (no port)", async () => {
// serverHost "::1" splits on ":" to ["", "", "1"], [0] is "" — NOT "::1".
// So ::1 is only detected as local when the host part equals "::1",
// which requires serverHost to have no port (split(":")[0] === "").
// This test documents that ::1 with port is NOT detected as local.
it("returns directUrl when serverHost is bare ::1", async () => {
session.setServerHost("::1");
const url = await (session as any).resolveLiveKitUrl("/livekit", "ws://[::1]:7880/livekit");
// split(":")[0] of "::1" is "" which does not match "::1", so it falls through.
// This means ::1 is effectively NOT handled as local by the current implementation.
// The proxy path kicks in instead.
expect(url).toBe("ws://127.0.0.1:7881/livekit");
// Bare IPv6 with multiple colons — detected as local, returns directUrl
expect(url).toBe("ws://[::1]:7880/livekit");
});
it("treats ::1 with port as non-local and routes through proxy", async () => {
session.setServerHost("::1:7880");
it("returns directUrl when serverHost is bracketed [::1]:7880", async () => {
session.setServerHost("[::1]:7880");
const url = await (session as any).resolveLiveKitUrl("/livekit", "ws://[::1]:7880/livekit");
// split(":")[0] of "::1:7880" is "" — not matched as local
expect(mockInvoke).toHaveBeenCalledWith("start_livekit_proxy", {
remoteHost: "::1:7880",
});
expect(url).toBe("ws://127.0.0.1:7881/livekit");
// Bracketed IPv6 — host extracted as "::1", detected as local
expect(url).toBe("ws://[::1]:7880/livekit");
});
it("calls ensureLiveKitProxy and returns proxy URL for remote host with slash path", async () => {