Subnet-Based Peer Permissions (#7168)

Signed-off-by: Gabriel-Trintinalia <gabriel.trintinalia@consensys.net>
This commit is contained in:
Gabriel-Trintinalia
2024-06-13 14:44:29 +10:00
committed by GitHub
parent 90d2db9736
commit e3e86c7ef6
14 changed files with 342 additions and 2 deletions

View File

@@ -26,6 +26,7 @@
- Support for eth_maxPriorityFeePerGas [#5658](https://github.com/hyperledger/besu/issues/5658)
- Enable continuous profiling with default setting [#7006](https://github.com/hyperledger/besu/pull/7006)
- A full and up to date implementation of EOF for Prague [#7169](https://github.com/hyperledger/besu/pull/7169)
- Add Subnet-Based Peer Permissions. [#7168](https://github.com/hyperledger/besu/pull/7168)
### Bug fixes
- Make `eth_gasPrice` aware of the base fee market [#7102](https://github.com/hyperledger/besu/pull/7102)

View File

@@ -80,6 +80,7 @@ dependencies {
implementation 'org.xerial.snappy:snappy-java'
implementation 'tech.pegasys:jc-kzg-4844'
implementation 'org.rocksdb:rocksdbjni'
implementation 'commons-net:commons-net'
runtimeOnly 'org.apache.logging.log4j:log4j-jul'
runtimeOnly 'com.splunk.logging:splunk-library-javalogging'

View File

@@ -88,6 +88,7 @@ import org.hyperledger.besu.ethereum.p2p.network.P2PNetwork;
import org.hyperledger.besu.ethereum.p2p.network.ProtocolManager;
import org.hyperledger.besu.ethereum.p2p.peers.DefaultPeer;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeDnsConfiguration;
import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissionSubnet;
import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissions;
import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissionsDenylist;
import org.hyperledger.besu.ethereum.p2p.rlpx.connections.netty.TLSConfiguration;
@@ -146,6 +147,7 @@ import com.google.common.base.Strings;
import graphql.GraphQL;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import org.apache.commons.net.util.SubnetUtils.SubnetInfo;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
import org.slf4j.Logger;
@@ -192,6 +194,7 @@ public class RunnerBuilder {
private JsonRpcIpcConfiguration jsonRpcIpcConfiguration;
private boolean legacyForkIdEnabled;
private Optional<EnodeDnsConfiguration> enodeDnsConfiguration;
private List<SubnetInfo> allowedSubnets = new ArrayList<>();
/** Instantiates a new Runner builder. */
public RunnerBuilder() {}
@@ -589,6 +592,17 @@ public class RunnerBuilder {
return this;
}
/**
* Add subnet configuration
*
* @param allowedSubnets the allowedSubnets
* @return the runner builder
*/
public RunnerBuilder allowedSubnets(final List<SubnetInfo> allowedSubnets) {
this.allowedSubnets = allowedSubnets;
return this;
}
/**
* Build Runner instance.
*
@@ -648,6 +662,10 @@ public class RunnerBuilder {
final PeerPermissionsDenylist bannedNodes = PeerPermissionsDenylist.create();
bannedNodeIds.forEach(bannedNodes::add);
PeerPermissionSubnet peerPermissionSubnet = new PeerPermissionSubnet(allowedSubnets);
final PeerPermissions defaultPeerPermissions =
PeerPermissions.combine(peerPermissionSubnet, bannedNodes);
final List<EnodeURL> bootnodes = discoveryConfiguration.getBootnodes();
final Synchronizer synchronizer = besuController.getSynchronizer();
@@ -667,8 +685,8 @@ public class RunnerBuilder {
final PeerPermissions peerPermissions =
nodePermissioningController
.map(nodePC -> new PeerPermissionsAdapter(nodePC, bootnodes, context.getBlockchain()))
.map(nodePerms -> PeerPermissions.combine(nodePerms, bannedNodes))
.orElse(bannedNodes);
.map(nodePerms -> PeerPermissions.combine(nodePerms, defaultPeerPermissions))
.orElse(defaultPeerPermissions);
LOG.info("Detecting NAT service.");
final boolean fallbackEnabled = natMethod == NatMethod.AUTO || natMethodFallbackEnabled;

View File

@@ -43,6 +43,7 @@ import org.hyperledger.besu.cli.config.NetworkName;
import org.hyperledger.besu.cli.config.ProfileName;
import org.hyperledger.besu.cli.converter.MetricCategoryConverter;
import org.hyperledger.besu.cli.converter.PercentageConverter;
import org.hyperledger.besu.cli.converter.SubnetInfoConverter;
import org.hyperledger.besu.cli.custom.JsonRPCAllowlistHostsProperty;
import org.hyperledger.besu.cli.error.BesuExecutionExceptionHandler;
import org.hyperledger.besu.cli.error.BesuParameterExceptionHandler;
@@ -243,6 +244,7 @@ import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.json.DecodeException;
import io.vertx.core.metrics.MetricsOptions;
import org.apache.commons.net.util.SubnetUtils.SubnetInfo;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
import org.slf4j.Logger;
@@ -527,6 +529,15 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
return autoDiscoveredDefaultIP;
}
@Option(
names = {"--net-restrict"},
arity = "1..*",
split = ",",
converter = SubnetInfoConverter.class,
description =
"Comma-separated list of allowed IP subnets (e.g., '192.168.1.0/24,10.0.0.0/8').")
private List<SubnetInfo> allowedSubnets;
}
@Option(
@@ -2320,6 +2331,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
.storageProvider(keyValueStorageProvider(keyValueStorageName))
.rpcEndpointService(rpcEndpointServiceImpl)
.enodeDnsConfiguration(getEnodeDnsConfiguration())
.allowedSubnets(p2PDiscoveryOptionGroup.allowedSubnets)
.build();
addShutdownHook(runner);

View File

@@ -0,0 +1,36 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.cli.converter;
import org.apache.commons.net.util.SubnetUtils;
import org.apache.commons.net.util.SubnetUtils.SubnetInfo;
import picocli.CommandLine;
/** The SubnetInfo converter for CLI options. */
public class SubnetInfoConverter implements CommandLine.ITypeConverter<SubnetInfo> {
/** Default Constructor. */
public SubnetInfoConverter() {}
/**
* Converts an IP addresses with CIDR notation into SubnetInfo
*
* @param value The IP addresses with CIDR notation.
* @return the SubnetInfo
*/
@Override
public SubnetInfo convert(final String value) {
return new SubnetUtils(value).getInfo();
}
}

View File

@@ -1217,6 +1217,28 @@ public class BesuCommandTest extends CommandTestAbstract {
.contains("Invalid value for option '--fast-sync-min-peers': 'ten' is not an int");
}
@Test
public void netRestrictParsedCorrectly() {
final String subnet1 = "127.0.0.1/24";
final String subnet2 = "10.0.0.1/24";
parseCommand("--net-restrict", String.join(",", subnet1, subnet2));
verify(mockRunnerBuilder).allowedSubnets(allowedSubnetsArgumentCaptor.capture());
assertThat(allowedSubnetsArgumentCaptor.getValue().size()).isEqualTo(2);
assertThat(allowedSubnetsArgumentCaptor.getValue().get(0).getCidrSignature())
.isEqualTo(subnet1);
assertThat(allowedSubnetsArgumentCaptor.getValue().get(1).getCidrSignature())
.isEqualTo(subnet2);
}
@Test
public void netRestrictInvalidShouldFail() {
final String subnet = "127.0.0.1/abc";
parseCommand("--net-restrict", subnet);
Mockito.verifyNoInteractions(mockRunnerBuilder);
assertThat(commandErrorOutput.toString(UTF_8))
.contains("Invalid value for option '--net-restrict'");
}
@Test
public void ethStatsOptionIsParsedCorrectly() {
final String url = "besu-node:secret@host:443";

View File

@@ -117,6 +117,7 @@ import io.opentelemetry.api.GlobalOpenTelemetry;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.json.JsonObject;
import org.apache.commons.net.util.SubnetUtils.SubnetInfo;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
@@ -261,6 +262,7 @@ public abstract class CommandTestAbstract {
@Captor protected ArgumentCaptor<ApiConfiguration> apiConfigurationCaptor;
@Captor protected ArgumentCaptor<EthstatsOptions> ethstatsOptionsArgumentCaptor;
@Captor protected ArgumentCaptor<List<SubnetInfo>> allowedSubnetsArgumentCaptor;
@BeforeEach
public void initMocks() throws Exception {
@@ -354,6 +356,7 @@ public abstract class CommandTestAbstract {
when(mockRunnerBuilder.legacyForkId(anyBoolean())).thenReturn(mockRunnerBuilder);
when(mockRunnerBuilder.apiConfiguration(any())).thenReturn(mockRunnerBuilder);
when(mockRunnerBuilder.enodeDnsConfiguration(any())).thenReturn(mockRunnerBuilder);
when(mockRunnerBuilder.allowedSubnets(any())).thenReturn(mockRunnerBuilder);
when(mockRunnerBuilder.build()).thenReturn(mockRunner);
final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithmFactory.getInstance();

View File

@@ -0,0 +1,59 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.cli.converter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.apache.commons.net.util.SubnetUtils.SubnetInfo;
import org.junit.jupiter.api.Test;
public class SubnetInfoConverterTest {
@Test
void testCreateIpRestrictionHandlerWithValidSubnets() {
String subnet = "192.168.1.0/24";
assertThat(parseSubnetRules(subnet).getCidrSignature()).isEqualTo(subnet);
}
@Test
void testCreateIpRestrictionHandlerWithInvalidSubnet() {
assertThrows(IllegalArgumentException.class, () -> parseSubnetRules("abc"));
}
@Test
void testCreateIpRestrictionHandlerMissingCIDR() {
assertThrows(IllegalArgumentException.class, () -> parseSubnetRules("192.168.1.0"));
}
@Test
void testCreateIpRestrictionHandlerBigCIDR() {
assertThrows(IllegalArgumentException.class, () -> parseSubnetRules("192.168.1.0:25"));
}
@Test
void testCreateIpRestrictionHandlerWithInvalidCIDR() {
assertThrows(IllegalArgumentException.class, () -> parseSubnetRules("192.168.1.0/abc"));
}
@Test
void testCreateIpRestrictionHandlerWithEmptyString() {
assertThrows(IllegalArgumentException.class, () -> parseSubnetRules(""));
}
private SubnetInfo parseSubnetRules(final String subnet) {
return new SubnetInfoConverter().convert(subnet);
}
}

View File

@@ -51,6 +51,7 @@ engine-jwt-disabled=true
engine-jwt-secret="/tmp/jwt.hex"
required-blocks=["8675309=123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"]
discovery-dns-url="enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@nodes.example.org"
net-restrict=["none"]
# chain
network="MAINNET"

View File

@@ -57,6 +57,7 @@ dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib'
implementation 'org.owasp.encoder:encoder'
implementation 'org.xerial.snappy:snappy-java'
implementation 'commons-net:commons-net'
annotationProcessor "org.immutables:value"
implementation "org.immutables:value-annotations"

View File

@@ -0,0 +1,78 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.p2p.permissions;
import org.hyperledger.besu.ethereum.p2p.peers.Peer;
import java.util.List;
import org.apache.commons.net.util.SubnetUtils.SubnetInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages peer permissions based on IP subnet restrictions.
*
* <p>This class extends {@link PeerPermissions} to implement access control based on IP subnets. It
* allows for the configuration of permitted subnets and uses these configurations to determine
* whether a peer should be allowed or denied access based on its IP address.
*
* <p>Note: If no subnets are specified, all peers are considered permitted by default.
*
* @see PeerPermissions
*/
public class PeerPermissionSubnet extends PeerPermissions {
private static final Logger LOG = LoggerFactory.getLogger(PeerPermissionSubnet.class);
private final List<SubnetInfo> allowedSubnets;
/**
* Constructs a new {@code PeerPermissionSubnet} instance with specified allowed subnets.
*
* @param allowedSubnets A list of {@link SubnetInfo} objects representing the subnets that are
* allowed to interact with the local node. Cannot be {@code null}.
*/
public PeerPermissionSubnet(final List<SubnetInfo> allowedSubnets) {
this.allowedSubnets = allowedSubnets;
}
/**
* Determines if a peer is permitted based on the configured subnets.
*
* <p>This method checks if the remote peer's IP address falls within any of the configured
* allowed subnets. If the peer's IP is within any of the allowed subnets, it is permitted.
* Otherwise, it is denied.
*
* @param localNode This parameter is not used in the current implementation.
* @param remotePeer The remote peer to check. Its IP address is used to determine permission.
* @param action Ignored. If the peer is not allowed in the subnet, all actions are now allowed.
* @return {@code true} if the peer is permitted based on its IP address; {@code false} otherwise.
*/
@Override
public boolean isPermitted(final Peer localNode, final Peer remotePeer, final Action action) {
// If no subnets are specified, all peers are permitted
if (allowedSubnets == null || allowedSubnets.isEmpty()) {
return true;
}
String remotePeerHostAddress = remotePeer.getEnodeURL().getIpAsString();
for (SubnetInfo subnet : allowedSubnets) {
if (subnet.isInRange(remotePeerHostAddress)) {
return true;
}
}
LOG.trace("Peer {} is not allowed in any of the configured subnets.", remotePeerHostAddress);
return false;
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.p2p.permissions;
import static org.assertj.core.api.Assertions.assertThat;
import org.hyperledger.besu.ethereum.p2p.peers.DefaultPeer;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURLImpl;
import org.hyperledger.besu.ethereum.p2p.peers.Peer;
import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissions.Action;
import java.util.List;
import org.apache.commons.net.util.SubnetUtils;
import org.apache.commons.net.util.SubnetUtils.SubnetInfo;
import org.junit.jupiter.api.Test;
public class PeerPermissionsSubnetTest {
private final Peer remoteNode = createPeer();
@Test
public void peerInSubnetRangeShouldBePermitted() {
List<SubnetInfo> allowedSubnets = List.of(subnet("127.0.0.0/24"));
PeerPermissionSubnet peerPermissionSubnet = new PeerPermissionSubnet(allowedSubnets);
checkPermissions(peerPermissionSubnet, remoteNode, true);
}
@Test
public void peerInAtLeastOneSubnetRangeShouldBePermitted() {
List<SubnetInfo> allowedSubnets = List.of(subnet("127.0.0.0/24"), subnet("10.0.0.1/24"));
PeerPermissionSubnet peerPermissionSubnet = new PeerPermissionSubnet(allowedSubnets);
checkPermissions(peerPermissionSubnet, remoteNode, true);
}
@Test
public void peerOutSubnetRangeShouldNotBePermitted() {
List<SubnetInfo> allowedSubnets = List.of(subnet("10.0.0.0/24"));
PeerPermissionSubnet peerPermissionSubnet = new PeerPermissionSubnet(allowedSubnets);
checkPermissions(peerPermissionSubnet, remoteNode, false);
}
@Test
public void peerShouldBePermittedIfNoSubnets() {
PeerPermissionSubnet peerPermissionSubnet = new PeerPermissionSubnet(List.of());
checkPermissions(peerPermissionSubnet, remoteNode, true);
}
private void checkPermissions(
final PeerPermissions peerPermissions, final Peer remotePeer, final boolean expectedResult) {
for (Action action : Action.values()) {
assertThat(peerPermissions.isPermitted(createPeer(), remotePeer, action))
.isEqualTo(expectedResult);
}
}
private SubnetInfo subnet(final String subnet) {
return new SubnetUtils(subnet).getInfo();
}
private Peer createPeer() {
return DefaultPeer.fromEnodeURL(
EnodeURLImpl.builder()
.nodeId(Peer.randomId())
.ipAddress("127.0.0.1")
.discoveryAndListeningPorts(EnodeURLImpl.DEFAULT_LISTENING_PORT)
.build());
}
}

View File

@@ -1353,6 +1353,14 @@
<sha256 value="8defda7ceb4888ffe4d4e63278956a1a3c514b75b8363d6716a1bd66fe962c7f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="commons-net" name="commons-net" version="3.11.0">
<artifact name="commons-net-3.11.0.jar">
<sha256 value="e78f28726a8565187cd381ed228e917ef39367de2280a4a7e19e85b06fe0c087" origin="Generated by Gradle"/>
</artifact>
<artifact name="commons-net-3.11.0.pom">
<sha256 value="fc80df2d6dd214df1769c92802f4e051e60410e22a620b1526dc75076a659ad6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="commons-net" name="commons-net" version="3.9.0">
<artifact name="commons-net-3.9.0.jar">
<sha256 value="e3c1566f821b84489308cd933f57e8c00dd8714dc96b898bef844386510d3461" origin="Generated by Gradle"/>
@@ -3007,6 +3015,11 @@
<sha256 value="555d0c9eaa69c042aff924927b9381e8f8174136d355eead445224452e6291cc" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.apache" name="apache" version="32">
<artifact name="apache-32.pom">
<sha256 value="cfd872c0ec27f53ae68f43dbc0fecded8add773079a53afbd390e407b42ce72f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.apache" name="apache" version="4">
<artifact name="apache-4.pom">
<sha256 value="9e9323a26ba8eb2394efef0c96d31b70df570808630dc147cab1e73541cc5194" origin="Generated by Gradle"/>
@@ -3161,6 +3174,11 @@
<sha256 value="48fd6dc846e56b1f408660d163e75300f9e384bb63be482a8082a21d72a8db9c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.apache.commons" name="commons-parent" version="70">
<artifact name="commons-parent-70.pom">
<sha256 value="60335a34e91f49ce50723b007d7c12514fd5766da17dfffb6732e1131e58cd1b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.apache.commons" name="commons-text" version="1.11.0">
<artifact name="commons-text-1.11.0.jar">
<sha256 value="2acf30a070b19163d5a480eae411a281341e870020e3534c6d5d4c8472739e30" origin="Generated by Gradle"/>
@@ -5135,6 +5153,14 @@
<sha256 value="21c4b0286f4b20069577ff4b20978a85c100ac8a46b6f1c8672fbaab337bc3f2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.junit" name="junit-bom" version="5.11.0-M1">
<artifact name="junit-bom-5.11.0-M1.module">
<sha256 value="8f632f8965e9b6f4069e3d58b9ea26b9a5bc76a98e89b43233777ace6dadb237" origin="Generated by Gradle"/>
</artifact>
<artifact name="junit-bom-5.11.0-M1.pom">
<sha256 value="b50975f5cba86204abda3dcd6f097af919fe22e21ef69478dd6b919f6c740d85" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.junit" name="junit-bom" version="5.7.1">
<artifact name="junit-bom-5.7.1.module">
<sha256 value="9854e3894d64b2485207e0046bca07b3d42d169e782f4fa8c9ce229a78faee04" origin="Generated by Gradle"/>

View File

@@ -134,6 +134,7 @@ dependencyManagement {
dependency 'org.apache.commons:commons-lang3:3.14.0'
dependency 'org.apache.commons:commons-text:1.11.0'
dependency 'org.apache.commons:commons-collections4:4.4'
dependency 'commons-net:commons-net:3.11.0'
dependencySet(group: 'org.apache.logging.log4j', version: '2.22.1') {
entry 'log4j-api'