This is an automated email from the ASF dual-hosted git repository. DaanHoogland pushed a commit to branch fix/prometheus-ipv6-normalization in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit 1b4db1a0e8a6067e17ad68b0ff0ca71624132489 Author: Daan Hoogland <[email protected]> AuthorDate: Tue Nov 3 15:34:29 2026 +0100 prometheus: skip blank/non-IP allowlist entries, avoid DNS, cache parsed list --- .../metrics/PrometheusExporterServerImpl.java | 56 +++++++++++++++++----- .../metrics/PrometheusExporterServerImplTest.java | 53 ++++++++++++++++++++ 2 files changed, 97 insertions(+), 12 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 2ccb2f785ca..5c51994a0ce 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.InetSocketAddress; 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<String> allowedAddresses = Collections.emptySet(); + private final class ExporterHandler implements HttpHandler { private PrometheusExporter prometheusExporter; @@ -55,17 +61,7 @@ public class PrometheusExporterServerImpl extends ManagerBase implements Prometh logger.debug("Prometheus exporter received client request from: " + remoteClientAddress); String response = "Forbidden"; int responseCode = 403; - final List<String> 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 class PrometheusExporterServerImpl extends ManagerBase implements Prometh } } + private synchronized Set<String> 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<String> 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 00000000000..835389f227f --- /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<String> 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<String> 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")); + } +}
