diff --git a/activemq-broker/src/main/java/org/apache/activemq/broker/Connector.java b/activemq-broker/src/main/java/org/apache/activemq/broker/Connector.java index bf6fd7bec76..2772f92b4a0 100644 --- a/activemq-broker/src/main/java/org/apache/activemq/broker/Connector.java +++ b/activemq-broker/src/main/java/org/apache/activemq/broker/Connector.java @@ -86,6 +86,44 @@ public interface Connector extends Service { long getMaxConnectionExceededCount(); + /** @return the configured remote address allow list, comma separated CIDRs or a file: URI, or null */ + String getAllowList(); + + /** @return the configured remote address deny list, comma separated CIDRs or a file: URI, or null */ + String getDenyList(); + + /** @return true if remote addresses are checked against the allow and deny lists */ + boolean isAllowDenyValidationEnabled(); + + /** Turn the remote address check on or off at runtime; the lists stay configured */ + void setAllowDenyValidationEnabled(boolean enabled); + + /** @return connections accepted by the remote address check since the last statistics reset */ + long getAllowedCount(); + + /** @return connections refused by the remote address check since the last statistics reset */ + long getDeniedCount(); + + /** @return number of valid CIDR entries loaded into the allow list */ + long getAllowListCount(); + + /** @return number of valid CIDR entries loaded into the deny list */ + long getDenyListCount(); + + /** @return number of allow list entries skipped because they were not valid CIDR blocks */ + long getAllowListInvalidCount(); + + /** @return number of deny list entries skipped because they were not valid CIDR blocks */ + long getDenyListInvalidCount(); + + /** + * Runs an IP literal or a CIDR block through the connector's allow/deny + * decision, ignoring whether enforcement is enabled. An address gets the exact + * decision a connection would; a block is allowed when an allow entry covers it + * and no deny entry covers the whole block. + */ + boolean allowed(String addressOrCidr); + boolean isAutoStart(); /** diff --git a/activemq-broker/src/main/java/org/apache/activemq/broker/TransportConnector.java b/activemq-broker/src/main/java/org/apache/activemq/broker/TransportConnector.java index 938165195c2..b92d683e0d9 100644 --- a/activemq-broker/src/main/java/org/apache/activemq/broker/TransportConnector.java +++ b/activemq-broker/src/main/java/org/apache/activemq/broker/TransportConnector.java @@ -43,7 +43,10 @@ import org.apache.activemq.transport.TransportServer; import org.apache.activemq.transport.discovery.DiscoveryAgent; import org.apache.activemq.transport.discovery.DiscoveryAgentFactory; +import org.apache.activemq.transport.tcp.TcpTransportServer; +import org.apache.activemq.util.CidrListLoader; import org.apache.activemq.util.ExceptionUtils; +import org.apache.activemq.util.RemoteAddressValidator; import org.apache.activemq.util.ServiceStopper; import org.apache.activemq.util.ServiceSupport; import org.slf4j.Logger; @@ -80,6 +83,15 @@ public class TransportConnector implements Connector, BrokerServiceAware { private int maximumConsumersAllowedPerConnection = Integer.MAX_VALUE; private PublishedAddressPolicy publishedAddressPolicy = new PublishedAddressPolicy(); private boolean allowLinkStealing = false; + // remote address allow/deny lists; see setAllowList / setDenyList + private String allowList; + private String denyList; + private boolean allowDenyValidationEnabled = false; + private RemoteAddressValidator remoteAddressValidator; + private long allowListCount; + private long denyListCount; + private long allowListInvalidCount; + private long denyListInvalidCount; private boolean warnOnRemoteClose = false; private boolean displayStackTrace = false; private boolean autoStart = true; @@ -134,6 +146,9 @@ public ManagedTransportConnector asManagedConnector(ManagementContext context, O rc.setMaximumProducersAllowedPerConnection(getMaximumProducersAllowedPerConnection()); rc.setPublishedAddressPolicy(getPublishedAddressPolicy()); rc.setAllowLinkStealing(allowLinkStealing); + rc.setAllowList(allowList); + rc.setDenyList(denyList); + rc.setAllowDenyValidationEnabled(allowDenyValidationEnabled); rc.setWarnOnRemoteClose(isWarnOnRemoteClose()); rc.setAutoStart(isAutoStart()); return rc; @@ -268,6 +283,7 @@ private void onAcceptError(Exception error, String remoteHost) { } } }); + installRemoteAddressValidator(); getServer().setBrokerInfo(brokerInfo); getServer().start(); @@ -620,6 +636,104 @@ public void setAllowLinkStealing(boolean allowLinkStealing) { this.allowLinkStealing = allowLinkStealing; } + /** + * Loads the allow and deny lists, builds the validator and hands it to the + * transport server. Only socket based servers (tcp, nio, ssl, auto and the + * protocol variants built on them) can check remote addresses. + */ + private void installRemoteAddressValidator() throws Exception { + var allow = CidrListLoader.load(allowList, getName() + " allowList"); + var deny = CidrListLoader.load(denyList, getName() + " denyList"); + allowListCount = allow.cidrs().size(); + denyListCount = deny.cidrs().size(); + allowListInvalidCount = allow.invalidCount(); + denyListInvalidCount = deny.invalidCount(); + remoteAddressValidator = new RemoteAddressValidator(allow.cidrs(), deny.cidrs()); + remoteAddressValidator.setEnabled(allowDenyValidationEnabled); + var transportServer = getServer(); + if (transportServer instanceof TcpTransportServer) { + ((TcpTransportServer) transportServer).setRemoteAddressValidator(remoteAddressValidator); + } else if (allowList != null || denyList != null) { + LOG.warn("allowList/denyList configured on connector {} but its transport does not support remote address validation", getName()); + } + } + + @Override + public String getAllowList() { + return allowList; + } + + /** + * CIDR blocks that remote addresses must fall within to connect, as a comma + * separated list (e.g. {@code 10.0.0.0/8,192.168.1.0/24}) or a {@code file:} URI + * to a file with one CIDR block per line. {@code ${activemq.conf}} and + * {@code ${activemq.data}} may be used in the URI. Empty means no restriction + * beyond the deny list. + */ + public void setAllowList(String allowList) { + this.allowList = allowList; + } + + @Override + public String getDenyList() { + return denyList; + } + + /** + * CIDR blocks that are refused regardless of the allow list, in the same + * comma separated or {@code file:} URI form as the allow list. Deny entries + * are checked first. + */ + public void setDenyList(String denyList) { + this.denyList = denyList; + } + + @Override + public boolean isAllowDenyValidationEnabled() { + return allowDenyValidationEnabled; + } + + /** + * Turns the allow/deny check on or off without removing the lists. Default + * false, so configured lists take effect only when this is set; the default + * may change in a future major release. May be changed at runtime through JMX. + */ + @Override + public void setAllowDenyValidationEnabled(boolean allowDenyValidationEnabled) { + this.allowDenyValidationEnabled = allowDenyValidationEnabled; + var validator = remoteAddressValidator; + if (validator != null) { + validator.setEnabled(allowDenyValidationEnabled); + } + } + + @Override + public long getAllowListCount() { + return allowListCount; + } + + @Override + public long getDenyListCount() { + return denyListCount; + } + + @Override + public long getAllowListInvalidCount() { + return allowListInvalidCount; + } + + @Override + public long getDenyListInvalidCount() { + return denyListInvalidCount; + } + + @Override + public boolean allowed(String addressOrCidr) { + var validator = remoteAddressValidator; + // before start no lists are in effect, so nothing is refused + return validator == null || validator.isAllowed(addressOrCidr); + } + @Override public boolean isAuditNetworkProducers() { return auditNetworkProducers; @@ -693,6 +807,16 @@ public long getMaxConnectionExceededCount() { return (server != null ? server.getMaxConnectionExceededCount() : 0l); } + @Override + public long getAllowedCount() { + return server instanceof TcpTransportServer ? ((TcpTransportServer) server).getAllowedCount() : 0L; + } + + @Override + public long getDeniedCount() { + return server instanceof TcpTransportServer ? ((TcpTransportServer) server).getDeniedCount() : 0L; + } + @Override public boolean isStarted() { return started.get(); diff --git a/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorView.java b/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorView.java index acd00377d04..a34b05d8a1f 100644 --- a/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorView.java +++ b/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorView.java @@ -145,6 +145,61 @@ public long getMaxConnectionExceededCount() { return this.connector.getMaxConnectionExceededCount(); } + @Override + public String getAllowList() { + return this.connector.getAllowList(); + } + + @Override + public String getDenyList() { + return this.connector.getDenyList(); + } + + @Override + public boolean isAllowDenyValidationEnabled() { + return this.connector.isAllowDenyValidationEnabled(); + } + + @Override + public void setAllowDenyValidationEnabled(boolean enabled) { + this.connector.setAllowDenyValidationEnabled(enabled); + } + + @Override + public long getAllowedCount() { + return this.connector.getAllowedCount(); + } + + @Override + public long getDeniedCount() { + return this.connector.getDeniedCount(); + } + + @Override + public long getAllowListCount() { + return this.connector.getAllowListCount(); + } + + @Override + public long getDenyListCount() { + return this.connector.getDenyListCount(); + } + + @Override + public long getAllowListInvalidCount() { + return this.connector.getAllowListInvalidCount(); + } + + @Override + public long getDenyListInvalidCount() { + return this.connector.getDenyListInvalidCount(); + } + + @Override + public boolean allowed(String addressOrCidr) { + return this.connector.allowed(addressOrCidr); + } + @Override public boolean isAutoStart() { return this.connector.isAutoStart(); diff --git a/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorViewMBean.java b/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorViewMBean.java index 4d040225619..6b16c3888a1 100644 --- a/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorViewMBean.java +++ b/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorViewMBean.java @@ -95,6 +95,39 @@ public interface ConnectorViewMBean extends Service { @MBeanInfo("Max connection exceeded count") long getMaxConnectionExceededCount(); + @MBeanInfo("Remote address allow list: comma separated CIDR blocks or a file: URI") + String getAllowList(); + + @MBeanInfo("Remote address deny list: comma separated CIDR blocks or a file: URI") + String getDenyList(); + + @MBeanInfo("Remote address allow/deny validation enabled") + boolean isAllowDenyValidationEnabled(); + + @MBeanInfo("Enable or disable remote address allow/deny validation; the lists stay configured") + void setAllowDenyValidationEnabled(boolean enabled); + + @MBeanInfo("Connections accepted by the remote address check") + long getAllowedCount(); + + @MBeanInfo("Connections refused by the remote address check") + long getDeniedCount(); + + @MBeanInfo("Valid CIDR entries loaded into the allow list") + long getAllowListCount(); + + @MBeanInfo("Valid CIDR entries loaded into the deny list") + long getDenyListCount(); + + @MBeanInfo("Allow list entries skipped as invalid") + long getAllowListInvalidCount(); + + @MBeanInfo("Deny list entries skipped as invalid") + long getDenyListInvalidCount(); + + @MBeanInfo("Would the IP address, or the CIDR block as a whole, be allowed by the allow/deny lists (enabled flag ignored)") + boolean allowed(@MBeanInfo("addressOrCidr") String addressOrCidr); + /** * @return true if transport connector auto start is enabled */ diff --git a/activemq-client/src/main/java/org/apache/activemq/transport/tcp/TcpTransportServer.java b/activemq-client/src/main/java/org/apache/activemq/transport/tcp/TcpTransportServer.java index 32cc6261c25..a446384c258 100644 --- a/activemq-client/src/main/java/org/apache/activemq/transport/tcp/TcpTransportServer.java +++ b/activemq-client/src/main/java/org/apache/activemq/transport/tcp/TcpTransportServer.java @@ -56,6 +56,7 @@ import org.apache.activemq.util.IOExceptionSupport; import org.apache.activemq.util.InetAddressUtil; import org.apache.activemq.util.IntrospectionSupport; +import org.apache.activemq.util.RemoteAddressValidator; import org.apache.activemq.util.ServiceListener; import org.apache.activemq.util.ServiceStopper; import org.apache.activemq.util.ServiceSupport; @@ -124,7 +125,15 @@ public class TcpTransportServer extends TransportServerThreadSupport implements * The maximum number of sockets allowed for this server */ protected int maximumConnections = Integer.MAX_VALUE; - protected final AtomicLong maximumConnectionsExceededCount = new AtomicLong(0l); + protected final AtomicLong maximumConnectionsExceededCount = new AtomicLong(0L); + + /** + * Optional allow/deny check applied to the remote address of every accepted + * socket before any other processing. Null means no check. + */ + protected volatile RemoteAddressValidator remoteAddressValidator; + protected final AtomicLong allowedCount = new AtomicLong(0L); + protected final AtomicLong deniedCount = new AtomicLong(0L); protected final AtomicInteger currentTransportCount = new AtomicInteger(); public TcpTransportServer(TcpTransportFactory transportFactory, URI location, ServerSocketFactory serverSocketFactory) throws IOException, @@ -577,6 +586,24 @@ final protected void doHandleSocket(Socket socket) { boolean closeSocket = true; boolean countIncremented = false; try { + + // A refused remote address is policy at work, not an error: count it, close the + // socket without writing anything, and stay out of the accept error path. One + // line per attempt is kept at DEBUG so a scanner cannot flood the log. + var validator = remoteAddressValidator; + if (validator != null && validator.isEnabled()) { + if (!validator.isAllowed(socket)) { + deniedCount.incrementAndGet(); + LOG.debug("Refused connection from {}: remote address not allowed", socket.getRemoteSocketAddress()); + try { + socket.close(); + } catch (IOException ignore) { + } + return; + } + allowedCount.incrementAndGet(); + } + int currentCount; do { currentCount = currentTransportCount.get(); @@ -735,8 +762,28 @@ public long getMaxConnectionExceededCount() { return this.maximumConnectionsExceededCount.get(); } + public RemoteAddressValidator getRemoteAddressValidator() { + return remoteAddressValidator; + } + + public void setRemoteAddressValidator(RemoteAddressValidator remoteAddressValidator) { + this.remoteAddressValidator = remoteAddressValidator; + } + + /** connections accepted by the remote address check since the last reset */ + public long getAllowedCount() { + return allowedCount.get(); + } + + /** connections refused by the remote address check since the last reset */ + public long getDeniedCount() { + return deniedCount.get(); + } + @Override public void resetStatistics() { - this.maximumConnectionsExceededCount.set(0l); + this.maximumConnectionsExceededCount.set(0L); + this.allowedCount.set(0L); + this.deniedCount.set(0L); } } diff --git a/activemq-client/src/main/java/org/apache/activemq/util/Cidr.java b/activemq-client/src/main/java/org/apache/activemq/util/Cidr.java new file mode 100644 index 00000000000..9f2fce51169 --- /dev/null +++ b/activemq-client/src/main/java/org/apache/activemq/util/Cidr.java @@ -0,0 +1,130 @@ +/** + * 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.activemq.util; + +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Objects; + +/** + * An IPv4 or IPv6 network block in CIDR notation. + * + *
Deliberately a plain class rather than a record: the activemq-spring schema
+ * generator parses this module's sources with QDox 1.x, which does not understand
+ * record declarations and fails the activemq-spring build on one.
+ */
+public final class Cidr {
+
+ private final String cidr;
+ private final byte[] networkAddress;
+ private final byte[] mask;
+
+ public Cidr(String cidr, byte[] networkAddress, byte[] mask) {
+ this.cidr = cidr;
+ this.networkAddress = networkAddress;
+ this.mask = mask;
+ }
+
+ /** the block as written, e.g. {@code 192.168.1.0/24} */
+ public String cidr() {
+ return cidr;
+ }
+
+ /** the network address with the mask applied */
+ public byte[] networkAddress() {
+ return networkAddress;
+ }
+
+ public byte[] mask() {
+ return mask;
+ }
+
+ /** number of leading one bits in the mask, i.e. the prefix length after the slash */
+ public int prefixLength() {
+ var bits = 0;
+ for (var b : mask) {
+ bits += Integer.bitCount(b & 0xFF);
+ }
+ return bits;
+ }
+
+ /** Returns {@code true} if every address of {@code other} falls within this block. */
+ boolean covers(final Cidr other) {
+ if (other.networkAddress.length != networkAddress.length || other.prefixLength() < prefixLength()) {
+ return false;
+ }
+ return Arrays.equals(applyMask(other.networkAddress, mask), networkAddress);
+ }
+
+ /** Returns {@code true} if {@code address} falls within this block. */
+ boolean contains(final InetAddress address) {
+ var raw = address.getAddress();
+ if (raw.length != networkAddress.length) {
+ // IPv4 vs IPv6 mismatch — never match.
+ return false;
+ }
+ var masked = applyMask(raw, mask);
+ return Arrays.equals(masked, networkAddress);
+ }
+
+ static byte[] buildMask(final int byteLen, final int prefixBits) {
+ var m = new byte[byteLen];
+ for (var i = 0; i < byteLen; i++) {
+ var bitsLeft = prefixBits - i * 8;
+ if (bitsLeft >= 8) {
+ m[i] = (byte) 0xFF;
+ } else if (bitsLeft > 0) {
+ m[i] = (byte) (0xFF & (0xFF << (8 - bitsLeft)));
+ } else {
+ m[i] = 0;
+ }
+ }
+ return m;
+ }
+
+ static byte[] applyMask(final byte[] addr, final byte[] mask) {
+ var result = new byte[addr.length];
+ for (var i = 0; i < addr.length; i++) {
+ result[i] = (byte) (addr[i] & mask[i]);
+ }
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Cidr)) {
+ return false;
+ }
+ var that = (Cidr) o;
+ return Objects.equals(cidr, that.cidr)
+ && Arrays.equals(networkAddress, that.networkAddress)
+ && Arrays.equals(mask, that.mask);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * (31 * Objects.hashCode(cidr) + Arrays.hashCode(networkAddress)) + Arrays.hashCode(mask);
+ }
+
+ @Override
+ public String toString() {
+ return cidr;
+ }
+}
diff --git a/activemq-client/src/main/java/org/apache/activemq/util/CidrConverter.java b/activemq-client/src/main/java/org/apache/activemq/util/CidrConverter.java
new file mode 100644
index 00000000000..69899be454f
--- /dev/null
+++ b/activemq-client/src/main/java/org/apache/activemq/util/CidrConverter.java
@@ -0,0 +1,111 @@
+/**
+ * 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.activemq.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class CidrConverter {
+
+ private static final Logger logger = LoggerFactory.getLogger(CidrConverter.class);
+
+ public static List Malformed CIDR entries are logged and skipped, and their number is reported
+ * on the result. Anything wrong with the file reference itself (scheme, path
+ * traversal, missing file, more than {@value #MAX_ENTRIES} entries or more than
+ * {@value #MAX_FILE_BYTES} bytes) is a configuration error and throws.
+ */
+public final class CidrListLoader {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CidrListLoader.class);
+
+ public static final int MAX_ENTRIES = 100_000;
+ public static final long MAX_FILE_BYTES = 10L * 1024 * 1024;
+
+ private static final String FILE_PREFIX = "file:";
+ private static final Map Evaluation order:
+ * Both IPv4 and IPv6 addresses are supported. IPv4-mapped IPv6 addresses
+ * (e.g. {@code ::ffff:192.168.1.1}) are automatically normalized to their
+ * IPv4 equivalent before matching, so a single IPv4 CIDR rule covers both
+ * plain IPv4 connections and dual-stack sockets that represent the same peer.
+ *
+ * Build the lists with {@link CidrListLoader}, which accepts a comma separated
+ * string or a {@code file:} URI and reports entries it had to skip.
+ */
+public class RemoteAddressValidator {
+
+ private final List The JDK does not expose a public API for this, so we detect the
+ * 16-byte pattern {@code [0,0,0,0, 0,0,0,0, 0,0,0xFF,0xFF, a,b,c,d]}
+ * manually and reconstruct the IPv4 address from the last four bytes.
+ */
+ static InetAddress normalise(final InetAddress addr) {
+ if (!(addr instanceof Inet6Address)) {
+ return addr;
+ }
+ var raw = addr.getAddress(); // always 16 bytes for Inet6Address
+ // Check for the ::ffff:0:0/96 prefix (bytes 0-9 = 0x00, bytes 10-11 = 0xFF)
+ for (var i = 0; i < 10; i++) {
+ if (raw[i] != 0) return addr;
+ }
+ if ((raw[10] & 0xFF) != 0xFF || (raw[11] & 0xFF) != 0xFF) {
+ return addr;
+ }
+ // Extract the embedded IPv4 address from the last 4 bytes.
+ var ipv4 = new byte[]{raw[12], raw[13], raw[14], raw[15]};
+ try {
+ return InetAddress.getByAddress(ipv4);
+ } catch (UnknownHostException e) {
+ // Should never happen for a 4-byte array; return original if it does.
+ return addr;
+ }
+ }
+
+}
diff --git a/activemq-client/src/test/java/org/apache/activemq/util/CidrConverterTest.java b/activemq-client/src/test/java/org/apache/activemq/util/CidrConverterTest.java
new file mode 100644
index 00000000000..21a677b3d23
--- /dev/null
+++ b/activemq-client/src/test/java/org/apache/activemq/util/CidrConverterTest.java
@@ -0,0 +1,130 @@
+/**
+ * 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.activemq.util;
+
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class CidrConverterTest {
+
+ @Test
+ public void testHostNamesAreNotResolved() {
+ // a CIDR must start with an IP literal; a host name is a configuration error, never a DNS lookup
+ assertThrows(IllegalArgumentException.class, () -> CidrConverter.fromString("localhost/32"));
+ assertThrows(IllegalArgumentException.class, () -> CidrConverter.fromString("example.com/24"));
+ assertEquals(0, CidrConverter.parseCidrStrings(java.util.List.of("example.com/24"), "test").size());
+ assertNotNull(CidrConverter.parseIpLiteral("192.168.1.1"));
+ assertNotNull(CidrConverter.parseIpLiteral("2001:db8::1"));
+ assertThrows(IllegalArgumentException.class, () -> CidrConverter.parseIpLiteral("localhost"));
+ }
+
+ @Test
+ public void fromStringValid() {
+ var cidr = CidrConverter.fromString("192.168.1.1/32");
+ assertEquals("192.168.1.1/32", cidr.cidr());
+ assertArrayEquals(new byte[]{-1, -1, -1, -1}, cidr.mask());
+ assertArrayEquals(new byte[]{-64, -88, 1, 1}, cidr.networkAddress());
+
+ cidr = CidrConverter.fromString("192.168.1.0/24");
+ assertEquals("192.168.1.0/24", cidr.cidr());
+ assertArrayEquals(new byte[]{-1, -1, -1, 0}, cidr.mask());
+ assertArrayEquals(new byte[]{-64, -88, 1, 0}, cidr.networkAddress());
+
+ cidr = CidrConverter.fromString("192.168.0.0/16");
+ assertEquals("192.168.0.0/16", cidr.cidr());
+ assertArrayEquals(new byte[]{-1, -1, 0, 0}, cidr.mask());
+ assertArrayEquals(new byte[]{-64, -88, 0, 0}, cidr.networkAddress());
+
+ cidr = CidrConverter.fromString("192.0.0.0/8");
+ assertEquals("192.0.0.0/8", cidr.cidr());
+ assertArrayEquals(new byte[]{-1, 0, 0, 0}, cidr.mask());
+ assertArrayEquals(new byte[]{-64, 0, 0, 0}, cidr.networkAddress());
+
+ cidr = CidrConverter.fromString("0.0.0.0/0");
+ assertEquals("0.0.0.0/0", cidr.cidr());
+ assertArrayEquals(new byte[]{0, 0, 0, 0}, cidr.mask());
+ assertArrayEquals(new byte[]{0, 0, 0, 0}, cidr.networkAddress());
+
+ cidr = CidrConverter.fromString("127.0.0.1/32");
+ assertEquals("127.0.0.1/32", cidr.cidr());
+ assertArrayEquals(new byte[]{-1, -1, -1, -1}, cidr.mask());
+ assertArrayEquals(new byte[]{127, 0, 0, 1}, cidr.networkAddress());
+ }
+
+ @Test
+ public void fromStringInvalid() {
+ var invalidInputs = new String[]{"not-an-ip/16", "1.2.3.4/-32", "1.2.3.4/33", "1.2.3.4/a", "1.2.3.4", "invalid3", "", "256.3.5.2/32"};
+
+ for (var input : invalidInputs) {
+ assertThrows(IllegalArgumentException.class, () -> {
+ CidrConverter.fromString(input);
+ });
+ }
+ }
+
+ @Test
+ public void parseCidrStrings() {
+ var cidrs = CidrConverter.parseCidrStrings(null, "empty.txt");
+ assertNotNull(cidrs);
+ assertTrue(cidrs.isEmpty());
+
+ cidrs = CidrConverter.parseCidrStrings(List.of(), "empty.txt");
+ assertNotNull(cidrs);
+ assertTrue(cidrs.isEmpty());
+
+ cidrs = CidrConverter.parseCidrStrings(List.of(""), "empty.txt");
+ assertNotNull(cidrs);
+ assertTrue(cidrs.isEmpty());
+
+ cidrs = CidrConverter.parseCidrStrings(List.of("192.168.1.1/32", "192.168.2.0/24", "10.10.0.0/16", "11.0.0.0/8"), "four.txt");
+ assertNotNull(cidrs);
+ assertFalse(cidrs.isEmpty());
+ assertEquals(Integer.valueOf(4), Integer.valueOf(cidrs.size()));
+
+ cidrs = CidrConverter.parseCidrStrings(List.of("192.168.1.1/32", "192.168.2.0/24", "10.10.0.0/16", "1.2.3.4/-32"), "four-one-invalid.txt");
+ assertNotNull(cidrs);
+ assertFalse(cidrs.isEmpty());
+ assertEquals(Integer.valueOf(3), Integer.valueOf(cidrs.size()));
+ }
+// {"192.168.1.50", "ALLOW — in allow /24, not denied"},
+// {"192.168.1.100", "DENY — explicit deny /32"},
+// {"10.5.6.7", "ALLOW — in allow 10/8, not denied"},
+// {"10.0.0.1", "DENY — explicit deny /32"},
+// {"10.255.255.99", "DENY — in denied /24 subnet"},
+// {"172.16.5.1", "ALLOW — in allow 172.16/12"},
+// {"8.8.8.8", "DENY — not in any allow entry"},
+// // ---- IPv6 ----
+// {"::1", "ALLOW — loopback in allow list"},
+// {"2001:db8::1", "ALLOW — in allow 2001:db8::/32"},
+// {"2001:db8:bad::1", "DENY — in deny 2001:db8:bad::/48"},
+// {"fe80::1", "ALLOW — link-local in allow fe80::/10"},
+// {"2001:db9::1", "DENY — not in any allow entry"},
+// // ---- IPv4-mapped IPv6 (dual-stack sockets) ----
+// {"::ffff:192.168.1.50", "ALLOW — mapped; normalised to 192.168.1.50"},
+// {"::ffff:192.168.1.100","DENY — mapped; normalised, hits deny /32"},
+// {"::ffff:8.8.8.8", "DENY — mapped; normalised, not in allow"},
+// };
+
+}
diff --git a/activemq-client/src/test/java/org/apache/activemq/util/CidrListLoaderTest.java b/activemq-client/src/test/java/org/apache/activemq/util/CidrListLoaderTest.java
new file mode 100644
index 00000000000..139acfdc232
--- /dev/null
+++ b/activemq-client/src/test/java/org/apache/activemq/util/CidrListLoaderTest.java
@@ -0,0 +1,166 @@
+/**
+ * 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.activemq.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import java.io.RandomAccessFile;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CidrListLoaderTest {
+
+ private Path dir;
+ private String savedConf;
+
+ @Before
+ public void setUp() throws Exception {
+ dir = Files.createDirectories(Path.of("target", "cidr-list-loader-test", Long.toString(System.nanoTime())));
+ savedConf = System.getProperty("activemq.conf");
+ }
+
+ @After
+ public void tearDown() {
+ if (savedConf == null) {
+ System.clearProperty("activemq.conf");
+ } else {
+ System.setProperty("activemq.conf", savedConf);
+ }
+ }
+
+ private Path write(String name, String content) throws Exception {
+ return Files.writeString(dir.resolve(name), content, StandardCharsets.UTF_8);
+ }
+
+ private static String uri(Path path) {
+ return "file:" + path.toAbsolutePath();
+ }
+
+ @Test
+ public void testCommaSeparatedValueCountsInvalidEntries() {
+ var list = CidrListLoader.load("10.0.0.0/8, 192.168.1.0/24 ,bogus,,", "test");
+ assertEquals(2, list.cidrs().size());
+ assertEquals(1, list.invalidCount());
+ assertEquals("10.0.0.0/8", list.cidrs().get(0).cidr());
+ }
+
+ @Test
+ public void testNullOrBlankIsEmpty() {
+ assertTrue(CidrListLoader.load(null, "test").cidrs().isEmpty());
+ assertTrue(CidrListLoader.load(" ", "test").cidrs().isEmpty());
+ assertEquals(0, CidrListLoader.load("", "test").invalidCount());
+ }
+
+ @Test
+ public void testFileWithCommentsBlanksAndOneBadLine() throws Exception {
+ var file = write("allow.txt", "# leading comment\n\n10.0.0.0/8 # trailing comment\n192.168.1.0/24\nnot-a-cidr\n \n2001:db8::/32\n");
+ var list = CidrListLoader.load(uri(file), "test");
+ assertEquals(3, list.cidrs().size());
+ assertEquals(1, list.invalidCount());
+ }
+
+ @Test
+ public void testFileUriWithTripleSlash() throws Exception {
+ var file = write("deny.txt", "10.0.0.0/8\n");
+ var list = CidrListLoader.load("file://" + file.toAbsolutePath(), "test");
+ assertEquals(1, list.cidrs().size());
+ }
+
+ @Test
+ public void testMacroExpansion() throws Exception {
+ System.setProperty("activemq.conf", dir.toAbsolutePath().toString());
+ write("allow.txt", "10.0.0.0/8\n172.16.0.0/12\n");
+ var list = CidrListLoader.load("file:${activemq.conf}/allow.txt", "test");
+ assertEquals(2, list.cidrs().size());
+ }
+
+ @Test
+ public void testUnsupportedMacroRejected() {
+ var e = assertThrows(IllegalArgumentException.class,
+ () -> CidrListLoader.load("file:${user.home}/allow.txt", "test"));
+ assertTrue(e.getMessage(), e.getMessage().contains("Unsupported macro"));
+ }
+
+ @Test
+ public void testUnsetMacroPropertyRejected() {
+ System.clearProperty("activemq.conf");
+ assertThrows(IllegalArgumentException.class,
+ () -> CidrListLoader.load("file:${activemq.conf}/allow.txt", "test"));
+ }
+
+ @Test
+ public void testOnlyFileSchemeAccepted() throws Exception {
+ assertThrows(IllegalArgumentException.class, () -> CidrListLoader.load("http://example.com/allow.txt", "test"));
+ // an unknown scheme without a slash is treated as a CIDR list and simply yields invalid entries
+ var list = CidrListLoader.load("classpath:allow.txt", "test");
+ assertEquals(1, list.invalidCount());
+ }
+
+ @Test
+ public void testAuthorityQueryAndFragmentRejected() throws Exception {
+ var file = write("allow.txt", "10.0.0.0/8\n");
+ assertThrows(IllegalArgumentException.class, () -> CidrListLoader.load("file://remotehost" + file.toAbsolutePath(), "test"));
+ assertThrows(IllegalArgumentException.class, () -> CidrListLoader.load(uri(file) + "?x=1", "test"));
+ assertThrows(IllegalArgumentException.class, () -> CidrListLoader.load(uri(file) + "#frag", "test"));
+ }
+
+ @Test
+ public void testParentSegmentRejected() throws Exception {
+ write("allow.txt", "10.0.0.0/8\n");
+ var inner = Files.createDirectories(dir.resolve("inner"));
+ System.setProperty("activemq.conf", inner.toAbsolutePath().toString());
+ assertThrows(IllegalArgumentException.class,
+ () -> CidrListLoader.load("file:${activemq.conf}/../allow.txt", "test"));
+ assertThrows(IllegalArgumentException.class,
+ () -> CidrListLoader.load(uri(inner) + "/../allow.txt", "test"));
+ }
+
+ @Test
+ public void testMissingFileRejected() {
+ var e = assertThrows(IllegalArgumentException.class,
+ () -> CidrListLoader.load(uri(dir.resolve("nope.txt")), "test"));
+ assertTrue(e.getMessage(), e.getMessage().contains("not a readable regular file"));
+ }
+
+ @Test
+ public void testTooManyEntriesRejected() throws Exception {
+ var content = IntStream.range(0, CidrListLoader.MAX_ENTRIES + 1)
+ .mapToObj(i -> "10.0.0.1/32\n").collect(Collectors.joining());
+ var file = write("huge.txt", content);
+ var e = assertThrows(IllegalArgumentException.class, () -> CidrListLoader.load(uri(file), "test"));
+ assertTrue(e.getMessage(), e.getMessage().contains("more than " + CidrListLoader.MAX_ENTRIES));
+ }
+
+ @Test
+ public void testFileOverSizeLimitRejected() throws Exception {
+ var file = dir.resolve("big.txt");
+ try (var raf = new RandomAccessFile(file.toFile(), "rw")) {
+ raf.setLength(CidrListLoader.MAX_FILE_BYTES + 1);
+ }
+ var e = assertThrows(IllegalArgumentException.class, () -> CidrListLoader.load(uri(file), "test"));
+ assertTrue(e.getMessage(), e.getMessage().contains("larger than"));
+ }
+}
diff --git a/activemq-client/src/test/java/org/apache/activemq/util/RemoteAddressValidatorTest.java b/activemq-client/src/test/java/org/apache/activemq/util/RemoteAddressValidatorTest.java
new file mode 100644
index 00000000000..8cf0109129e
--- /dev/null
+++ b/activemq-client/src/test/java/org/apache/activemq/util/RemoteAddressValidatorTest.java
@@ -0,0 +1,141 @@
+/**
+ * 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.activemq.util;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.List;
+
+import org.junit.Test;
+
+public class RemoteAddressValidatorTest {
+
+ private static List
+ *
+ *
+ *