Skip to content

Commit 9807716

Browse files
author
dmitrii
committed
Avoid running the IPv6 regex on IPv4:port in normalizeIp
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 do the cheap structural checks first: bracketed IPv6 ([ipv6] / [ipv6]:port, with a strict suffix check), then single-colon IPv4:port via isIP(host, "4"), and only then the version-less isIP fallback. 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%).
1 parent 6ee5432 commit 9807716

2 files changed

Lines changed: 41 additions & 18 deletions

File tree

agent_api/src/main/java/dev/aikido/agent_api/helpers/net/ProxyForwardedParser.java

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -55,35 +55,32 @@ private static String normalizeIp(String ip) {
5555

5656
ip = ip.trim();
5757

58-
if (IPValidator.isIP(ip)) {
59-
return ip;
60-
}
61-
62-
if (ip.startsWith("[") && ip.endsWith("]")) {
63-
String unwrappedIp = ip.substring(1, ip.length() - 1);
64-
if (IPValidator.isIP(unwrappedIp)) {
65-
return unwrappedIp;
66-
}
67-
}
68-
6958
if (ip.startsWith("[")) {
70-
int closingBracket = ip.indexOf("]:");
71-
if (closingBracket > 0) {
59+
int closingBracket = ip.indexOf(']');
60+
boolean validSuffix = closingBracket > 0
61+
&& (closingBracket == ip.length() - 1 || ip.startsWith(":", closingBracket + 1));
62+
if (validSuffix) {
7263
String unwrappedIp = ip.substring(1, closingBracket);
7364
if (IPValidator.isIP(unwrappedIp)) {
7465
return unwrappedIp;
7566
}
7667
}
68+
return null;
7769
}
7870

79-
// Some proxies pass along port numbers with IP addresses :
80-
if (ip.contains(":")) {
81-
String[] ipParts = ip.split(":");
82-
if (ipParts.length == 2 && IPValidator.isIP(ipParts[0], "4")) {
83-
return ipParts[0];
71+
// Handle IPv4:port before the version-less isIP so it skips the IPv6 regex.
72+
int firstColon = ip.indexOf(':');
73+
if (firstColon > 0 && ip.indexOf(':', firstColon + 1) < 0) {
74+
String host = ip.substring(0, firstColon);
75+
if (IPValidator.isIP(host, "4")) {
76+
return host;
8477
}
8578
}
8679

80+
if (IPValidator.isIP(ip)) {
81+
return ip;
82+
}
83+
8784
return null;
8885
}
8986

agent_api/src/test/java/helpers/ProxyForwardedParserTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,30 @@ void testGetIpFromRequest_RawIpWithEmptyStringFallsBackToRawIp() {
214214
String result = getIpFromRequest("", new HashMap<>());
215215
assertEquals("", result);
216216
}
217+
218+
@Test
219+
void testGetIpFromRequest_IPv4WithMultiplePortColonsFallsBackToRawIp() {
220+
headers.put("X-Forwarded-For", List.of("1.2.3.4:80:90"));
221+
String result = getIpFromRequest("10.0.0.1", headers);
222+
assertEquals("10.0.0.1", result);
223+
}
224+
225+
@Test
226+
void testGetIpFromRequest_IPv4PortResolvesWithoutTouchingLaterEntries() {
227+
headers.put("X-Forwarded-For", List.of("203.0.113.7:54321, 8.8.8.8"));
228+
String result = getIpFromRequest("10.0.0.1", headers);
229+
assertEquals("203.0.113.7", result);
230+
}
231+
232+
@Test
233+
void testGetIpFromRequest_BracketedIPv4Resolves() {
234+
String result = getIpFromRequest("[1.2.3.4]", new HashMap<>());
235+
assertEquals("1.2.3.4", result);
236+
}
237+
238+
@Test
239+
void testGetIpFromRequest_BracketedIpWithTrailingGarbageFallsBackToRawIp() {
240+
String result = getIpFromRequest("[::1]foo", new HashMap<>());
241+
assertEquals("[::1]foo", result);
242+
}
217243
}

0 commit comments

Comments
 (0)