diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java index 002ffea8..1662ace0 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java @@ -39,22 +39,25 @@ public static void report(String hostname, InetAddress[] inetAddresses) { // Removing them here ensures each (hostname, port) pair is counted exactly once. Set ports = PendingHostnamesStore.getAndRemove(hostname); - // The outbound-domains list is meant for hostnames, not raw IP literals. - // A private/internal IP literal passed straight to getAllByName (DNS-resolver - // bootstrap, service discovery connecting by IP, libraries building a private-IP - // matcher, ...) is not an outbound domain and would otherwise flood the - // "new outbound connection" feature. Skip recording those; SSRF/stored-SSRF and - // outbound-domain blocking below are unaffected. - boolean isPrivateIpLiteral = IsPrivateIP.isPrivateIp(hostname); - if (!isPrivateIpLiteral) { - if (!ports.isEmpty()) { - for (int port : ports) { - HostnamesStore.incrementHits(hostname, port); - } - } else { - // We still need to report a hit to the hostname for outbound domain blocking - HostnamesStore.incrementHits(hostname, 0); + // Don't report private/internal IP literals as outbound connections, consistent + // with the other Zen agents. A raw private IP reaching getAllByName is infrastructure, + // not a real outbound domain: the Reactor Netty resolver bootstrap resolving the + // any-address/nameservers, service discovery connecting by IP, a library building a + // private-IP matcher, etc. We fully return so we also skip outbound blocking below; + // otherwise lockdown mode (blockNewOutgoingRequests) would block these internal + // resolutions and break the application. Real domains that resolve to private IPs are + // not literals, so they fall through and are still tracked and SSRF-checked. + if (IsPrivateIP.isPrivateIp(hostname)) { + return; + } + + if (!ports.isEmpty()) { + for (int port : ports) { + HostnamesStore.incrementHits(hostname, port); } + } else { + // We still need to report a hit to the hostname for outbound domain blocking + HostnamesStore.incrementHits(hostname, 0); } // Block if the hostname is in the blocked domains list diff --git a/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java b/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java index a8d9ea8d..a8f2142f 100644 --- a/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java +++ b/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java @@ -278,4 +278,34 @@ public void testPublicIpLiteralStillRecorded() { assertEquals(1, entries.length); assertEquals("1.1.1.1", entries[0].getHostname()); } + + @Test + public void testPrivateIpLiteralNotBlockedInLockdownMode() throws UnknownHostException { + // Lockdown (blockNewOutgoingRequests=true) blocks any host not on the allow list. + // A private IP literal must be fully ignored via early return, so it is neither + // recorded nor blocked; otherwise lockdown would break internal resolutions. + ServiceConfigStore.updateFromAPIResponse(new APIResponse( + true, null, 0L, null, null, null, true, List.of(), true, true, List.of() + )); + InetAddress[] resolved = new InetAddress[]{InetAddress.getByName("10.0.0.0")}; + + assertDoesNotThrow(() -> DNSRecordCollector.report("10.0.0.0", resolved)); + assertEquals(0, HostnamesStore.getHostnamesAsList().length); + } + + @Test + public void testPrivateIpLiteralViaUrlInLockdownNotBlockedNorRecorded() throws UnknownHostException { + // http://10.0.0.1:8080 -> URLCollector registers pending port 8080, then + // getAllByName("10.0.0.1"). The private IP is fully ignored: not recorded, not blocked + // in lockdown, and the pending port is still consumed. + ServiceConfigStore.updateFromAPIResponse(new APIResponse( + true, null, 0L, null, null, null, true, List.of(), true, true, List.of() + )); + PendingHostnamesStore.add("10.0.0.1", 8080); + InetAddress[] resolved = new InetAddress[]{InetAddress.getByName("10.0.0.1")}; + + assertDoesNotThrow(() -> DNSRecordCollector.report("10.0.0.1", resolved)); + assertEquals(0, HostnamesStore.getHostnamesAsList().length); + assertTrue(PendingHostnamesStore.getPorts("10.0.0.1").isEmpty()); + } }