From 90fa9e8741f89f6c73701b760886f9825e410489 Mon Sep 17 00:00:00 2001 From: Bergasha Date: Wed, 19 Aug 2026 13:52:14 +1000 Subject: [PATCH] Added tests for the LAN/WAN parsing and fixed IPv4-mapped IPv6 addresses being misclassified as WAN - a dual-stack server can report a LAN client as ::ffff:192.168.x.x, which the private-range check was missing since it only matched literal IPv6 ranges --- .../src/jellyfin/jellyfin-integration.spec.ts | 58 +++++++++++++++++++ .../src/jellyfin/jellyfin-integration.ts | 14 +++-- 2 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 packages/integrations/src/jellyfin/jellyfin-integration.spec.ts diff --git a/packages/integrations/src/jellyfin/jellyfin-integration.spec.ts b/packages/integrations/src/jellyfin/jellyfin-integration.spec.ts new file mode 100644 index 000000000..46e21c7ed --- /dev/null +++ b/packages/integrations/src/jellyfin/jellyfin-integration.spec.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from "vitest"; + +import { extractHost, parseLocation } from "./jellyfin-integration"; + +describe("extractHost", () => { + test("strips the port from an IPv4 endpoint", () => { + expect(extractHost("192.168.1.100:54321")).toBe("192.168.1.100"); + }); + + test("returns an IPv4 address unchanged when there's no port", () => { + expect(extractHost("8.8.8.8")).toBe("8.8.8.8"); + }); + + test("strips brackets and the port from a bracketed IPv6 endpoint", () => { + expect(extractHost("[::1]:8096")).toBe("::1"); + }); + + test("returns a bare IPv6 address unchanged when there's no port", () => { + expect(extractHost("::1")).toBe("::1"); + }); +}); + +describe("parseLocation", () => { + test("returns null when there's no endpoint to classify", () => { + expect(parseLocation(null)).toBeNull(); + expect(parseLocation(undefined)).toBeNull(); + expect(parseLocation("")).toBeNull(); + }); + + test("returns null for a value that isn't an IP", () => { + expect(parseLocation("not-an-ip:1234")).toBeNull(); + }); + + test("classifies private IPv4 ranges as lan", () => { + expect(parseLocation("192.168.1.100:54321")).toBe("lan"); + expect(parseLocation("10.0.0.5:1234")).toBe("lan"); + expect(parseLocation("172.16.5.5:1234")).toBe("lan"); + expect(parseLocation("127.0.0.1:8096")).toBe("lan"); + }); + + test("classifies a public IPv4 address as wan", () => { + expect(parseLocation("8.8.8.8:1234")).toBe("wan"); + }); + + test("classifies IPv6 loopback and unique-local addresses as lan", () => { + expect(parseLocation("[::1]:8096")).toBe("lan"); + expect(parseLocation("[fd12:3456:789a::1]:8096")).toBe("lan"); + }); + + test("classifies a public IPv6 address as wan", () => { + expect(parseLocation("[2606:4700:4700::1111]:8096")).toBe("wan"); + }); + + test("unwraps an IPv4-mapped IPv6 address before classifying it", () => { + expect(parseLocation("[::ffff:192.168.1.5]:8096")).toBe("lan"); + expect(parseLocation("[::ffff:8.8.8.8]:8096")).toBe("wan"); + }); +}); diff --git a/packages/integrations/src/jellyfin/jellyfin-integration.ts b/packages/integrations/src/jellyfin/jellyfin-integration.ts index d97dcc127..e5cc6511e 100644 --- a/packages/integrations/src/jellyfin/jellyfin-integration.ts +++ b/packages/integrations/src/jellyfin/jellyfin-integration.ts @@ -32,7 +32,7 @@ privateAddresses.addSubnet("127.0.0.0", 8, "ipv4"); privateAddresses.addSubnet("fc00::", 7, "ipv6"); privateAddresses.addAddress("::1", "ipv6"); -function extractHost(remoteEndPoint: string): string { +export function extractHost(remoteEndPoint: string): string { if (remoteEndPoint.startsWith("[")) { const end = remoteEndPoint.indexOf("]"); return end === -1 ? remoteEndPoint : remoteEndPoint.slice(1, end); @@ -42,10 +42,16 @@ function extractHost(remoteEndPoint: string): string { return isIPv4WithPort ? remoteEndPoint.slice(0, remoteEndPoint.lastIndexOf(":")) : remoteEndPoint; } -function parseLocation(remoteEndPoint: string | null | undefined): "lan" | "wan" | null { +export function parseLocation(remoteEndPoint: string | null | undefined): "lan" | "wan" | null { if (!remoteEndPoint) return null; - const host = extractHost(remoteEndPoint); - const family = isIP(host); + let host = extractHost(remoteEndPoint); + let family = isIP(host); + // A dual-stack server can report an IPv4 client as an IPv4-mapped IPv6 address + // (e.g. "::ffff:192.168.1.5") - unwrap it so the IPv4 private ranges still apply. + if (family === 6 && host.toLowerCase().startsWith("::ffff:") && isIP(host.slice(7)) === 4) { + host = host.slice(7); + family = 4; + } if (!family) return null; return privateAddresses.check(host, family === 4 ? "ipv4" : "ipv6") ? "lan" : "wan"; }