From 9cea77ff49757574c23b2c2f854369a38c15527b Mon Sep 17 00:00:00 2001 From: sunlei Date: Sun, 26 Jul 2026 15:04:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=B6=E7=AA=84=E7=BD=91=E7=BB=9CAgen?= =?UTF-8?q?t=20IPv4=E4=BF=9D=E7=95=99=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../network-agent-v2.types.ts | 2 +- .../network-agent-v2.types.spec.ts | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/modules/admin/platform-config/network-management/network-agent-v2.types.ts b/src/modules/admin/platform-config/network-management/network-agent-v2.types.ts index da8e393..7e023eb 100644 --- a/src/modules/admin/platform-config/network-management/network-agent-v2.types.ts +++ b/src/modules/admin/platform-config/network-management/network-agent-v2.types.ts @@ -354,7 +354,7 @@ function port(value: unknown, name: string): number { if (typeof value !== 'numb function booleanValue(value: unknown, name: string): boolean { if (typeof value !== 'boolean') invalid(name); return value; } function enumValue(value: unknown, allowed: readonly T[], name: string): T { if (typeof value !== 'string' || !allowed.includes(value as T)) invalid(name); return value as T; } function isoString(value: unknown, name: string): string { const result = stringValue(value, name, 64); const match = RFC3339_PATTERN.exec(result); if (!match) invalid(name); const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); const hour = Number(match[4]); const minute = Number(match[5]); const second = Number(match[6]); const offsetHour = match[8] === 'Z' ? 0 : Number(match[10]); const offsetMinute = match[8] === 'Z' ? 0 : Number(match[11]); const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); const days = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; if (month < 1 || month > 12 || day < 1 || day > days[month - 1] || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59 || Number.isNaN(Date.parse(result))) invalid(name); return result; } -function publicIpv4(value: unknown): string { const result = stringValue(value, 'publicIpv4', 15); const [a, b, c] = result.split('.').map(Number); if (isIP(result) !== 4 || a === 0 || a === 10 || a === 127 || a >= 224 || (a === 100 && b >= 64 && b <= 127) || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 192 && b === 0) || (a === 192 && b === 88 && c === 99) || (a === 198 && (b === 18 || b === 19)) || (a === 198 && b === 51 && c === 100) || (a === 203 && b === 0 && c === 113)) invalid('publicIpv4'); return result; } +function publicIpv4(value: unknown): string { const result = stringValue(value, 'publicIpv4', 15); const [a, b, c] = result.split('.').map(Number); if (isIP(result) !== 4 || a === 0 || a === 10 || a === 127 || a >= 224 || (a === 100 && b >= 64 && b <= 127) || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 192 && b === 0 && (c === 0 || c === 2)) || (a === 192 && b === 88 && c === 99) || (a === 198 && (b === 18 || b === 19)) || (a === 198 && b === 51 && c === 100) || (a === 203 && b === 0 && c === 113)) invalid('publicIpv4'); return result; } function publicIpv6(value: unknown): string { const result = stringValue(value, 'publicIpv6', 45); if (isIP(result) !== 6) invalid('publicIpv6'); let normalized: string; try { const hostname = new URL(`http://[${result}]/`).hostname; normalized = hostname.slice(1, -1).toLowerCase(); } catch { invalid('publicIpv6'); } const firstHextet = Number.parseInt(normalized.split(':', 1)[0], 16); if (!Number.isInteger(firstHextet) || firstHextet < 0x2000 || firstHextet > 0x3fff) invalid('publicIpv6'); return normalized; } function digest(value: unknown): string { return createHash('sha256').update(goJsonStringify(value)).digest('hex'); } function goJsonStringify(value: unknown): string { return JSON.stringify(value).replace(//g, '\\u003e').replace(/&/g, '\\u0026').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); } diff --git a/test/admin/network-management/network-agent-v2.types.spec.ts b/test/admin/network-management/network-agent-v2.types.spec.ts index 352fc68..7995725 100644 --- a/test/admin/network-management/network-agent-v2.types.spec.ts +++ b/test/admin/network-management/network-agent-v2.types.spec.ts @@ -117,7 +117,7 @@ describe('network agent MQTT v2 contract', () => { ).toThrow(); }); - it.each(['192.0.0.1', '::ffff:8.8.8.8'])( + it.each(['192.0.0.1', '192.0.2.1', '::ffff:8.8.8.8'])( 'rejects non-dotted-public IPv4 %s', (publicIpv4) => { expect(() => @@ -139,6 +139,25 @@ describe('network agent MQTT v2 contract', () => { }, ); + it('accepts 192.0.1.1 outside the reserved /24 ranges', () => { + expect( + parseEndpointEventV2( + JSON.stringify({ + ...eventValue(), + endpoint: { + mechanism: 'tcp_natmap', + observedAt: '2026-07-26T00:01:00Z', + publicIpv4: '192.0.1.1', + publicPort: 443, + validatedAt: '2026-07-26T00:01:10Z', + validUntil: '2026-07-26T00:03:00Z', + }, + type: 'published', + }), + ), + ).toMatchObject({ endpoint: { publicIpv4: '192.0.1.1' } }); + }); + it('uses shared UTF-8 byte limits for name, version, and global IPv6', () => { const desiredWithName = (name: string) => { const value = fixture('network-v2-desired.json');