From e4a22e932f0ae0cc1cd2748c725ecaef95d53ad0 Mon Sep 17 00:00:00 2001 From: dahn Date: Tue, 26 May 2026 12:11:03 +0200 Subject: [PATCH 1/2] Fix Prometheus exporter not recognizing ::1 as IPv6 loopback Normalize IPv6 addresses using InetAddress.getHostAddress() so that compressed forms like ::1 and full forms like 0:0:0:0:0:0:0:1 are treated as equivalent when checking the allowed IPs list. Fixes #6714 --- .../metrics/PrometheusExporterServerImpl.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterServerImpl.java b/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterServerImpl.java index d9f25d2f5772..2ccb2f785cac 100644 --- a/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterServerImpl.java +++ b/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterServerImpl.java @@ -26,9 +26,13 @@ import javax.inject.Inject; import java.io.IOException; import java.io.OutputStream; +import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; public class PrometheusExporterServerImpl extends ManagerBase implements PrometheusExporterServer, Configurable { @@ -47,11 +51,21 @@ private final class ExporterHandler implements HttpHandler { @Override public void handle(final HttpExchange httpExchange) throws IOException { - final String remoteClientAddress = httpExchange.getRemoteAddress().getAddress().toString().replace("/", ""); + final String remoteClientAddress = httpExchange.getRemoteAddress().getAddress().getHostAddress(); logger.debug("Prometheus exporter received client request from: " + remoteClientAddress); String response = "Forbidden"; int responseCode = 403; - if (Arrays.asList(PrometheusExporterAllowedAddresses.value().split(",")).contains(remoteClientAddress)) { + final List allowedAddresses = Arrays.stream(PrometheusExporterAllowedAddresses.value().split(",")) + .map(addr -> { + try { + return InetAddress.getByName(addr.trim()).getHostAddress(); + } catch (UnknownHostException e) { + logger.warn("Invalid IP address in prometheus.exporter.allowed.ips: " + addr.trim()); + return addr.trim(); + } + }) + .collect(Collectors.toList()); + if (allowedAddresses.contains(remoteClientAddress)) { prometheusExporter.updateMetrics(); response = prometheusExporter.getMetrics(); responseCode = 200; From 1b4db1a0e8a6067e17ad68b0ff0ca71624132489 Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Tue, 3 Nov 2026 15:34:29 +0100 Subject: [PATCH 2/2] prometheus: skip blank/non-IP allowlist entries, avoid DNS, cache parsed list --- .../metrics/PrometheusExporterServerImpl.java | 56 +++++++++++++++---- .../PrometheusExporterServerImplTest.java | 53 ++++++++++++++++++ 2 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 plugins/integrations/prometheus/src/test/java/org/apache/cloudstack/metrics/PrometheusExporterServerImplTest.java diff --git a/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterServerImpl.java b/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterServerImpl.java index 2ccb2f785cac..5c51994a0ceb 100644 --- a/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterServerImpl.java +++ b/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterServerImpl.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.metrics; import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.net.NetUtils; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; @@ -31,7 +32,9 @@ import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.Arrays; -import java.util.List; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; public class PrometheusExporterServerImpl extends ManagerBase implements PrometheusExporterServer, Configurable { @@ -41,6 +44,9 @@ public class PrometheusExporterServerImpl extends ManagerBase implements Prometh @Inject private PrometheusExporter prometheusExporter; + private String allowedAddressesConfig; + private Set allowedAddresses = Collections.emptySet(); + private final class ExporterHandler implements HttpHandler { private PrometheusExporter prometheusExporter; @@ -55,17 +61,7 @@ public void handle(final HttpExchange httpExchange) throws IOException { logger.debug("Prometheus exporter received client request from: " + remoteClientAddress); String response = "Forbidden"; int responseCode = 403; - final List allowedAddresses = Arrays.stream(PrometheusExporterAllowedAddresses.value().split(",")) - .map(addr -> { - try { - return InetAddress.getByName(addr.trim()).getHostAddress(); - } catch (UnknownHostException e) { - logger.warn("Invalid IP address in prometheus.exporter.allowed.ips: " + addr.trim()); - return addr.trim(); - } - }) - .collect(Collectors.toList()); - if (allowedAddresses.contains(remoteClientAddress)) { + if (getAllowedAddresses().contains(remoteClientAddress)) { prometheusExporter.updateMetrics(); response = prometheusExporter.getMetrics(); responseCode = 200; @@ -88,6 +84,42 @@ public void handle(final HttpExchange httpExchange) throws IOException { } } + private synchronized Set getAllowedAddresses() { + final String config = PrometheusExporterAllowedAddresses.value(); + if (!config.equals(allowedAddressesConfig)) { + allowedAddresses = normalizeAddresses(config); + allowedAddressesConfig = config; + } + return allowedAddresses; + } + + /** + * Normalizes a comma separated list of IP addresses to their canonical text form, so that + * equivalent notations (e.g. ::1 and 0:0:0:0:0:0:0:1) match the remote client address. + * Blank entries and anything that is not an IP literal are ignored, so no DNS resolution takes place. + */ + protected Set normalizeAddresses(final String addresses) { + return Arrays.stream(addresses.split(",")) + .map(String::trim) + .filter(addr -> !addr.isEmpty()) + .map(this::normalizeAddress) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + private String normalizeAddress(final String address) { + if (!NetUtils.isValidIp4(address) && !NetUtils.isValidIp6(address)) { + logger.warn("Ignoring invalid IP address [{}] in {}", address, PrometheusExporterAllowedAddresses.key()); + return null; + } + try { + return InetAddress.getByName(address).getHostAddress(); + } catch (UnknownHostException e) { + logger.warn("Ignoring IP address [{}] in {} that could not be parsed", address, PrometheusExporterAllowedAddresses.key(), e); + return null; + } + } + @Override public boolean start() { if (EnablePrometheusExporter.value()) { diff --git a/plugins/integrations/prometheus/src/test/java/org/apache/cloudstack/metrics/PrometheusExporterServerImplTest.java b/plugins/integrations/prometheus/src/test/java/org/apache/cloudstack/metrics/PrometheusExporterServerImplTest.java new file mode 100644 index 000000000000..835389f227f3 --- /dev/null +++ b/plugins/integrations/prometheus/src/test/java/org/apache/cloudstack/metrics/PrometheusExporterServerImplTest.java @@ -0,0 +1,53 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.metrics; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Set; + +import org.junit.Test; + +public class PrometheusExporterServerImplTest { + + private final PrometheusExporterServerImpl server = new PrometheusExporterServerImpl(); + + @Test + public void testNormalizeAddressesIpv6LoopbackForms() { + Set addresses = server.normalizeAddresses("::1"); + assertEquals(server.normalizeAddresses("0:0:0:0:0:0:0:1"), addresses); + assertTrue(addresses.contains("0:0:0:0:0:0:0:1")); + } + + @Test + public void testNormalizeAddressesTrimsEntries() { + Set addresses = server.normalizeAddresses(" 127.0.0.1 , 10.1.1.1"); + assertEquals(Set.of("127.0.0.1", "10.1.1.1"), addresses); + } + + @Test + public void testNormalizeAddressesIgnoresBlankEntries() { + assertTrue(server.normalizeAddresses("").isEmpty()); + assertEquals(Set.of("10.1.1.1"), server.normalizeAddresses(",10.1.1.1,, ,")); + } + + @Test + public void testNormalizeAddressesIgnoresNonIpEntries() { + assertEquals(Set.of("10.1.1.1"), server.normalizeAddresses("localhost,10.1.1.1,not-an-ip")); + } +}