From 5f9b47b311936ec7b770d14b684d9f07ecf4ccd6 Mon Sep 17 00:00:00 2001 From: dmitrii Date: Thu, 9 Jul 2026 00:27:30 +0200 Subject: [PATCH] Avoid running the IPv6 regex on IPv4:port in normalizeIp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit normalizeIp() called the version-less IPValidator.isIP(ip) first, which runs the expensive IPv6 regex on every value that is not a bare IPv4 — including IPv4:port forms that proxies routinely send in X-Forwarded-For / the peer address. Since this runs in the per-request context constructor, it added CPU on proxied traffic (introduced in #307). Reorder normalizeIp to run the cheap structural checks before the version-less isIP, keeping the same checks (and branch bodies) as zen-node's getClientIpFromHeader: single-colon IPv4:port via isIP(host, "4"), then bracketed IPv6 ([ipv6] / [ipv6]:port), and only then the version-less isIP as the final fallback. node's isIP is native and cheap, ours is a regex, so the only deviation from node is running isIP last instead of first. Behavior is unchanged (existing ProxyForwardedParserTest suite passes; added cases for multi-colon IPv4, IPv4:port short-circuit, bracketed IPv4 and malformed "[::1]foo"). Verified with a JFR A/B on zen-demo-java under ip:port traffic: total agent CPU 4.90% -> 4.41% (baseline before the regression 4.53%). --- .../helpers/net/ProxyForwardedParser.java | 16 ++++++------ .../helpers/ProxyForwardedParserTest.java | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/ProxyForwardedParser.java b/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/ProxyForwardedParser.java index b42f4051..7daf7fb5 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/ProxyForwardedParser.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/helpers/net/ProxyForwardedParser.java @@ -55,8 +55,12 @@ private static String normalizeIp(String ip) { ip = ip.trim(); - if (IPValidator.isIP(ip)) { - return ip; + // Some proxies pass along port numbers with IPv4 addresses: ip:port + if (ip.contains(":")) { + String[] ipParts = ip.split(":"); + if (ipParts.length == 2 && IPValidator.isIP(ipParts[0], "4")) { + return ipParts[0]; + } } if (ip.startsWith("[") && ip.endsWith("]")) { @@ -76,12 +80,8 @@ private static String normalizeIp(String ip) { } } - // Some proxies pass along port numbers with IP addresses : - if (ip.contains(":")) { - String[] ipParts = ip.split(":"); - if (ipParts.length == 2 && IPValidator.isIP(ipParts[0], "4")) { - return ipParts[0]; - } + if (IPValidator.isIP(ip)) { + return ip; } return null; diff --git a/agent_api/src/test/java/helpers/ProxyForwardedParserTest.java b/agent_api/src/test/java/helpers/ProxyForwardedParserTest.java index 4bedb823..b4a81912 100644 --- a/agent_api/src/test/java/helpers/ProxyForwardedParserTest.java +++ b/agent_api/src/test/java/helpers/ProxyForwardedParserTest.java @@ -214,4 +214,30 @@ void testGetIpFromRequest_RawIpWithEmptyStringFallsBackToRawIp() { String result = getIpFromRequest("", new HashMap<>()); assertEquals("", result); } + + @Test + void testGetIpFromRequest_IPv4WithMultiplePortColonsFallsBackToRawIp() { + headers.put("X-Forwarded-For", List.of("1.2.3.4:80:90")); + String result = getIpFromRequest("10.0.0.1", headers); + assertEquals("10.0.0.1", result); + } + + @Test + void testGetIpFromRequest_IPv4PortResolvesWithoutTouchingLaterEntries() { + headers.put("X-Forwarded-For", List.of("203.0.113.7:54321, 8.8.8.8")); + String result = getIpFromRequest("10.0.0.1", headers); + assertEquals("203.0.113.7", result); + } + + @Test + void testGetIpFromRequest_BracketedIPv4Resolves() { + String result = getIpFromRequest("[1.2.3.4]", new HashMap<>()); + assertEquals("1.2.3.4", result); + } + + @Test + void testGetIpFromRequest_BracketedIpWithTrailingGarbageFallsBackToRawIp() { + String result = getIpFromRequest("[::1]foo", new HashMap<>()); + assertEquals("[::1]foo", result); + } }