Skip to content

Commit

Permalink
Subnet-Based Peer Permissions (#7168)
Browse files Browse the repository at this point in the history
Signed-off-by: Gabriel-Trintinalia <[email protected]>
  • Loading branch information
Gabriel-Trintinalia authored Jun 13, 2024
1 parent 90d2db9 commit e3e86c7
Show file tree
Hide file tree
Showing 14 changed files with 342 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions besu/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
22 changes: 20 additions & 2 deletions besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
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;
Expand Down Expand Up @@ -146,6 +147,7 @@
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;
Expand Down Expand Up @@ -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() {}
Expand Down Expand Up @@ -589,6 +592,17 @@ public RunnerBuilder enodeDnsConfiguration(final EnodeDnsConfiguration enodeDnsC
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.
*
Expand Down Expand Up @@ -648,6 +662,10 @@ public Runner build() {
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();
Expand All @@ -667,8 +685,8 @@ public Runner build() {
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;
Expand Down
12 changes: 12 additions & 0 deletions besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
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;
Expand Down Expand Up @@ -243,6 +244,7 @@
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;
Expand Down Expand Up @@ -527,6 +529,15 @@ private InetAddress autoDiscoverDefaultIP() {

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(
Expand Down Expand Up @@ -2320,6 +2331,7 @@ private Runner synchronize(
.storageProvider(keyValueStorageProvider(keyValueStorageName))
.rpcEndpointService(rpcEndpointServiceImpl)
.enodeDnsConfiguration(getEnodeDnsConfiguration())
.allowedSubnets(p2PDiscoveryOptionGroup.allowedSubnets)
.build();

addShutdownHook(runner);
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
22 changes: 22 additions & 0 deletions besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,28 @@ public void parsesInvalidFastSyncMinPeersOptionWrongFormatShouldFail() {
.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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -354,6 +356,7 @@ public void initMocks() throws Exception {
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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions besu/src/test/resources/everything_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions ethereum/p2p/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit e3e86c7

Please sign in to comment.