From 9af562f0721029759f1290d51148a2f9c3c78a8a Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Fri, 13 Nov 2026 11:08:08 +0000 Subject: [PATCH] Extend the WebRTC session signal Co-Authored-By: Claude Opus 5.5 --- .changeset/webrtc-session-signal.md | 7 +++++ .../webrtcSignal.ts | 6 ++-- .../api/captcha/webrtcSignal.unit.test.ts | 29 +++++++++++++++++-- packages/types-database/src/types/provider.ts | 1 + packages/types/src/provider/webrtc.ts | 5 ++-- 5 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 .changeset/webrtc-session-signal.md diff --git a/.changeset/webrtc-session-signal.md b/.changeset/webrtc-session-signal.md new file mode 100644 index 0000000000..fbd5abf2c6 --- /dev/null +++ b/.changeset/webrtc-session-signal.md @@ -0,0 +1,7 @@ +--- +"@prosopo/types": patch +"@prosopo/types-database": patch +"@prosopo/provider": patch +--- + +Extend the WebRTC session signal. diff --git a/packages/provider/src/api/captcha/getFrictionlessCaptchaChallenge/webrtcSignal.ts b/packages/provider/src/api/captcha/getFrictionlessCaptchaChallenge/webrtcSignal.ts index 710927dfbe..b53a9acd12 100644 --- a/packages/provider/src/api/captcha/getFrictionlessCaptchaChallenge/webrtcSignal.ts +++ b/packages/provider/src/api/captcha/getFrictionlessCaptchaChallenge/webrtcSignal.ts @@ -184,10 +184,10 @@ export const evaluateWebRtc = async ({ } const leakedIp = comparable[0]?.address; - let leaked: WebRtcSignal["leaked"]; + let network: WebRtcSignal["leaked"]; if (leakedIp) { try { - leaked = await leakedNetwork(leakedIp, ipInfoService); + network = await leakedNetwork(leakedIp, ipInfoService); } catch (err) { logger.debug(() => ({ msg: "leaked address lookup failed", err })); } @@ -195,6 +195,6 @@ export const evaluateWebRtc = async ({ return { ...withSeen, outcome: WebRtcOutcome.mismatch, - ...(leaked && { leaked }), + ...(leakedIp && { leaked: { ip: leakedIp, ...network } }), }; }; diff --git a/packages/provider/src/tests/unit/api/captcha/webrtcSignal.unit.test.ts b/packages/provider/src/tests/unit/api/captcha/webrtcSignal.unit.test.ts index 8275c110b2..79f809b4ec 100644 --- a/packages/provider/src/tests/unit/api/captcha/webrtcSignal.unit.test.ts +++ b/packages/provider/src/tests/unit/api/captcha/webrtcSignal.unit.test.ts @@ -177,20 +177,45 @@ describe("evaluateWebRtc", () => { }); }); - it("is a mismatch behind a proxy, recording the leaked network but not the address", async () => { + it("is a mismatch behind a proxy, recording the leaked address and its network", async () => { const signal = await evaluate( report([mdnsHost, srflx(REAL_IP, 49637)]), PROXY_IP, ); expect(signal?.outcome).toBe(WebRtcOutcome.mismatch); expect(signal?.leaked).toEqual({ + ip: REAL_IP, asnNumber: 5089, countryCode: "GB", isProxy: false, isVPN: false, isDatacenter: false, }); - expect(JSON.stringify(signal)).not.toContain(REAL_IP); + }); + + it("records the leaked address even when its network cannot be looked up", async () => { + const unavailable: IIpInfoService = { + ...ipInfoService, + isAvailable: (): boolean => false, + }; + const signal = await evaluateWebRtc({ + report: report([mdnsHost, srflx(REAL_IP, 49637)]), + requestIp: PROXY_IP, + socketPath, + ipInfoService: unavailable, + logger, + }); + expect(signal?.outcome).toBe(WebRtcOutcome.mismatch); + expect(signal?.leaked).toEqual({ ip: REAL_IP }); + }); + + it("does not record an address on a match", async () => { + const signal = await evaluate( + report([mdnsHost, srflx(REAL_IP, 49637)]), + REAL_IP, + ); + expect(signal?.outcome).toBe(WebRtcOutcome.match); + expect(signal?.leaked).toBeUndefined(); }); it("flags a candidate no STUN request matches", async () => { diff --git a/packages/types-database/src/types/provider.ts b/packages/types-database/src/types/provider.ts index a7f68df9c8..fb7252f32a 100644 --- a/packages/types-database/src/types/provider.ts +++ b/packages/types-database/src/types/provider.ts @@ -905,6 +905,7 @@ const WebRtcSignalRecordSchema = new Schema( leaked: { type: new Schema( { + ip: { type: String, required: false }, asnNumber: { type: Number, required: false }, countryCode: { type: String, required: false }, isProxy: { type: Boolean, required: false }, diff --git a/packages/types/src/provider/webrtc.ts b/packages/types/src/provider/webrtc.ts index 7ea8a0560e..ff5de9c2f6 100644 --- a/packages/types/src/provider/webrtc.ts +++ b/packages/types/src/provider/webrtc.ts @@ -53,8 +53,8 @@ export const WebRtcProbeReportSchema = z.object({ export type WebRtcProbeReport = z.infer; -// Stored on the session. Holds no raw address: a mismatch records the leaked -// address's network, not the address. +// Stored on the session. A mismatch records the leaked address and its +// network, so a site can block the machine behind the proxy, not only the exit. export const WebRtcSignalSchema = z.object({ outcome: z.nativeEnum(WebRtcOutcome), clientStatus: z.nativeEnum(WebRtcClientStatus), @@ -68,6 +68,7 @@ export const WebRtcSignalSchema = z.object({ requestIpSeen: z.boolean().optional(), leaked: z .object({ + ip: z.string().max(45).optional(), asnNumber: z.number().int().optional(), countryCode: z.string().max(8).optional(), isProxy: z.boolean().optional(),