import { describe, expect, it } from "vitest"; import { ClientAddressConfigError, createClientAddressConfig, createClientAddressResolver, findInvalidTrustedProxies, normalizeClientAddress, } from "../src/identity/client-address.js"; const INTERNAL_HEADER = "x-ekklesia-client-address"; describe("createClientAddressConfig", () => { it("defaults to direct mode when mode is entirely omitted", () => { expect(createClientAddressConfig()).toEqual({ mode: "direct" }); expect(createClientAddressConfig({})).toEqual({ mode: "direct" }); expect(Object.isFrozen(createClientAddressConfig())).toBe(true); }); it("rejects direct mode combined with a header or trusted proxies", () => { expect(() => createClientAddressConfig({ header: "x-forwarded-for", mode: "direct" })).toThrow( ClientAddressConfigError, ); try { createClientAddressConfig({ mode: "direct", trustedProxies: ["10.0.0.1"] }); } catch (error) { expect(error).toMatchObject({ code: "CLIENT_ADDRESS_DIRECT_MODE_CONTRADICTION" }); } }); it("rejects an invalid mode value", () => { let captured: unknown; try { createClientAddressConfig({ mode: "proxy" }); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "CLIENT_ADDRESS_MODE_INVALID" }); }); it("accepts a valid trusted-proxy configuration", () => { const config = createClientAddressConfig({ header: "X-Forwarded-For", mode: "trusted-proxy", trustedProxies: ["10.0.0.5", "192.168.1.0/24"], }); expect(config).toEqual({ header: "x-forwarded-for", mode: "trusted-proxy", trustedProxies: ["10.0.0.5", "192.168.1.0/24"], }); expect(Object.isFrozen(config)).toBe(true); if (config.mode === "trusted-proxy") { expect(Object.isFrozen(config.trustedProxies)).toBe(true); } }); it("rejects trusted-proxy mode missing a header", () => { let captured: unknown; try { createClientAddressConfig({ mode: "trusted-proxy", trustedProxies: ["10.0.0.1"] }); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "CLIENT_ADDRESS_HEADER_MISSING" }); }); it("rejects an invalid header name", () => { let captured: unknown; try { createClientAddressConfig({ header: "not a header!", mode: "trusted-proxy", trustedProxies: ["10.0.0.1"], }); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "CLIENT_ADDRESS_HEADER_INVALID" }); }); it("rejects the internal platform header as a configured source header", () => { let captured: unknown; try { createClientAddressConfig( { header: INTERNAL_HEADER, mode: "trusted-proxy", trustedProxies: ["10.0.0.1"], }, INTERNAL_HEADER, ); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "CLIENT_ADDRESS_HEADER_INVALID" }); }); it("rejects trusted-proxy mode with an empty trusted-proxy list", () => { let captured: unknown; try { createClientAddressConfig({ header: "x-forwarded-for", mode: "trusted-proxy", trustedProxies: [], }); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "CLIENT_ADDRESS_TRUSTED_PROXIES_EMPTY" }); }); it("rejects a malformed trusted-proxy entry", () => { for (const trustedProxies of [ ["not-an-ip"], ["10.0.0.1/33"], ["10.0.0.1/-1"], ["10.0.0.1/1e3"], [""], ]) { let captured: unknown; try { createClientAddressConfig({ header: "x-forwarded-for", mode: "trusted-proxy", trustedProxies, }); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "CLIENT_ADDRESS_TRUSTED_PROXY_INVALID" }); } }); it("never leaks the offending mode, header, or trusted-proxy value in the thrown error", () => { const hostileHeader = "x-hostile-secret-header"; let captured: unknown; try { createClientAddressConfig({ header: hostileHeader, mode: "trusted-proxy", trustedProxies: ["definitely-not-an-ip"], }); } catch (error) { captured = error; } const publicText = `${String(captured)}\n${ captured instanceof Error ? (captured.stack ?? "") : "" }`; expect(publicText).not.toContain(hostileHeader); expect(publicText).not.toContain("definitely-not-an-ip"); }); }); describe("findInvalidTrustedProxies", () => { it("identifies exactly the malformed entries", () => { expect( findInvalidTrustedProxies(["10.0.0.1", "not-an-ip", "192.168.0.0/24", "10.0.0.1/99"]), ).toEqual(["not-an-ip", "10.0.0.1/99"]); }); it("accepts IPv6 addresses and ranges", () => { expect(findInvalidTrustedProxies(["::1", "fd00::/8", "2001:db8::1"])).toEqual([]); }); }); describe("normalizeClientAddress", () => { it("passes a plain IPv4 address through unchanged", () => { expect(normalizeClientAddress("192.0.2.10")).toBe("192.0.2.10"); }); it("lowercases a plain IPv6 address", () => { expect(normalizeClientAddress("2001:DB8::1")).toBe("2001:db8::1"); }); it("collapses an IPv4-mapped IPv6 address to its embedded IPv4 form", () => { expect(normalizeClientAddress("::ffff:127.0.0.1")).toBe("127.0.0.1"); expect(normalizeClientAddress("::FFFF:203.0.113.5")).toBe("203.0.113.5"); }); it("rejects a zone id as ambiguous", () => { expect(normalizeClientAddress("fe80::1%eth0")).toBeUndefined(); }); it("rejects malformed, empty, or non-IP input", () => { for (const value of ["", " ", "not-an-ip", "999.999.999.999", "1.2.3"]) { expect(normalizeClientAddress(value)).toBeUndefined(); } }); it("trims surrounding whitespace", () => { expect(normalizeClientAddress(" 192.0.2.10 ")).toBe("192.0.2.10"); }); }); describe("createClientAddressResolver — direct mode", () => { it("resolves the actual immediate socket peer, ignoring any forwarded header", () => { const resolver = createClientAddressResolver({ mode: "direct" }); const result = resolver.resolve({ headers: new Headers({ "x-forwarded-for": "203.0.113.99" }), peerAddress: "192.0.2.1", }); expect(result).toEqual({ address: "192.0.2.1", ok: true }); }); it("fails closed when no peer address is available", () => { const resolver = createClientAddressResolver({ mode: "direct" }); const result = resolver.resolve({ headers: new Headers(), peerAddress: undefined }); expect(result).toEqual({ ok: false, reason: "PEER_ADDRESS_UNAVAILABLE" }); }); it("fails closed when the peer address is not a valid IP", () => { const resolver = createClientAddressResolver({ mode: "direct" }); const result = resolver.resolve({ headers: new Headers(), peerAddress: "not-an-ip" }); expect(result).toEqual({ ok: false, reason: "PEER_ADDRESS_UNAVAILABLE" }); }); it("normalizes an IPv4-mapped IPv6 peer to its IPv4 form", () => { const resolver = createClientAddressResolver({ mode: "direct" }); const result = resolver.resolve({ headers: new Headers(), peerAddress: "::ffff:198.51.100.7" }); expect(result).toEqual({ address: "198.51.100.7", ok: true }); }); it("gives distinct real peer addresses distinct resolved identities", () => { const resolver = createClientAddressResolver({ mode: "direct" }); const first = resolver.resolve({ headers: new Headers(), peerAddress: "127.0.0.2" }); const second = resolver.resolve({ headers: new Headers(), peerAddress: "127.0.0.3" }); expect(first).toEqual({ address: "127.0.0.2", ok: true }); expect(second).toEqual({ address: "127.0.0.3", ok: true }); expect(first).not.toEqual(second); }); }); describe("createClientAddressResolver — trusted-proxy mode", () => { function proxyResolver(trustedProxies: readonly string[] = ["10.0.0.1", "10.0.0.2"]) { return createClientAddressResolver({ header: "x-forwarded-for", mode: "trusted-proxy", trustedProxies, }); } it("ignores the forwarded header entirely when the immediate peer is not a trusted proxy", () => { const resolver = proxyResolver(); const result = resolver.resolve({ headers: new Headers({ "x-forwarded-for": "203.0.113.99" }), peerAddress: "198.51.100.1", }); // Falls back to direct-mode behavior: the untrusted peer itself, never the header content. expect(result).toEqual({ address: "198.51.100.1", ok: true }); }); it("honors a single-value forwarded header from a trusted proxy", () => { const resolver = proxyResolver(); const result = resolver.resolve({ headers: new Headers({ "x-forwarded-for": "203.0.113.5" }), peerAddress: "10.0.0.1", }); expect(result).toEqual({ address: "203.0.113.5", ok: true }); }); it("walks a chained forwarded header right-to-left, skipping trusted-proxy hops", () => { const resolver = proxyResolver(); const result = resolver.resolve({ headers: new Headers({ "x-forwarded-for": "203.0.113.5, 10.0.0.2" }), peerAddress: "10.0.0.1", }); expect(result).toEqual({ address: "203.0.113.5", ok: true }); }); it("walks past multiple trusted-proxy hops to find the genuine client", () => { const resolver = proxyResolver(["10.0.0.1", "10.0.0.2", "10.0.0.3"]); const result = resolver.resolve({ headers: new Headers({ "x-forwarded-for": "203.0.113.5, 10.0.0.3, 10.0.0.2" }), peerAddress: "10.0.0.1", }); expect(result).toEqual({ address: "203.0.113.5", ok: true }); }); it("fails closed when the trusted proxy sends no forwarded header at all", () => { const resolver = proxyResolver(); const result = resolver.resolve({ headers: new Headers(), peerAddress: "10.0.0.1" }); expect(result).toEqual({ ok: false, reason: "FORWARDED_HEADER_MISSING" }); }); it("fails closed on an empty (but present) forwarded header value", () => { const resolver = proxyResolver(); const result = resolver.resolve({ headers: new Headers({ "x-forwarded-for": "" }), peerAddress: "10.0.0.1", }); expect(result).toEqual({ ok: false, reason: "FORWARDED_HEADER_MALFORMED" }); }); it("fails closed on a malformed token anywhere in the chain", () => { const resolver = proxyResolver(); const result = resolver.resolve({ headers: new Headers({ "x-forwarded-for": "203.0.113.5, not-an-ip" }), peerAddress: "10.0.0.1", }); expect(result).toEqual({ ok: false, reason: "FORWARDED_HEADER_MALFORMED" }); }); it("fails closed on an empty comma-separated segment (ambiguous multi-valued form)", () => { const resolver = proxyResolver(); const result = resolver.resolve({ headers: new Headers({ "x-forwarded-for": "203.0.113.5,, 10.0.0.1" }), peerAddress: "10.0.0.1", }); expect(result).toEqual({ ok: false, reason: "FORWARDED_HEADER_MALFORMED" }); }); it("fails closed when the entire chain resolves to trusted-proxy hops with no genuine client left", () => { const resolver = proxyResolver(); const result = resolver.resolve({ headers: new Headers({ "x-forwarded-for": "10.0.0.2, 10.0.0.1" }), peerAddress: "10.0.0.1", }); expect(result).toEqual({ ok: false, reason: "FORWARDED_CHAIN_EXHAUSTED" }); }); it("normalizes an IPv4-mapped IPv6 hop before matching it against a trusted IPv4 proxy", () => { const resolver = proxyResolver(["10.0.0.1"]); const result = resolver.resolve({ headers: new Headers({ "x-forwarded-for": "203.0.113.5, ::ffff:10.0.0.1" }), peerAddress: "10.0.0.1", }); expect(result).toEqual({ address: "203.0.113.5", ok: true }); }); it("matches a trusted-proxy CIDR range, not only an exact address", () => { const resolver = proxyResolver(["192.168.1.0/24"]); const result = resolver.resolve({ headers: new Headers({ "x-forwarded-for": "203.0.113.5" }), peerAddress: "192.168.1.42", }); expect(result).toEqual({ address: "203.0.113.5", ok: true }); }); it("never lets a spoofed header from an untrusted peer choose a client identity that collides with a trusted flow", () => { const resolver = proxyResolver(); // An attacker connecting directly (not through the real proxy) claims to forward from a // trusted-proxy address itself — since the *immediate peer* is what gates trust, not header // content, this must still resolve to the attacker's own real peer address. const result = resolver.resolve({ headers: new Headers({ "x-forwarded-for": "203.0.113.5, 10.0.0.1" }), peerAddress: "203.0.113.200", }); expect(result).toEqual({ address: "203.0.113.200", ok: true }); }); });