Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subnet-Based Peer Permissions #7168

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1e2cb65
Initial implementation of net restrict
Gabriel-Trintinalia Apr 19, 2024
991fc41
Refactoring and tests
Gabriel-Trintinalia Apr 22, 2024
bce4085
Simplify code
Gabriel-Trintinalia Apr 22, 2024
5a180dc
Merge branch 'hyperledger:main' into 6620-enable-IP-filtering
Gabriel-Trintinalia May 28, 2024
eb18207
Merge branch 'main' into 6620-enable-IP-filtering
Gabriel-Trintinalia May 31, 2024
d9b6073
Remove manual parsing of rule filter
Gabriel-Trintinalia May 31, 2024
6971290
Refactor to fail at startup instead of runtime
Gabriel-Trintinalia Jun 3, 2024
3462116
Remove redundant utility class
Gabriel-Trintinalia Jun 4, 2024
0063584
Merge branch 'main' into 6620-enable-IP-filtering
Gabriel-Trintinalia Jun 4, 2024
2adb525
Fix javadoc
Gabriel-Trintinalia Jun 5, 2024
cabf377
Refactor to control the peer connection in the permission level
Gabriel-Trintinalia Jun 5, 2024
9bcfdfd
Change javadoc
Gabriel-Trintinalia Jun 5, 2024
d849575
Merge branch 'main' into 6620-enable-IP-filtering
Gabriel-Trintinalia Jun 5, 2024
6fb5be0
Change javadoc
Gabriel-Trintinalia Jun 5, 2024
6762e3c
Merge branch 'main' into 6620-enable-IP-filtering
Gabriel-Trintinalia Jun 5, 2024
9c7de7d
Merge branch 'main' into 6620-enable-IP-filtering
Gabriel-Trintinalia Jun 5, 2024
7a7bc30
Change version to versions gradle
Gabriel-Trintinalia Jun 5, 2024
62c690e
Change CHANGELOG.md
Gabriel-Trintinalia Jun 5, 2024
2ca8d51
Add tests
Gabriel-Trintinalia Jun 6, 2024
c575047
Change log level
Gabriel-Trintinalia Jun 6, 2024
650537d
Simplify code
Gabriel-Trintinalia Jun 6, 2024
e7314e6
Simplify code
Gabriel-Trintinalia Jun 6, 2024
d4119a0
PR suggestions
Gabriel-Trintinalia Jun 12, 2024
fe82274
Merge main into branch
Gabriel-Trintinalia Jun 12, 2024
f2c01c7
Add dependency
Gabriel-Trintinalia Jun 12, 2024
30d716d
Merge branch 'main' into 6620-enable-IP-filtering
Gabriel-Trintinalia Jun 12, 2024
10c928d
Fix sha256
Gabriel-Trintinalia Jun 12, 2024
d994756
Merge branch '6620-enable-IP-filtering' of https://github.com/Gabriel…
Gabriel-Trintinalia Jun 12, 2024
13edd8d
add extra null check
Gabriel-Trintinalia Jun 12, 2024
57f4513
Merge main into branch
Gabriel-Trintinalia Jun 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading