[PAN-2614] Expand permissioning interface (#1471)

Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
mbaxter 6 years ago committed by GitHub
parent c23482c60b
commit 22ec27796d
  1. 26
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/PeerDiscoveryAgent.java
  2. 5
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/VertxPeerDiscoveryAgent.java
  3. 119
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryController.java
  4. 54
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryPermissions.java
  5. 13
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/RecursivePeerRefreshState.java
  6. 151
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/network/DefaultP2PNetwork.java
  7. 130
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/network/NodePermissioningAdapter.java
  8. 44
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/permissions/PeerPermissions.java
  9. 9
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/permissions/PeerPermissionsBlacklist.java
  10. 44
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/rlpx/PeerRlpxPermissions.java
  11. 105
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/NodePermissioningControllerTestHelper.java
  12. 302
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/discovery/PeerDiscoveryAgentTest.java
  13. 14
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/discovery/PeerDiscoveryTestHelper.java
  14. 5
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/MockPeerDiscoveryAgent.java
  15. 241
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryControllerTest.java
  16. 39
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/RecursivePeerRefreshStateTest.java
  17. 223
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/network/DefaultP2PNetworkTest.java
  18. 370
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/network/NodePermissioningAdapterTest.java
  19. 48
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/network/P2PNetworkTest.java
  20. 50
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/network/PeerReputationManagerTest.java
  21. 50
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/permissions/PeerPermissionsBlacklistTest.java
  22. 121
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/permissions/PeerPermissionsTest.java
  23. 8
      ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/node/NodePermissioningController.java
  24. 16
      ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/node/provider/SyncStatusNodePermissioningProvider.java
  25. 18
      ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/node/NodePermissioningControllerTest.java
  26. 25
      ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/node/provider/SyncStatusNodePermissioningProviderTest.java

@ -27,7 +27,6 @@ import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PingPacketData;
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.TimerUtil;
import tech.pegasys.pantheon.ethereum.p2p.peers.PeerId;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions;
import tech.pegasys.pantheon.ethereum.permissioning.node.NodePermissioningController;
import tech.pegasys.pantheon.metrics.MetricsSystem;
import tech.pegasys.pantheon.util.NetworkUtility;
import tech.pegasys.pantheon.util.Subscribers;
@ -64,7 +63,6 @@ public abstract class PeerDiscoveryAgent {
protected final List<DiscoveryPeer> bootstrapPeers;
private final List<PeerRequirement> peerRequirements = new CopyOnWriteArrayList<>();
private final PeerPermissions peerPermissions;
private final Optional<NodePermissioningController> nodePermissioningController;
private final MetricsSystem metricsSystem;
/* The peer controller, which takes care of the state machine of peers. */
protected Optional<PeerDiscoveryController> controller = Optional.empty();
@ -74,8 +72,10 @@ public abstract class PeerDiscoveryAgent {
private final BytesValue id;
protected final DiscoveryConfiguration config;
/* This is the {@link tech.pegasys.pantheon.ethereum.p2p.Peer} object holding who we are. */
private DiscoveryPeer advertisedPeer;
/* This is the {@link tech.pegasys.pantheon.ethereum.p2p.Peer} object representing our local node.
* This value is empty on construction, and is set after the discovery agent is started.
*/
private Optional<DiscoveryPeer> localNode = Optional.empty();
/* Is discovery enabled? */
private boolean isActive = false;
@ -85,7 +85,6 @@ public abstract class PeerDiscoveryAgent {
final SECP256K1.KeyPair keyPair,
final DiscoveryConfiguration config,
final PeerPermissions peerPermissions,
final Optional<NodePermissioningController> nodePermissioningController,
final MetricsSystem metricsSystem) {
this.metricsSystem = metricsSystem;
checkArgument(keyPair != null, "keypair cannot be null");
@ -94,7 +93,6 @@ public abstract class PeerDiscoveryAgent {
validateConfiguration(config);
this.peerPermissions = peerPermissions;
this.nodePermissioningController = nodePermissioningController;
this.bootstrapPeers =
config.getBootnodes().stream().map(DiscoveryPeer::fromEnode).collect(Collectors.toList());
@ -125,7 +123,7 @@ public abstract class PeerDiscoveryAgent {
.thenAccept(
(InetSocketAddress localAddress) -> {
// Once listener is set up, finish initializing
advertisedPeer =
final DiscoveryPeer ourNode =
DiscoveryPeer.fromEnode(
EnodeURL.builder()
.nodeId(id)
@ -133,8 +131,9 @@ public abstract class PeerDiscoveryAgent {
.listeningPort(tcpPort)
.discoveryPort(localAddress.getPort())
.build());
this.localNode = Optional.of(ourNode);
isActive = true;
startController();
startController(ourNode);
});
} else {
this.isActive = false;
@ -146,23 +145,22 @@ public abstract class PeerDiscoveryAgent {
this.peerRequirements.add(peerRequirement);
}
private void startController() {
final PeerDiscoveryController controller = createController();
private void startController(final DiscoveryPeer localNode) {
final PeerDiscoveryController controller = createController(localNode);
this.controller = Optional.of(controller);
controller.start();
}
private PeerDiscoveryController createController() {
private PeerDiscoveryController createController(final DiscoveryPeer localNode) {
return PeerDiscoveryController.builder()
.keypair(keyPair)
.localPeer(advertisedPeer)
.localPeer(localNode)
.bootstrapNodes(bootstrapPeers)
.outboundMessageHandler(this::handleOutgoingPacket)
.timerUtil(createTimer())
.workerExecutor(createWorkerExecutor())
.peerRequirement(PeerRequirement.combine(peerRequirements))
.peerPermissions(peerPermissions)
.nodePermissioningController(nodePermissioningController)
.peerBondedObservers(peerBondedObservers)
.metricsSystem(metricsSystem)
.build();
@ -232,7 +230,7 @@ public abstract class PeerDiscoveryAgent {
}
public Optional<DiscoveryPeer> getAdvertisedPeer() {
return Optional.ofNullable(advertisedPeer);
return localNode;
}
public BytesValue getId() {

@ -22,7 +22,6 @@ import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PeerDiscoveryContro
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.TimerUtil;
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.VertxTimerUtil;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions;
import tech.pegasys.pantheon.ethereum.permissioning.node.NodePermissioningController;
import tech.pegasys.pantheon.metrics.MetricCategory;
import tech.pegasys.pantheon.metrics.MetricsSystem;
import tech.pegasys.pantheon.util.NetworkUtility;
@ -31,7 +30,6 @@ import java.io.IOException;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
@ -59,9 +57,8 @@ public class VertxPeerDiscoveryAgent extends PeerDiscoveryAgent {
final KeyPair keyPair,
final DiscoveryConfiguration config,
final PeerPermissions peerPermissions,
final Optional<NodePermissioningController> nodePermissioningController,
final MetricsSystem metricsSystem) {
super(keyPair, config, peerPermissions, nodePermissioningController, metricsSystem);
super(keyPair, config, peerPermissions, metricsSystem);
checkArgument(vertx != null, "vertx instance cannot be null");
this.vertx = vertx;

@ -28,7 +28,6 @@ import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryStatus;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.peers.PeerId;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions;
import tech.pegasys.pantheon.ethereum.permissioning.node.NodePermissioningController;
import tech.pegasys.pantheon.metrics.Counter;
import tech.pegasys.pantheon.metrics.LabelledMetric;
import tech.pegasys.pantheon.metrics.MetricCategory;
@ -120,8 +119,7 @@ public class PeerDiscoveryController {
// The peer representation of this node
private final DiscoveryPeer localPeer;
private final OutboundMessageHandler outboundMessageHandler;
private final PeerPermissions peerPermissions;
private final Optional<NodePermissioningController> nodePermissioningController;
private final PeerDiscoveryPermissions peerPermissions;
private final DiscoveryProtocolLogger discoveryProtocolLogger;
private final LabelledMetric<Counter> interactionCounter;
private final LabelledMetric<Counter> interactionRetryCounter;
@ -129,13 +127,15 @@ public class PeerDiscoveryController {
private RetryDelayFunction retryDelayFunction = RetryDelayFunction.linear(1.5, 2000, 60000);
private final AsyncExecutor workerExecutor;
private final long tableRefreshIntervalMs;
private final PeerRequirement peerRequirement;
private final long tableRefreshIntervalMs;
private OptionalLong tableRefreshTimerId = OptionalLong.empty();
private long lastRefreshTime = -1;
private OptionalLong tableRefreshTimerId = OptionalLong.empty();
private final long cleanPeerTableIntervalMs;
private final AtomicBoolean peerTableIsDirty = new AtomicBoolean(false);
private OptionalLong cleanTableTimerId = OptionalLong.empty();
// Observers for "peer bonded" discovery events.
private final Subscribers<Consumer<PeerBondedEvent>> peerBondedObservers;
@ -151,9 +151,9 @@ public class PeerDiscoveryController {
final TimerUtil timerUtil,
final AsyncExecutor workerExecutor,
final long tableRefreshIntervalMs,
final long cleanPeerTableIntervalMs,
final PeerRequirement peerRequirement,
final PeerPermissions peerPermissions,
final Optional<NodePermissioningController> nodePermissioningController,
final Subscribers<Consumer<PeerBondedEvent>> peerBondedObservers,
final MetricsSystem metricsSystem) {
this.timerUtil = timerUtil;
@ -163,13 +163,14 @@ public class PeerDiscoveryController {
this.peerTable = peerTable;
this.workerExecutor = workerExecutor;
this.tableRefreshIntervalMs = tableRefreshIntervalMs;
this.cleanPeerTableIntervalMs = cleanPeerTableIntervalMs;
this.peerRequirement = peerRequirement;
this.peerPermissions = peerPermissions;
this.nodePermissioningController = nodePermissioningController;
this.outboundMessageHandler = outboundMessageHandler;
this.peerBondedObservers = peerBondedObservers;
this.discoveryProtocolLogger = new DiscoveryProtocolLogger(metricsSystem);
this.peerPermissions = new PeerDiscoveryPermissions(localPeer, peerPermissions);
metricsSystem.createIntegerGauge(
MetricCategory.NETWORK,
"discovery_inflight_interactions_current",
@ -202,7 +203,7 @@ public class PeerDiscoveryController {
final List<DiscoveryPeer> initialDiscoveryPeers =
bootstrapNodes.stream()
.filter(this::isPeerPermittedToReceiveMessages)
.filter(peerPermissions::isAllowedInPeerTable)
.collect(Collectors.toList());
initialDiscoveryPeers.stream().forEach(peerTable::tryAdd);
@ -213,35 +214,23 @@ public class PeerDiscoveryController {
timerUtil,
localPeer,
peerTable,
this::isPeerPermittedToReceiveMessages,
peerPermissions,
PEER_REFRESH_ROUND_TIMEOUT_IN_SECONDS,
100);
peerPermissions.subscribeUpdate(this::handlePermissionsUpdate);
if (nodePermissioningController.isPresent()) {
// if smart contract permissioning is enabled, bond with bootnodes
if (nodePermissioningController.get().getSyncStatusNodePermissioningProvider().isPresent()) {
for (final DiscoveryPeer p : initialDiscoveryPeers) {
bond(p);
}
}
nodePermissioningController
.get()
.startPeerDiscoveryCallback(
() -> recursivePeerRefreshState.start(initialDiscoveryPeers, localPeer.getId()));
recursivePeerRefreshState.start(initialDiscoveryPeers, localPeer.getId());
} else {
recursivePeerRefreshState.start(initialDiscoveryPeers, localPeer.getId());
}
final long timerId =
final long refreshTimerId =
timerUtil.setPeriodic(
Math.min(REFRESH_CHECK_INTERVAL_MILLIS, tableRefreshIntervalMs),
this::refreshTableIfRequired);
tableRefreshTimerId = OptionalLong.of(timerId);
tableRefreshTimerId = OptionalLong.of(refreshTimerId);
cleanTableTimerId =
OptionalLong.of(
timerUtil.setPeriodic(cleanPeerTableIntervalMs, this::cleanPeerTableIfRequired));
}
public CompletableFuture<?> stop() {
@ -251,25 +240,13 @@ public class PeerDiscoveryController {
tableRefreshTimerId.ifPresent(timerUtil::cancelTimer);
tableRefreshTimerId = OptionalLong.empty();
cleanTableTimerId.ifPresent(timerUtil::cancelTimer);
cleanTableTimerId = OptionalLong.empty();
inflightInteractions.values().forEach(PeerInteractionState::cancelTimers);
inflightInteractions.clear();
return CompletableFuture.completedFuture(null);
}
private boolean isPeerPermittedToReceiveMessages(final Peer remotePeer) {
return peerPermissions.isPermitted(remotePeer)
&& nodePermissioningController
.map(c -> c.isPermitted(localPeer.getEnodeURL(), remotePeer.getEnodeURL()))
.orElse(true);
}
private boolean isPeerPermittedToSendMessages(final Peer remotePeer) {
return peerPermissions.isPermitted(remotePeer)
&& nodePermissioningController
.map(c -> c.isPermitted(remotePeer.getEnodeURL(), localPeer.getEnodeURL()))
.orElse(true);
}
private void handlePermissionsUpdate(
final boolean addRestrictions, final Optional<List<Peer>> affectedPeers) {
if (!addRestrictions) {
@ -278,7 +255,19 @@ public class PeerDiscoveryController {
}
// If we have an explicit list of peers, drop each peer from our discovery table
affectedPeers.ifPresent(peers -> peers.forEach(this::dropPeer));
if (affectedPeers.isPresent()) {
affectedPeers.get().forEach(this::dropPeerIfDisallowed);
return;
}
// Otherwise, signal that we need to clean up the peer table
peerTableIsDirty.set(true);
}
private void dropPeerIfDisallowed(final Peer peer) {
if (!peerPermissions.isAllowedInPeerTable(peer)) {
dropPeer(peer);
}
}
public void dropPeer(final PeerId peer) {
@ -304,11 +293,6 @@ public class PeerDiscoveryController {
return;
}
if (!isPeerPermittedToSendMessages(sender)) {
LOG.trace("Dropping packet from disallowed peer ({})", sender.getEnodeURLString());
return;
}
// Load the peer from the table, or use the instance that comes in.
final Optional<DiscoveryPeer> maybeKnownPeer = peerTable.get(sender);
final DiscoveryPeer peer = maybeKnownPeer.orElse(sender);
@ -316,7 +300,8 @@ public class PeerDiscoveryController {
switch (packet.getType()) {
case PING:
if (addToPeerTable(peer)) {
if (peerPermissions.allowInboundBonding(peer)) {
addToPeerTable(peer);
final PingPacketData ping = packet.getPacketData(PingPacketData.class).get();
respondToPing(ping, packet.getHash(), peer);
}
@ -337,9 +322,10 @@ public class PeerDiscoveryController {
peer, getPeersFromNeighborsPacket(packet)));
break;
case FIND_NEIGHBORS:
if (!peerKnown) {
if (!peerKnown || !peerPermissions.allowInboundNeighborsRequest(peer)) {
break;
}
final FindNeighborsPacketData fn =
packet.getPacketData(FindNeighborsPacketData.class).get();
respondToFindNeighbors(fn, peer);
@ -361,6 +347,10 @@ public class PeerDiscoveryController {
}
private boolean addToPeerTable(final DiscoveryPeer peer) {
if (!peerPermissions.isAllowedInPeerTable(peer)) {
return false;
}
final PeerTable.AddResult result = peerTable.tryAdd(peer);
if (result.getOutcome() == AddOutcome.SELF) {
return false;
@ -416,6 +406,12 @@ public class PeerDiscoveryController {
}
}
private void cleanPeerTableIfRequired() {
if (peerTableIsDirty.compareAndSet(true, false)) {
peerTable.streamAllPeers().forEach(this::dropPeerIfDisallowed);
}
}
@VisibleForTesting
RecursivePeerRefreshState getRecursivePeerRefreshState() {
return recursivePeerRefreshState;
@ -562,7 +558,7 @@ public class PeerDiscoveryController {
* @return List of peers.
*/
public Stream<DiscoveryPeer> streamDiscoveredPeers() {
return peerTable.streamAllPeers();
return peerTable.streamAllPeers().filter(peerPermissions::isAllowedInPeerTable);
}
public void setRetryDelayFunction(final RetryDelayFunction retryDelayFunction) {
@ -655,8 +651,8 @@ public class PeerDiscoveryController {
private PeerRequirement peerRequirement = PeerRequirement.NOOP;
private PeerPermissions peerPermissions = PeerPermissions.noop();
private long tableRefreshIntervalMs = MILLISECONDS.convert(30, TimeUnit.MINUTES);
private long cleanPeerTableIntervalMs = MILLISECONDS.convert(1, TimeUnit.MINUTES);
private List<DiscoveryPeer> bootstrapNodes = new ArrayList<>();
private Optional<NodePermissioningController> nodePermissioningController = Optional.empty();
private PeerTable peerTable;
private Subscribers<Consumer<PeerBondedEvent>> peerBondedObservers = new Subscribers<>();
@ -685,9 +681,9 @@ public class PeerDiscoveryController {
timerUtil,
workerExecutor,
tableRefreshIntervalMs,
cleanPeerTableIntervalMs,
peerRequirement,
peerPermissions,
nodePermissioningController,
peerBondedObservers,
metricsSystem);
}
@ -752,6 +748,12 @@ public class PeerDiscoveryController {
return this;
}
public Builder cleanPeerTableIntervalMs(final long cleanPeerTableIntervalMs) {
checkArgument(cleanPeerTableIntervalMs >= 0);
this.cleanPeerTableIntervalMs = cleanPeerTableIntervalMs;
return this;
}
public Builder peerRequirement(final PeerRequirement peerRequirement) {
checkNotNull(peerRequirement);
this.peerRequirement = peerRequirement;
@ -764,13 +766,6 @@ public class PeerDiscoveryController {
return this;
}
public Builder nodePermissioningController(
final Optional<NodePermissioningController> nodePermissioningController) {
checkNotNull(nodePermissioningController);
this.nodePermissioningController = nodePermissioningController;
return this;
}
public Builder peerBondedObservers(
final Subscribers<Consumer<PeerBondedEvent>> peerBondedObservers) {
checkNotNull(peerBondedObservers);

@ -0,0 +1,54 @@
/*
* Copyright 2019 ConsenSys AG.
*
* 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.
*/
package tech.pegasys.pantheon.ethereum.p2p.discovery.internal;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions.Action;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PermissionsUpdateCallback;
class PeerDiscoveryPermissions {
private final Peer localNode;
private final PeerPermissions peerPermissions;
PeerDiscoveryPermissions(final Peer localNode, final PeerPermissions peerPermissions) {
this.localNode = localNode;
this.peerPermissions = peerPermissions;
}
public void subscribeUpdate(final PermissionsUpdateCallback callback) {
peerPermissions.subscribeUpdate(callback);
}
public boolean isAllowedInPeerTable(final Peer peer) {
return peerPermissions.isPermitted(localNode, peer, Action.DISCOVERY_ALLOW_IN_PEER_TABLE);
}
public boolean allowOutboundBonding(final Peer peer) {
return peerPermissions.isPermitted(localNode, peer, Action.DISCOVERY_ALLOW_OUTBOUND_BONDING);
}
public boolean allowInboundBonding(final Peer peer) {
return peerPermissions.isPermitted(localNode, peer, Action.DISCOVERY_ACCEPT_INBOUND_BONDING);
}
public boolean allowOutboundNeighborsRequest(final Peer peer) {
return peerPermissions.isPermitted(
localNode, peer, Action.DISCOVERY_ALLOW_OUTBOUND_NEIGHBORS_REQUEST);
}
public boolean allowInboundNeighborsRequest(final Peer peer) {
return peerPermissions.isPermitted(
localNode, peer, Action.DISCOVERY_SERVE_INBOUND_NEIGHBORS_REQUEST);
}
}

@ -16,7 +16,6 @@ import static tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PeerDistance
import tech.pegasys.pantheon.ethereum.p2p.discovery.DiscoveryPeer;
import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryStatus;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.util.bytes.BytesValue;
import java.util.List;
@ -37,7 +36,7 @@ public class RecursivePeerRefreshState {
private static final Logger LOG = LogManager.getLogger();
private static final int MAX_CONCURRENT_REQUESTS = 3;
private BytesValue target;
private final OutboundDiscoveryMessagingPermissions peerPermissions;
private final PeerDiscoveryPermissions peerPermissions;
private final PeerTable peerTable;
private final DiscoveryPeer localPeer;
@ -61,7 +60,7 @@ public class RecursivePeerRefreshState {
final TimerUtil timerUtil,
final DiscoveryPeer localPeer,
final PeerTable peerTable,
final OutboundDiscoveryMessagingPermissions peerPermissions,
final PeerDiscoveryPermissions peerPermissions,
final int timeoutPeriodInSeconds,
final int maxRounds) {
this.bondingAgent = bondingAgent;
@ -182,7 +181,6 @@ public class RecursivePeerRefreshState {
private boolean satisfiesMapAdditionCriteria(final DiscoveryPeer discoPeer) {
return !oneTrueMap.containsKey(discoPeer.getId())
&& peerPermissions.isPermitted(discoPeer)
&& (initialPeers.contains(discoPeer) || !peerTable.get(discoPeer).isPresent())
&& !discoPeer.getId().equals(localPeer.getId());
}
@ -247,12 +245,14 @@ public class RecursivePeerRefreshState {
private List<MetadataPeer> bondingRoundCandidates() {
return oneTrueMap.values().stream()
.filter(MetadataPeer::isBondingCandidate)
.filter(p -> peerPermissions.allowOutboundBonding(p.getPeer()))
.collect(Collectors.toList());
}
private List<MetadataPeer> neighboursRoundCandidates() {
return oneTrueMap.values().stream()
.filter(MetadataPeer::isNeighboursRoundCandidate)
.filter(p -> peerPermissions.allowOutboundNeighborsRequest(p.getPeer()))
.limit(MAX_CONCURRENT_REQUESTS)
.collect(Collectors.toList());
}
@ -379,9 +379,4 @@ public class RecursivePeerRefreshState {
timeoutCancelled.set(true);
}
}
@FunctionalInterface
public interface OutboundDiscoveryMessagingPermissions {
boolean isPermitted(Peer remotePeer);
}
}

@ -34,9 +34,11 @@ import tech.pegasys.pantheon.ethereum.p2p.network.netty.HandshakeHandlerInbound;
import tech.pegasys.pantheon.ethereum.p2p.network.netty.HandshakeHandlerOutbound;
import tech.pegasys.pantheon.ethereum.p2p.network.netty.PeerConnectionRegistry;
import tech.pegasys.pantheon.ethereum.p2p.network.netty.TimeoutHandler;
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissionsBlacklist;
import tech.pegasys.pantheon.ethereum.p2p.rlpx.PeerRlpxPermissions;
import tech.pegasys.pantheon.ethereum.p2p.wire.Capability;
import tech.pegasys.pantheon.ethereum.p2p.wire.PeerInfo;
import tech.pegasys.pantheon.ethereum.p2p.wire.SubProtocol;
@ -150,12 +152,11 @@ public class DefaultP2PNetwork implements P2PNetwork {
private final SECP256K1.KeyPair keyPair;
private final BytesValue nodeId;
private volatile OptionalInt listeningPort = OptionalInt.empty();
private volatile Optional<EnodeURL> localEnode = Optional.empty();
private volatile Optional<Peer> localNode = Optional.empty();
private volatile Optional<PeerInfo> ourPeerInfo = Optional.empty();
private final PeerPermissions peerPermissions;
private final Optional<NodePermissioningController> nodePermissioningController;
private final Optional<Blockchain> blockchain;
private volatile Optional<PeerRlpxPermissions> rlpxPermissions = Optional.empty();
@VisibleForTesting final Collection<Peer> peerMaintainConnectionList;
@VisibleForTesting final PeerConnectionRegistry connections;
@ -170,7 +171,6 @@ public class DefaultP2PNetwork implements P2PNetwork {
private final Callbacks callbacks = new Callbacks(protocolCallbacks, disconnectCallbacks);
private final LabelledMetric<Counter> outboundMessagesCounter;
private OptionalLong blockAddedObserverId = OptionalLong.empty();
private OptionalLong peerBondedObserverId = OptionalLong.empty();
private final AtomicBoolean started = new AtomicBoolean(false);
@ -189,8 +189,6 @@ public class DefaultP2PNetwork implements P2PNetwork {
* @param supportedCapabilities The wire protocol capabilities to advertise to connected peers.
* @param peerPermissions An object that determines whether peers are allowed to connect
* @param metricsSystem The metrics system to capture metrics with.
* @param nodePermissioningController Controls node permissioning.
* @param blockchain The blockchain to subscribe to BlockAddedEvents.
*/
DefaultP2PNetwork(
final PeerDiscoveryAgent peerDiscoveryAgent,
@ -198,16 +196,12 @@ public class DefaultP2PNetwork implements P2PNetwork {
final NetworkingConfiguration config,
final List<Capability> supportedCapabilities,
final PeerPermissions peerPermissions,
final MetricsSystem metricsSystem,
final Optional<NodePermissioningController> nodePermissioningController,
final Blockchain blockchain) {
final MetricsSystem metricsSystem) {
this.peerDiscoveryAgent = peerDiscoveryAgent;
this.keyPair = keyPair;
this.config = config;
this.supportedCapabilities = supportedCapabilities;
this.nodePermissioningController = nodePermissioningController;
this.blockchain = Optional.ofNullable(blockchain);
this.peerMaintainConnectionList = new HashSet<>();
this.connections = new PeerConnectionRegistry(metricsSystem);
@ -219,10 +213,9 @@ public class DefaultP2PNetwork implements P2PNetwork {
final PeerPermissionsBlacklist misbehavingPeers = PeerPermissionsBlacklist.create(500);
PeerReputationManager reputationManager = new PeerReputationManager(misbehavingPeers);
this.peerPermissions = PeerPermissions.combine(peerPermissions, misbehavingPeers);
this.peerPermissions.subscribeUpdate(this::handlePermissionsUpdate);
peerDiscoveryAgent.addPeerRequirement(() -> connections.size() >= maxPeers);
this.nodePermissioningController.ifPresent(
c -> c.subscribeToUpdates(this::checkCurrentConnections));
subscribeDisconnect(reputationManager);
subscribeDisconnect(connections);
@ -347,9 +340,10 @@ public class DefaultP2PNetwork implements P2PNetwork {
return;
}
if (!isPeerAllowed(connection)) {
final Peer peer = connection.getPeer();
if (!rlpxPermissions.isPresent()
|| !rlpxPermissions.get().allowNewInboundConnectionFrom(peer)) {
connection.disconnect(DisconnectReason.UNKNOWN);
peerDiscoveryAgent.dropPeer(connection.getPeer());
return;
}
@ -368,7 +362,9 @@ public class DefaultP2PNetwork implements P2PNetwork {
peer.getEnodeURL().isListening(),
"Invalid enode url. Enode url must contain a non-zero listening port.");
final boolean added = peerMaintainConnectionList.add(peer);
if (isPeerAllowed(peer) && !isConnectingOrConnected(peer)) {
final boolean allowConnection =
rlpxPermissions.isPresent() && rlpxPermissions.get().allowNewOutboundConnectionTo(peer);
if (allowConnection && !isConnectingOrConnected(peer)) {
// Connect immediately if appropriate
connect(peer);
}
@ -394,9 +390,13 @@ public class DefaultP2PNetwork implements P2PNetwork {
}
void checkMaintainedConnectionPeers() {
if (!rlpxPermissions.isPresent()) {
return;
}
final PeerRlpxPermissions permissions = rlpxPermissions.get();
peerMaintainConnectionList.stream()
.filter(p -> !isConnectingOrConnected(p))
.filter(this::isPeerAllowed)
.filter(permissions::allowNewOutboundConnectionTo)
.forEach(this::connect);
}
@ -406,11 +406,15 @@ public class DefaultP2PNetwork implements P2PNetwork {
if (availablePeerSlots <= 0) {
return;
}
if (!rlpxPermissions.isPresent()) {
return;
}
final PeerRlpxPermissions permissions = rlpxPermissions.get();
final List<DiscoveryPeer> peers =
streamDiscoveredPeers()
.filter(peer -> peer.getStatus() == PeerDiscoveryStatus.BONDED)
.filter(this::isPeerAllowed)
.filter(permissions::allowNewOutboundConnectionTo)
.filter(peer -> !isConnected(peer) && !isConnecting(peer))
.sorted(Comparator.comparing(DiscoveryPeer::getLastAttemptedConnection))
.collect(Collectors.toList());
@ -443,11 +447,17 @@ public class DefaultP2PNetwork implements P2PNetwork {
@Override
public CompletableFuture<PeerConnection> connect(final Peer peer) {
final CompletableFuture<PeerConnection> connectionFuture = new CompletableFuture<>();
if (!localEnode.isPresent()) {
if (!localNode.isPresent() || !rlpxPermissions.isPresent()) {
connectionFuture.completeExceptionally(
new IllegalStateException("Attempt to connect to peer before network is ready"));
return connectionFuture;
}
if (!rlpxPermissions.get().allowNewOutboundConnectionTo(peer)) {
// Peer not allowed
connectionFuture.completeExceptionally(
new IllegalStateException("Unable to connect to disallowed peer: " + peer));
return connectionFuture;
}
LOG.trace("Initiating connection to peer: {}", peer.getId());
final CompletableFuture<PeerConnection> existingPendingConnection =
@ -554,22 +564,8 @@ public class DefaultP2PNetwork implements P2PNetwork {
peerBondedObserverId =
OptionalLong.of(peerDiscoveryAgent.observePeerBondedEvents(handlePeerBondedEvent()));
if (nodePermissioningController.isPresent()) {
if (blockchain.isPresent()) {
synchronized (this) {
if (!blockAddedObserverId.isPresent()) {
blockAddedObserverId =
OptionalLong.of(
blockchain.get().observeBlockAdded((evt, chain) -> checkCurrentConnections()));
}
}
} else {
throw new IllegalStateException(
"Network permissioning needs to listen to BlockAddedEvents. Blockchain can't be null.");
}
}
createLocalEnode();
final Peer ourNode = createLocalNode();
this.rlpxPermissions = Optional.of(new PeerRlpxPermissions(ourNode, peerPermissions));
peerConnectionScheduler.scheduleWithFixedDelay(
this::checkMaintainedConnectionPeers, 2, 60, TimeUnit.SECONDS);
@ -587,43 +583,39 @@ public class DefaultP2PNetwork implements P2PNetwork {
};
}
private synchronized void checkCurrentConnections() {
private void handlePermissionsUpdate(
final boolean permissionsRestricted, final Optional<List<Peer>> peers) {
if (!permissionsRestricted || !rlpxPermissions.isPresent()) {
// Nothing to do
return;
}
final PeerRlpxPermissions permissions = rlpxPermissions.get();
if (peers.isPresent()) {
peers.get().stream()
.filter(p -> !permissions.allowOngoingConnection(p))
.map(Peer::getId)
.map(connections::getConnectionForPeer)
.filter(Optional::isPresent)
.map(Optional::get)
.forEach(conn -> conn.disconnect(DisconnectReason.REQUESTED));
} else {
checkAllConnections(permissions);
}
}
private void checkAllConnections(final PeerRlpxPermissions permissions) {
connections
.getPeerConnections()
.forEach(
peerConnection -> {
if (!isPeerAllowed(peerConnection)) {
final Peer peer = peerConnection.getPeer();
if (!permissions.allowOngoingConnection(peer)) {
peerConnection.disconnect(DisconnectReason.REQUESTED);
peerDiscoveryAgent.dropPeer(peerConnection.getPeer());
}
});
}
private boolean isPeerAllowed(final PeerConnection conn) {
return isPeerAllowed(conn.getPeer());
}
private boolean isPeerAllowed(final Peer peer) {
final Optional<EnodeURL> maybeEnode = getLocalEnode();
if (!maybeEnode.isPresent()) {
// If the network isn't ready yet, deny connections
return false;
}
final EnodeURL localEnode = maybeEnode.get();
if (peer.getId().equals(nodeId)) {
// Peer matches our node id
return false;
}
if (!peerPermissions.isPermitted(peer)) {
return false;
}
return nodePermissioningController
.map(c -> c.isPermitted(localEnode, peer.getEnodeURL()))
.orElse(true);
}
@VisibleForTesting
boolean isConnecting(final Peer peer) {
return pendingConnections.containsKey(peer);
@ -650,9 +642,8 @@ public class DefaultP2PNetwork implements P2PNetwork {
peerDiscoveryAgent.stop().join();
peerBondedObserverId.ifPresent(peerDiscoveryAgent::removePeerBondedObserver);
peerBondedObserverId = OptionalLong.empty();
blockchain.ifPresent(b -> blockAddedObserverId.ifPresent(b::removeObserver));
blockAddedObserverId = OptionalLong.empty();
peerDiscoveryAgent.stop().join();
peerPermissions.close();
workers.shutdownGracefully();
boss.shutdownGracefully();
}
@ -692,12 +683,12 @@ public class DefaultP2PNetwork implements P2PNetwork {
@Override
public Optional<EnodeURL> getLocalEnode() {
return localEnode;
return localNode.map(Peer::getEnodeURL);
}
private void createLocalEnode() {
if (localEnode.isPresent() || !listeningPort.isPresent()) {
return;
private Peer createLocalNode() {
if (localNode.isPresent() || !listeningPort.isPresent()) {
return localNode.orElse(null);
}
final OptionalInt discoveryPort =
@ -716,7 +707,9 @@ public class DefaultP2PNetwork implements P2PNetwork {
.build();
LOG.info("Enode URL {}", localEnode.toString());
this.localEnode = Optional.of(localEnode);
final Peer ourNode = DefaultPeer.fromEnodeURL(localEnode);
this.localNode = Optional.of(ourNode);
return ourNode;
}
private void onConnectionEstablished(final PeerConnection connection) {
@ -742,6 +735,13 @@ public class DefaultP2PNetwork implements P2PNetwork {
}
private P2PNetwork doBuild() {
// Fold NodePermissioningController into peerPermissions
if (nodePermissioningController.isPresent()) {
final List<EnodeURL> bootnodes = config.getDiscovery().getBootnodes();
final PeerPermissions nodePermissions =
new NodePermissioningAdapter(nodePermissioningController.get(), bootnodes, blockchain);
peerPermissions = PeerPermissions.combine(peerPermissions, nodePermissions);
}
peerDiscoveryAgent = peerDiscoveryAgent == null ? createDiscoveryAgent() : peerDiscoveryAgent;
return new DefaultP2PNetwork(
@ -750,9 +750,7 @@ public class DefaultP2PNetwork implements P2PNetwork {
config,
supportedCapabilities,
peerPermissions,
metricsSystem,
nodePermissioningController,
blockchain);
metricsSystem);
}
private void validate() {
@ -771,12 +769,7 @@ public class DefaultP2PNetwork implements P2PNetwork {
private PeerDiscoveryAgent createDiscoveryAgent() {
return new VertxPeerDiscoveryAgent(
vertx,
keyPair,
config.getDiscovery(),
peerPermissions,
nodePermissioningController,
metricsSystem);
vertx, keyPair, config.getDiscovery(), peerPermissions, metricsSystem);
}
public Builder vertx(final Vertx vertx) {

@ -0,0 +1,130 @@
/*
* Copyright 2019 ConsenSys AG.
*
* 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.
*/
package tech.pegasys.pantheon.ethereum.p2p.network;
import static com.google.common.base.Preconditions.checkNotNull;
import tech.pegasys.pantheon.ethereum.chain.Blockchain;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions;
import tech.pegasys.pantheon.ethereum.permissioning.node.NodePermissioningController;
import tech.pegasys.pantheon.ethereum.permissioning.node.provider.SyncStatusNodePermissioningProvider;
import tech.pegasys.pantheon.util.enode.EnodeURL;
import java.util.List;
import java.util.Optional;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
class NodePermissioningAdapter extends PeerPermissions {
private static final Logger LOG = LogManager.getLogger();
private final NodePermissioningController nodePermissioningController;
private final List<EnodeURL> bootnodes;
private final Blockchain blockchain;
private final long blockchainListenId;
private final long nodePermissioningListenId;
public NodePermissioningAdapter(
final NodePermissioningController nodePermissioningController,
final List<EnodeURL> bootnodes,
final Blockchain blockchain) {
checkNotNull(nodePermissioningController);
checkNotNull(bootnodes);
checkNotNull(blockchain);
this.nodePermissioningController = nodePermissioningController;
this.bootnodes = bootnodes;
this.blockchain = blockchain;
// TODO: These events should be more targeted
blockchainListenId =
blockchain.observeBlockAdded((evt, chain) -> dispatchUpdate(true, Optional.empty()));
nodePermissioningListenId =
this.nodePermissioningController.subscribeToUpdates(
() -> dispatchUpdate(true, Optional.empty()));
}
@Override
public boolean isPermitted(final Peer localNode, final Peer remotePeer, final Action action) {
switch (action) {
case DISCOVERY_ALLOW_IN_PEER_TABLE:
return outboundIsPermitted(localNode, remotePeer);
case DISCOVERY_ALLOW_OUTBOUND_NEIGHBORS_REQUEST:
return allowOutboundNeighborsRequests(localNode, remotePeer);
case DISCOVERY_ALLOW_OUTBOUND_BONDING:
return allowOutboundBonding(localNode, remotePeer);
case DISCOVERY_ACCEPT_INBOUND_BONDING:
case DISCOVERY_SERVE_INBOUND_NEIGHBORS_REQUEST:
case RLPX_ALLOW_NEW_INBOUND_CONNECTION:
return inboundIsPermitted(localNode, remotePeer);
case RLPX_ALLOW_NEW_OUTBOUND_CONNECTION:
return outboundIsPermitted(localNode, remotePeer);
case RLPX_ALLOW_ONGOING_CONNECTION:
// TODO: This should probably check outbound || inbound, or the check should be more
// granular
return outboundIsPermitted(localNode, remotePeer);
default:
// Return false for unknown / unhandled permissions
LOG.error(
"Permissions denied for unknown action {}",
action.name(),
new IllegalStateException("Unhandled permissions action " + action.name()));
return false;
}
}
private boolean allowOutboundBonding(final Peer localNode, final Peer remotePeer) {
boolean outboundMessagingAllowed = outboundIsPermitted(localNode, remotePeer);
if (!nodePermissioningController.getSyncStatusNodePermissioningProvider().isPresent()) {
return outboundMessagingAllowed;
}
// We're using smart-contract based permissioning
// If we're out of sync, only allow bonding to our bootnodes
final SyncStatusNodePermissioningProvider syncStatus =
nodePermissioningController.getSyncStatusNodePermissioningProvider().get();
return outboundMessagingAllowed
&& (syncStatus.hasReachedSync() || bootnodes.contains(remotePeer.getEnodeURL()));
}
private boolean allowOutboundNeighborsRequests(final Peer localNode, final Peer remotePeer) {
boolean outboundMessagingAllowed = outboundIsPermitted(localNode, remotePeer);
if (!nodePermissioningController.getSyncStatusNodePermissioningProvider().isPresent()) {
return outboundMessagingAllowed;
}
// We're using smart-contract based permissioning
// Only allow neighbors requests if we're in sync
final SyncStatusNodePermissioningProvider syncStatus =
nodePermissioningController.getSyncStatusNodePermissioningProvider().get();
return outboundMessagingAllowed && syncStatus.hasReachedSync();
}
private boolean outboundIsPermitted(final Peer localNode, final Peer remotePeer) {
return nodePermissioningController.isPermitted(
localNode.getEnodeURL(), remotePeer.getEnodeURL());
}
private boolean inboundIsPermitted(final Peer localNode, final Peer remotePeer) {
return nodePermissioningController.isPermitted(
remotePeer.getEnodeURL(), localNode.getEnodeURL());
}
@Override
public void close() {
blockchain.removeObserver(blockchainListenId);
nodePermissioningController.unsubscribeFromUpdates(nodePermissioningListenId);
}
}

@ -22,7 +22,7 @@ import java.util.stream.Stream;
import com.google.common.collect.ImmutableList;
public abstract class PeerPermissions {
public abstract class PeerPermissions implements AutoCloseable {
private final Subscribers<PermissionsUpdateCallback> updateSubscribers = new Subscribers<>();
public static final PeerPermissions NOOP = new NoopPeerPermissions();
@ -39,11 +39,38 @@ public abstract class PeerPermissions {
return CombinedPeerPermissions.create(permissions);
}
// Defines what actions can be queried for permissions checks
public enum Action {
DISCOVERY_ALLOW_IN_PEER_TABLE,
DISCOVERY_ACCEPT_INBOUND_BONDING,
DISCOVERY_ALLOW_OUTBOUND_BONDING,
DISCOVERY_SERVE_INBOUND_NEIGHBORS_REQUEST,
DISCOVERY_ALLOW_OUTBOUND_NEIGHBORS_REQUEST,
RLPX_ALLOW_NEW_INBOUND_CONNECTION,
RLPX_ALLOW_NEW_OUTBOUND_CONNECTION,
RLPX_ALLOW_ONGOING_CONNECTION
}
/**
* @param peer The {@link Peer} object representing the remote node
* @return True if we are allowed to communicate with this peer.
*/
public abstract boolean isPermitted(final Peer peer);
/**
* Checks whether the local node is permitted to engage in some action with the given remote peer.
*
* @param localNode The local node that is querying for permissions.
* @param remotePeer The remote peer that the local node is checking for permissions
* @param action The action for which the local node is checking permissions
* @return {@code true} If the given action is allowed with the given peer
*/
public abstract boolean isPermitted(
final Peer localNode, final Peer remotePeer, final Action action);
@Override
public void close() {
// Do nothing by default
}
public void subscribeUpdate(final PermissionsUpdateCallback callback) {
updateSubscribers.subscribe(callback);
@ -56,7 +83,7 @@ public abstract class PeerPermissions {
private static class NoopPeerPermissions extends PeerPermissions {
@Override
public boolean isPermitted(final Peer peer) {
public boolean isPermitted(final Peer localNode, final Peer remotePeer, final Action action) {
return true;
}
}
@ -99,13 +126,20 @@ public abstract class PeerPermissions {
}
@Override
public boolean isPermitted(final Peer peer) {
public boolean isPermitted(final Peer localNode, final Peer remotePeer, final Action action) {
for (PeerPermissions permission : permissions) {
if (!permission.isPermitted(peer)) {
if (!permission.isPermitted(localNode, remotePeer, action)) {
return false;
}
}
return true;
}
@Override
public void close() {
for (final PeerPermissions permission : permissions) {
permission.close();
}
}
}
}

@ -51,8 +51,8 @@ public class PeerPermissionsBlacklist extends PeerPermissions {
}
@Override
public boolean isPermitted(final Peer peer) {
return !blacklist.contains(peer.getId());
public boolean isPermitted(final Peer localNode, final Peer remotePeer, final Action action) {
return !blacklist.contains(remotePeer.getId());
}
public void add(final Peer peer) {
@ -78,4 +78,9 @@ public class PeerPermissionsBlacklist extends PeerPermissions {
dispatchUpdate(false, Optional.empty());
}
}
@Override
public void close() {
blacklist.clear();
}
}

@ -0,0 +1,44 @@
/*
* Copyright 2019 ConsenSys AG.
*
* 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.
*/
package tech.pegasys.pantheon.ethereum.p2p.rlpx;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions.Action;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PermissionsUpdateCallback;
public class PeerRlpxPermissions {
private final Peer localNode;
private final PeerPermissions peerPermissions;
public PeerRlpxPermissions(final Peer localNode, final PeerPermissions peerPermissions) {
this.localNode = localNode;
this.peerPermissions = peerPermissions;
}
public boolean allowNewOutboundConnectionTo(final Peer peer) {
return peerPermissions.isPermitted(localNode, peer, Action.RLPX_ALLOW_NEW_OUTBOUND_CONNECTION);
}
public boolean allowNewInboundConnectionFrom(final Peer peer) {
return peerPermissions.isPermitted(localNode, peer, Action.RLPX_ALLOW_NEW_INBOUND_CONNECTION);
}
public boolean allowOngoingConnection(final Peer peer) {
return peerPermissions.isPermitted(localNode, peer, Action.RLPX_ALLOW_ONGOING_CONNECTION);
}
public void subscribeUpdate(final PermissionsUpdateCallback callback) {
peerPermissions.subscribeUpdate(callback);
}
}

@ -1,105 +0,0 @@
/*
* Copyright 2018 ConsenSys AG.
*
* 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.
*/
package tech.pegasys.pantheon.ethereum.p2p;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.permissioning.node.NodePermissioningController;
import tech.pegasys.pantheon.util.enode.EnodeURL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Collectors;
import org.mockito.ArgumentCaptor;
public class NodePermissioningControllerTestHelper {
private final EnodeURL localNode;
private final Collection<EnodeURL> permittedNodes = new ArrayList<>();
private final Collection<EnodeURL> notPermittedNodes = new ArrayList<>();
private boolean allowAll = false;
private boolean denyAll = false;
public NodePermissioningControllerTestHelper(final Peer localPeer) {
this.localNode = localPeer.getEnodeURL();
}
public NodePermissioningControllerTestHelper withPermittedPeers(final Peer... peers) {
this.permittedNodes.addAll(
Arrays.stream(peers).map(Peer::getEnodeURL).collect(Collectors.toList()));
return this;
}
public NodePermissioningControllerTestHelper withForbiddenPeers(final Peer... peers) {
this.notPermittedNodes.addAll(
Arrays.stream(peers).map(Peer::getEnodeURL).collect(Collectors.toList()));
return this;
}
public NodePermissioningControllerTestHelper allowAll() {
this.allowAll = true;
return this;
}
public NodePermissioningControllerTestHelper denyAll() {
this.denyAll = true;
return this;
}
public NodePermissioningController build() {
final NodePermissioningController nodePermissioningController =
spy(new NodePermissioningController(Optional.empty(), new ArrayList<>()));
if (allowAll && denyAll) {
throw new IllegalArgumentException(
"Can't allow all nodes and deny all nodes in the same NodePermissioningController");
} else if (allowAll) {
when(nodePermissioningController.isPermitted(any(), any())).thenReturn(true);
} else if (denyAll) {
when(nodePermissioningController.isPermitted(any(), any())).thenReturn(false);
} else {
permittedNodes.forEach(
node -> {
when(nodePermissioningController.isPermitted(eq(localNode), eq(node))).thenReturn(true);
when(nodePermissioningController.isPermitted(eq(node), eq(localNode))).thenReturn(true);
});
notPermittedNodes.forEach(
node -> {
when(nodePermissioningController.isPermitted(eq(localNode), eq(node)))
.thenReturn(false);
when(nodePermissioningController.isPermitted(eq(node), eq(localNode)))
.thenReturn(false);
});
}
ArgumentCaptor<Runnable> callback = ArgumentCaptor.forClass(Runnable.class);
doAnswer(
(i) -> {
callback.getValue().run();
return null;
})
.when(nodePermissioningController)
.startPeerDiscoveryCallback(callback.capture());
return nodePermissioningController;
}
}

@ -15,6 +15,10 @@ package tech.pegasys.pantheon.ethereum.p2p.discovery;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryTestHelper.AgentBuilder;
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.FindNeighborsPacketData;
@ -24,6 +28,8 @@ import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.NeighborsPacketData
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.Packet;
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PacketType;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions.Action;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissionsBlacklist;
import tech.pegasys.pantheon.util.enode.EnodeURL;
@ -153,28 +159,293 @@ public class PeerDiscoveryAgentTest {
assertThat(peerDiscoveryAgent2.streamDiscoveredPeers().collect(toList()).size()).isEqualTo(0);
}
protected void bondViaIncomingPing(
final MockPeerDiscoveryAgent agent, final MockPeerDiscoveryAgent otherNode) {
final Packet pingPacket = helper.createPingPacket(otherNode, agent);
helper.sendMessageBetweenAgents(otherNode, agent, pingPacket);
@Test
public void peerTable_allowPeer() {
// Setup peer
final MockPeerDiscoveryAgent otherNode = helper.startDiscoveryAgent();
final DiscoveryPeer remotePeer = otherNode.getAdvertisedPeer().get();
final PeerPermissions peerPermissions = mock(PeerPermissions.class);
final MockPeerDiscoveryAgent agent =
helper.createDiscoveryAgent(
helper.agentBuilder().bootstrapPeers(remotePeer).peerPermissions(peerPermissions));
when(peerPermissions.isPermitted(any(), any(), any())).thenReturn(false);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_ALLOW_IN_PEER_TABLE)))
.thenReturn(true);
agent.start(999);
assertThat(agent.streamDiscoveredPeers()).hasSize(1);
}
@Test
public void peerTable_disallowPeer() {
// Setup peer
final MockPeerDiscoveryAgent otherNode = helper.startDiscoveryAgent();
final DiscoveryPeer remotePeer = otherNode.getAdvertisedPeer().get();
final PeerPermissions peerPermissions = mock(PeerPermissions.class);
final MockPeerDiscoveryAgent agent =
helper.createDiscoveryAgent(
helper.agentBuilder().bootstrapPeers(remotePeer).peerPermissions(peerPermissions));
when(peerPermissions.isPermitted(any(), any(), any())).thenReturn(true);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_ALLOW_IN_PEER_TABLE)))
.thenReturn(false);
agent.start(999);
assertThat(agent.streamDiscoveredPeers()).hasSize(0);
}
@Test
public void dontBondWithNonPermittedPeer() {
public void bonding_allowIncomingBonding() {
// Start an agent with no bootstrap peers.
final PeerPermissionsBlacklist blacklist = PeerPermissionsBlacklist.create();
final PeerPermissions peerPermissions = mock(PeerPermissions.class);
final MockPeerDiscoveryAgent agent =
helper.startDiscoveryAgent(Collections.emptyList(), blacklist);
// Setup peer
helper.startDiscoveryAgent(Collections.emptyList(), peerPermissions);
final Peer localNode = agent.getAdvertisedPeer().get();
// Setup peer and permissions
final MockPeerDiscoveryAgent otherNode = helper.startDiscoveryAgent();
final Peer remotePeer = otherNode.getAdvertisedPeer().get();
when(peerPermissions.isPermitted(eq(localNode), any(), any())).thenReturn(false);
when(peerPermissions.isPermitted(
eq(localNode), eq(remotePeer), eq(Action.DISCOVERY_ACCEPT_INBOUND_BONDING)))
.thenReturn(true);
// Bond
bondViaIncomingPing(agent, otherNode);
blacklist.add(otherNode.getId());
// Check that peer received a return pong
List<IncomingPacket> remoteIncomingPackets = otherNode.getIncomingPackets();
assertThat(remoteIncomingPackets).hasSize(1);
final IncomingPacket firstMsg = remoteIncomingPackets.get(0);
assertThat(firstMsg.packet.getType()).isEqualTo(PacketType.PONG);
assertThat(firstMsg.fromAgent).isEqualTo(agent);
}
@Test
public void bonding_disallowIncomingBonding() {
// Start an agent with no bootstrap peers.
final PeerPermissions peerPermissions = mock(PeerPermissions.class);
final MockPeerDiscoveryAgent agent =
helper.startDiscoveryAgent(Collections.emptyList(), peerPermissions);
final Peer localNode = agent.getAdvertisedPeer().get();
// Setup peer and permissions
final MockPeerDiscoveryAgent otherNode = helper.startDiscoveryAgent();
final Peer remotePeer = otherNode.getAdvertisedPeer().get();
when(peerPermissions.isPermitted(eq(localNode), any(), any())).thenReturn(true);
when(peerPermissions.isPermitted(
eq(localNode), eq(remotePeer), eq(Action.DISCOVERY_ACCEPT_INBOUND_BONDING)))
.thenReturn(false);
// Bond
bondViaIncomingPing(agent, otherNode);
// Check peer was not allowed to connect
assertThat(agent.streamDiscoveredPeers()).hasSize(0);
// Check that peer did not receive a return pong
assertThat(otherNode.getIncomingPackets()).isEmpty();
}
@Test
public void bonding_allowOutgoingBonding() {
// Setup peer
final MockPeerDiscoveryAgent otherNode = helper.startDiscoveryAgent();
final DiscoveryPeer remotePeer = otherNode.getAdvertisedPeer().get();
final PeerPermissions peerPermissions = mock(PeerPermissions.class);
final MockPeerDiscoveryAgent agent =
helper.createDiscoveryAgent(
helper.agentBuilder().bootstrapPeers(remotePeer).peerPermissions(peerPermissions));
when(peerPermissions.isPermitted(any(), any(), any())).thenReturn(false);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_ALLOW_OUTBOUND_BONDING)))
.thenReturn(true);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_ALLOW_IN_PEER_TABLE)))
.thenReturn(true);
agent.start(999);
// Check peer was allowed
assertThat(agent.streamDiscoveredPeers()).hasSize(1);
// Check that peer received a return ping
List<IncomingPacket> remoteIncomingPackets = otherNode.getIncomingPackets();
assertThat(remoteIncomingPackets).hasSize(1);
final IncomingPacket firstMsg = remoteIncomingPackets.get(0);
assertThat(firstMsg.packet.getType()).isEqualTo(PacketType.PING);
assertThat(firstMsg.fromAgent).isEqualTo(agent);
}
@Test
public void bonding_disallowOutgoingBonding() {
// Setup peer
final MockPeerDiscoveryAgent otherNode = helper.startDiscoveryAgent();
final DiscoveryPeer remotePeer = otherNode.getAdvertisedPeer().get();
final PeerPermissions peerPermissions = mock(PeerPermissions.class);
final MockPeerDiscoveryAgent agent =
helper.createDiscoveryAgent(
helper.agentBuilder().bootstrapPeers(remotePeer).peerPermissions(peerPermissions));
when(peerPermissions.isPermitted(any(), any(), any())).thenReturn(false);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_ALLOW_OUTBOUND_BONDING)))
.thenReturn(false);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_ALLOW_IN_PEER_TABLE)))
.thenReturn(true);
agent.start(999);
assertThat(agent.streamDiscoveredPeers()).hasSize(1);
List<IncomingPacket> remoteIncomingPackets = otherNode.getIncomingPackets();
assertThat(remoteIncomingPackets).isEmpty();
}
@Test
public void neighbors_allowOutgoingRequest() {
// Setup peer
final MockPeerDiscoveryAgent otherNode = helper.startDiscoveryAgent();
final DiscoveryPeer remotePeer = otherNode.getAdvertisedPeer().get();
final PeerPermissions peerPermissions = mock(PeerPermissions.class);
final MockPeerDiscoveryAgent agent =
helper.createDiscoveryAgent(
helper.agentBuilder().bootstrapPeers(remotePeer).peerPermissions(peerPermissions));
when(peerPermissions.isPermitted(any(), any(), any())).thenReturn(false);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_ALLOW_IN_PEER_TABLE)))
.thenReturn(true);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_ALLOW_OUTBOUND_BONDING)))
.thenReturn(true);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_ALLOW_OUTBOUND_NEIGHBORS_REQUEST)))
.thenReturn(true);
agent.start(999);
assertThat(agent.streamDiscoveredPeers()).hasSize(1);
List<IncomingPacket> remoteIncomingPackets = otherNode.getIncomingPackets();
assertThat(remoteIncomingPackets).hasSize(2);
// Peer should get a ping
final IncomingPacket firstMsg = remoteIncomingPackets.get(0);
assertThat(firstMsg.packet.getType()).isEqualTo(PacketType.PING);
assertThat(firstMsg.fromAgent).isEqualTo(agent);
// Then a neighbors request
final IncomingPacket secondMsg = remoteIncomingPackets.get(1);
assertThat(secondMsg.packet.getType()).isEqualTo(PacketType.FIND_NEIGHBORS);
assertThat(secondMsg.fromAgent).isEqualTo(agent);
}
@Test
public void neighbors_disallowOutgoingRequest() {
// Setup peer
final MockPeerDiscoveryAgent otherNode = helper.startDiscoveryAgent();
final DiscoveryPeer remotePeer = otherNode.getAdvertisedPeer().get();
final PeerPermissions peerPermissions = mock(PeerPermissions.class);
final MockPeerDiscoveryAgent agent =
helper.createDiscoveryAgent(
helper.agentBuilder().bootstrapPeers(remotePeer).peerPermissions(peerPermissions));
when(peerPermissions.isPermitted(any(), any(), any())).thenReturn(true);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_ALLOW_OUTBOUND_NEIGHBORS_REQUEST)))
.thenReturn(false);
agent.start(999);
assertThat(agent.streamDiscoveredPeers()).hasSize(1);
List<IncomingPacket> remoteIncomingPackets = otherNode.getIncomingPackets();
assertThat(remoteIncomingPackets).hasSize(1);
// Peer should get a ping
final IncomingPacket firstMsg = remoteIncomingPackets.get(0);
assertThat(firstMsg.packet.getType()).isEqualTo(PacketType.PING);
assertThat(firstMsg.fromAgent).isEqualTo(agent);
}
@Test
public void neighbors_allowIncomingRequest() {
// Setup peer
final MockPeerDiscoveryAgent otherNode = helper.startDiscoveryAgent();
final DiscoveryPeer remotePeer = otherNode.getAdvertisedPeer().get();
final PeerPermissions peerPermissions = mock(PeerPermissions.class);
final MockPeerDiscoveryAgent agent =
helper.createDiscoveryAgent(
helper.agentBuilder().bootstrapPeers(remotePeer).peerPermissions(peerPermissions));
when(peerPermissions.isPermitted(any(), any(), any())).thenReturn(false);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_ALLOW_IN_PEER_TABLE)))
.thenReturn(true);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_ALLOW_OUTBOUND_BONDING)))
.thenReturn(true);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_SERVE_INBOUND_NEIGHBORS_REQUEST)))
.thenReturn(true);
agent.start(999);
// Send request for neighbors
requestNeighbors(otherNode, agent);
assertThat(agent.streamDiscoveredPeers()).hasSize(1);
List<IncomingPacket> remoteIncomingPackets = otherNode.getIncomingPackets();
assertThat(remoteIncomingPackets).hasSize(2);
// Peer should get a ping
final IncomingPacket firstMsg = remoteIncomingPackets.get(0);
assertThat(firstMsg.packet.getType()).isEqualTo(PacketType.PING);
assertThat(firstMsg.fromAgent).isEqualTo(agent);
// Then a neighbors response
final IncomingPacket secondMsg = remoteIncomingPackets.get(1);
assertThat(secondMsg.packet.getType()).isEqualTo(PacketType.NEIGHBORS);
assertThat(secondMsg.fromAgent).isEqualTo(agent);
}
@Test
public void neighbors_disallowIncomingRequest() {
// Setup peer
final MockPeerDiscoveryAgent otherNode = helper.startDiscoveryAgent();
final DiscoveryPeer remotePeer = otherNode.getAdvertisedPeer().get();
final PeerPermissions peerPermissions = mock(PeerPermissions.class);
final MockPeerDiscoveryAgent agent =
helper.createDiscoveryAgent(
helper.agentBuilder().bootstrapPeers(remotePeer).peerPermissions(peerPermissions));
when(peerPermissions.isPermitted(any(), any(), any())).thenReturn(true);
when(peerPermissions.isPermitted(
any(), eq(remotePeer), eq(Action.DISCOVERY_SERVE_INBOUND_NEIGHBORS_REQUEST)))
.thenReturn(false);
agent.start(999);
// Send request for neighbors
requestNeighbors(otherNode, agent);
// Peer should get a ping and a neighbors request, but no neighbors response
assertThat(agent.streamDiscoveredPeers()).hasSize(1);
List<IncomingPacket> remoteIncomingPackets = otherNode.getIncomingPackets();
assertThat(remoteIncomingPackets).hasSize(2);
// Peer should get a ping
final IncomingPacket firstMsg = remoteIncomingPackets.get(0);
assertThat(firstMsg.packet.getType()).isEqualTo(PacketType.PING);
assertThat(firstMsg.fromAgent).isEqualTo(agent);
// And a request FOR neighbors, but no response to its neighbors request
final IncomingPacket secondMsg = remoteIncomingPackets.get(1);
assertThat(secondMsg.packet.getType()).isEqualTo(PacketType.FIND_NEIGHBORS);
assertThat(secondMsg.fromAgent).isEqualTo(agent);
}
@Test
@ -192,4 +463,17 @@ public class PeerDiscoveryAgentTest {
assertThat(agent.isActive()).isFalse();
}
protected void bondViaIncomingPing(
final MockPeerDiscoveryAgent agent, final MockPeerDiscoveryAgent otherNode) {
final Packet pingPacket = helper.createPingPacket(otherNode, agent);
helper.sendMessageBetweenAgents(otherNode, agent, pingPacket);
}
protected void requestNeighbors(
final MockPeerDiscoveryAgent fromAgent, final MockPeerDiscoveryAgent toAgent) {
final FindNeighborsPacketData data = FindNeighborsPacketData.create(Peer.randomId());
final Packet packet = Packet.create(PacketType.FIND_NEIGHBORS, data, fromAgent.getKeyPair());
helper.sendMessageBetweenAgents(fromAgent, toAgent, packet);
}
}

@ -23,7 +23,6 @@ import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PacketType;
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PingPacketData;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions;
import tech.pegasys.pantheon.ethereum.permissioning.node.NodePermissioningController;
import tech.pegasys.pantheon.util.bytes.BytesValue;
import tech.pegasys.pantheon.util.enode.EnodeURL;
@ -32,7 +31,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -178,7 +176,6 @@ public class PeerDiscoveryTestHelper {
private final Map<BytesValue, MockPeerDiscoveryAgent> agents;
private final AtomicInteger nextAvailablePort;
private Optional<NodePermissioningController> nodePermissioningController = Optional.empty();
private List<EnodeURL> bootnodes = Collections.emptyList();
private boolean active = true;
private PeerPermissions peerPermissions = PeerPermissions.noop();
@ -208,11 +205,6 @@ public class PeerDiscoveryTestHelper {
return peers.stream().map(Peer::getEnodeURL).collect(Collectors.toList());
}
public AgentBuilder nodePermissioningController(final NodePermissioningController controller) {
this.nodePermissioningController = Optional.ofNullable(controller);
return this;
}
public AgentBuilder peerPermissions(final PeerPermissions peerPermissions) {
this.peerPermissions = peerPermissions;
return this;
@ -230,11 +222,7 @@ public class PeerDiscoveryTestHelper {
config.setActive(active);
return new MockPeerDiscoveryAgent(
SECP256K1.KeyPair.generate(),
config,
peerPermissions,
nodePermissioningController,
agents);
SECP256K1.KeyPair.generate(), config, peerPermissions, agents);
}
}
}

@ -18,7 +18,6 @@ import tech.pegasys.pantheon.ethereum.p2p.discovery.DiscoveryPeer;
import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryAgent;
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PeerDiscoveryController.AsyncExecutor;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions;
import tech.pegasys.pantheon.ethereum.permissioning.node.NodePermissioningController;
import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem;
import tech.pegasys.pantheon.util.bytes.BytesValue;
@ -28,7 +27,6 @@ import java.util.Arrays;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class MockPeerDiscoveryAgent extends PeerDiscoveryAgent {
@ -40,9 +38,8 @@ public class MockPeerDiscoveryAgent extends PeerDiscoveryAgent {
final KeyPair keyPair,
final DiscoveryConfiguration config,
final PeerPermissions peerPermissions,
final Optional<NodePermissioningController> nodePermissioningController,
final Map<BytesValue, MockPeerDiscoveryAgent> agentNetwork) {
super(keyPair, config, peerPermissions, nodePermissioningController, new NoOpMetricsSystem());
super(keyPair, config, peerPermissions, new NoOpMetricsSystem());
this.agentNetwork = agentNetwork;
}

@ -30,7 +30,6 @@ import static org.mockito.Mockito.when;
import tech.pegasys.pantheon.crypto.SECP256K1;
import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair;
import tech.pegasys.pantheon.ethereum.p2p.NodePermissioningControllerTestHelper;
import tech.pegasys.pantheon.ethereum.p2p.discovery.DiscoveryPeer;
import tech.pegasys.pantheon.ethereum.p2p.discovery.Endpoint;
import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryEvent.PeerBondedEvent;
@ -38,8 +37,8 @@ import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryStatus;
import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryTestHelper;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions.Action;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissionsBlacklist;
import tech.pegasys.pantheon.ethereum.permissioning.node.NodePermissioningController;
import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem;
import tech.pegasys.pantheon.util.Subscribers;
import tech.pegasys.pantheon.util.bytes.Bytes32;
@ -973,83 +972,205 @@ public class PeerDiscoveryControllerTest {
}
@Test
public void shouldNotBondWithNonPermittedPeer() {
public void streamDiscoveredPeers() {
final List<DiscoveryPeer> peers = createPeersInLastBucket(localPeer, 3);
final OutboundMessageHandler outboundMessageHandler = mock(OutboundMessageHandler.class);
final PeerPermissions peerPermissions = mock(PeerPermissions.class);
doReturn(true).when(peerPermissions).isPermitted(any(), any(), any());
final DiscoveryPeer discoveryPeer = peers.get(0);
final DiscoveryPeer notPermittedPeer = peers.get(1);
final DiscoveryPeer permittedPeer = peers.get(2);
final DiscoveryPeer localNode =
DiscoveryPeer.fromEnode(
EnodeURL.builder()
.ipAddress("127.0.0.1")
.nodeId(Peer.randomId())
.discoveryAndListeningPorts(30303)
.build());
final NodePermissioningController nodePermissioningController =
new NodePermissioningControllerTestHelper(localPeer)
.withPermittedPeers(discoveryPeer, permittedPeer)
.withForbiddenPeers(notPermittedPeer)
controller =
getControllerBuilder()
.localPeer(localNode)
.peers(peers)
.outboundMessageHandler(outboundMessageHandler)
.peerPermissions(peerPermissions)
.build();
controller.start();
assertThat(controller.streamDiscoveredPeers().collect(Collectors.toList()))
.containsExactlyInAnyOrderElementsOf(peers);
// Disallow peer - it should be filtered from list
final Peer disallowed = peers.get(0);
doReturn(false)
.when(peerPermissions)
.isPermitted(eq(localNode), eq(disallowed), eq(Action.DISCOVERY_ALLOW_IN_PEER_TABLE));
// Peer stream should filter disallowed
assertThat(controller.streamDiscoveredPeers().collect(Collectors.toList()))
.containsExactlyInAnyOrder(peers.get(1), peers.get(2));
}
@Test
public void updatePermissions_restrictWithList() {
final List<DiscoveryPeer> peers = createPeersInLastBucket(localPeer, 3);
final OutboundMessageHandler outboundMessageHandler = mock(OutboundMessageHandler.class);
final TestPeerPermissions peerPermissions = spy(new TestPeerPermissions());
doReturn(true).when(peerPermissions).isPermitted(any(), any(), any());
final DiscoveryPeer localNode =
DiscoveryPeer.fromEnode(
EnodeURL.builder()
.ipAddress("127.0.0.1")
.nodeId(Peer.randomId())
.discoveryAndListeningPorts(30303)
.build());
controller =
getControllerBuilder()
.peers(discoveryPeer)
.nodePermissioningController(nodePermissioningController)
.localPeer(localNode)
.peers(peers)
.outboundMessageHandler(outboundMessageHandler)
.peerPermissions(peerPermissions)
.build();
controller.start();
final Endpoint localEndpoint = localPeer.getEndpoint();
assertThat(controller.streamDiscoveredPeers().collect(Collectors.toList()))
.containsExactlyInAnyOrderElementsOf(peers);
// Setup ping to be sent to discoveryPeer
List<SECP256K1.KeyPair> keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1);
PingPacketData pingPacketData =
PingPacketData.create(localEndpoint, discoveryPeer.getEndpoint());
final Packet discoPeerPing = Packet.create(PacketType.PING, pingPacketData, keyPairs.get(0));
mockPacketCreation(PacketType.PING, discoveryPeer, discoPeerPing);
// Disallow peer - it should be filtered from list
final Peer disallowed = peers.get(0);
doReturn(false)
.when(peerPermissions)
.isPermitted(eq(localNode), eq(disallowed), eq(Action.DISCOVERY_ALLOW_IN_PEER_TABLE));
peerPermissions.testDispatchUpdate(true, Optional.of(Collections.singletonList(disallowed)));
controller.start();
verify(outboundMessageHandler, times(1)).send(any(), matchPacketOfType(PacketType.PING));
// Peer stream should filter disallowed
assertThat(controller.streamDiscoveredPeers().collect(Collectors.toList()))
.containsExactlyInAnyOrder(peers.get(1), peers.get(2));
final Packet pongFromDiscoPeer =
MockPacketDataFactory.mockPongPacket(discoveryPeer, discoPeerPing.getHash());
controller.onMessage(pongFromDiscoPeer, discoveryPeer);
// Peer should be dropped
verify(controller, times(1)).dropPeer(any());
verify(controller, times(1)).dropPeer(disallowed);
}
verify(outboundMessageHandler, times(1))
.send(eq(discoveryPeer), matchPacketOfType(PacketType.FIND_NEIGHBORS));
@Test
public void updatePermissions_restrictWithNoList() {
final List<DiscoveryPeer> peers = createPeersInLastBucket(localPeer, 3);
final OutboundMessageHandler outboundMessageHandler = mock(OutboundMessageHandler.class);
final TestPeerPermissions peerPermissions = spy(new TestPeerPermissions());
final MockTimerUtil timerUtil = new MockTimerUtil();
doReturn(true).when(peerPermissions).isPermitted(any(), any(), any());
final DiscoveryPeer localNode =
DiscoveryPeer.fromEnode(
EnodeURL.builder()
.ipAddress("127.0.0.1")
.nodeId(Peer.randomId())
.discoveryAndListeningPorts(30303)
.build());
// Setup ping to be sent to otherPeer after neighbors packet is received
keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1);
pingPacketData = PingPacketData.create(localEndpoint, notPermittedPeer.getEndpoint());
final Packet pingPacket = Packet.create(PacketType.PING, pingPacketData, keyPairs.get(0));
mockPacketCreation(PacketType.PING, notPermittedPeer, pingPacket);
controller =
getControllerBuilder()
.localPeer(localNode)
.peers(peers)
.outboundMessageHandler(outboundMessageHandler)
.peerPermissions(peerPermissions)
.timerUtil(timerUtil)
.build();
controller.start();
// Setup ping to be sent to otherPeer2 after neighbors packet is received
keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1);
pingPacketData = PingPacketData.create(localEndpoint, permittedPeer.getEndpoint());
final Packet pingPacket2 = Packet.create(PacketType.PING, pingPacketData, keyPairs.get(0));
mockPacketCreation(PacketType.PING, permittedPeer, pingPacket2);
assertThat(controller.streamDiscoveredPeers().collect(Collectors.toList()))
.containsExactlyInAnyOrderElementsOf(peers);
final Packet neighborsPacket =
MockPacketDataFactory.mockNeighborsPacket(discoveryPeer, notPermittedPeer, permittedPeer);
controller.onMessage(neighborsPacket, discoveryPeer);
// Disallow peer - it should be filtered from list
final Peer disallowed = peers.get(0);
doReturn(false)
.when(peerPermissions)
.isPermitted(eq(localNode), eq(disallowed), eq(Action.DISCOVERY_ALLOW_IN_PEER_TABLE));
peerPermissions.testDispatchUpdate(true, Optional.empty());
timerUtil.runHandlers();
// Peer stream should filter disallowed
assertThat(controller.streamDiscoveredPeers().collect(Collectors.toList()))
.containsExactlyInAnyOrder(peers.get(1), peers.get(2));
verify(controller, times(0)).bond(notPermittedPeer);
verify(controller, times(1)).bond(permittedPeer);
// Peer should be dropped
verify(controller, times(1)).dropPeer(any());
verify(controller, times(1)).dropPeer(disallowed);
}
@Test
public void shouldNotRespondToPingFromNonWhitelistedDiscoveryPeer() {
public void updatePermissions_relaxPermissionsWithList() {
final List<DiscoveryPeer> peers = createPeersInLastBucket(localPeer, 3);
final DiscoveryPeer discoPeer = peers.get(0);
final OutboundMessageHandler outboundMessageHandler = mock(OutboundMessageHandler.class);
final TestPeerPermissions peerPermissions = spy(new TestPeerPermissions());
final MockTimerUtil timerUtil = new MockTimerUtil();
doReturn(true).when(peerPermissions).isPermitted(any(), any(), any());
final DiscoveryPeer localNode =
DiscoveryPeer.fromEnode(
EnodeURL.builder()
.ipAddress("127.0.0.1")
.nodeId(Peer.randomId())
.discoveryAndListeningPorts(30303)
.build());
final NodePermissioningController nodePermissioningController =
new NodePermissioningControllerTestHelper(localPeer).withForbiddenPeers(discoPeer).build();
controller =
getControllerBuilder()
.localPeer(localNode)
.peers(peers)
.outboundMessageHandler(outboundMessageHandler)
.peerPermissions(peerPermissions)
.timerUtil(timerUtil)
.build();
controller.start();
assertThat(controller.streamDiscoveredPeers().collect(Collectors.toList()))
.containsExactlyInAnyOrderElementsOf(peers);
final Peer firstPeer = peers.get(0);
peerPermissions.testDispatchUpdate(false, Optional.of(Collections.singletonList(firstPeer)));
timerUtil.runHandlers();
assertThat(controller.streamDiscoveredPeers().collect(Collectors.toList()))
.containsExactlyInAnyOrderElementsOf(peers);
verify(controller, never()).dropPeer(any());
}
@Test
public void updatePermissions_relaxPermissionsWithNoList() {
final List<DiscoveryPeer> peers = createPeersInLastBucket(localPeer, 3);
final OutboundMessageHandler outboundMessageHandler = mock(OutboundMessageHandler.class);
final TestPeerPermissions peerPermissions = spy(new TestPeerPermissions());
final MockTimerUtil timerUtil = new MockTimerUtil();
doReturn(true).when(peerPermissions).isPermitted(any(), any(), any());
final DiscoveryPeer localNode =
DiscoveryPeer.fromEnode(
EnodeURL.builder()
.ipAddress("127.0.0.1")
.nodeId(Peer.randomId())
.discoveryAndListeningPorts(30303)
.build());
controller =
getControllerBuilder()
.peers(discoPeer)
.nodePermissioningController(nodePermissioningController)
.localPeer(localNode)
.peers(peers)
.outboundMessageHandler(outboundMessageHandler)
.peerPermissions(peerPermissions)
.timerUtil(timerUtil)
.build();
controller.start();
final Packet pingPacket = mockPingPacket(peers.get(0), localPeer);
controller.onMessage(pingPacket, peers.get(0));
assertThat(controller.streamDiscoveredPeers()).doesNotContain(peers.get(0));
assertThat(controller.streamDiscoveredPeers().collect(Collectors.toList()))
.containsExactlyInAnyOrderElementsOf(peers);
peerPermissions.testDispatchUpdate(false, Optional.empty());
timerUtil.runHandlers();
assertThat(controller.streamDiscoveredPeers().collect(Collectors.toList()))
.containsExactlyInAnyOrderElementsOf(peers);
verify(controller, never()).dropPeer(any());
}
private static Packet mockPingPacket(final DiscoveryPeer from, final DiscoveryPeer to) {
@ -1113,7 +1234,6 @@ public class PeerDiscoveryControllerTest {
static class ControllerBuilder {
private Collection<DiscoveryPeer> discoPeers = Collections.emptyList();
private Optional<NodePermissioningController> nodePermissioningController = Optional.empty();
private MockTimerUtil timerUtil = new MockTimerUtil();
private KeyPair keypair;
private DiscoveryPeer localPeer;
@ -1142,11 +1262,6 @@ public class PeerDiscoveryControllerTest {
return this;
}
ControllerBuilder nodePermissioningController(final NodePermissioningController controller) {
this.nodePermissioningController = Optional.of(controller);
return this;
}
ControllerBuilder timerUtil(final MockTimerUtil timerUtil) {
this.timerUtil = timerUtil;
return this;
@ -1197,10 +1312,22 @@ public class PeerDiscoveryControllerTest {
.tableRefreshIntervalMs(TABLE_REFRESH_INTERVAL_MS)
.peerRequirement(PEER_REQUIREMENT)
.peerPermissions(peerPermissions)
.nodePermissioningController(nodePermissioningController)
.peerBondedObservers(peerBondedObservers)
.metricsSystem(new NoOpMetricsSystem())
.build());
}
}
private static class TestPeerPermissions extends PeerPermissions {
@Override
public boolean isPermitted(final Peer localNode, final Peer remotePeer, final Action action) {
return true;
}
public void testDispatchUpdate(
final boolean permissionsRestricted, final Optional<List<Peer>> affectedPeers) {
this.dispatchUpdate(permissionsRestricted, affectedPeers);
}
}
}

@ -27,7 +27,6 @@ import tech.pegasys.pantheon.ethereum.p2p.discovery.DiscoveryPeer;
import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryStatus;
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.RecursivePeerRefreshState.BondingAgent;
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.RecursivePeerRefreshState.FindNeighbourDispatcher;
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.RecursivePeerRefreshState.OutboundDiscoveryMessagingPermissions;
import tech.pegasys.pantheon.util.bytes.BytesValue;
import tech.pegasys.pantheon.util.enode.EnodeURL;
@ -39,8 +38,7 @@ import org.junit.Test;
public class RecursivePeerRefreshStateTest {
private static final BytesValue TARGET = createId(0);
private final OutboundDiscoveryMessagingPermissions peerPermissions =
mock(OutboundDiscoveryMessagingPermissions.class);
private final PeerDiscoveryPermissions peerPermissions = mock(PeerDiscoveryPermissions.class);
private final BondingAgent bondingAgent = mock(BondingAgent.class);
private final FindNeighbourDispatcher neighborFinder = mock(FindNeighbourDispatcher.class);
private final MockTimerUtil timerUtil = new MockTimerUtil();
@ -65,7 +63,11 @@ public class RecursivePeerRefreshStateTest {
@Before
public void setup() {
// Default peerPermissions to be permissive
when(peerPermissions.isPermitted(any())).thenReturn(true);
when(peerPermissions.isAllowedInPeerTable(any())).thenReturn(true);
when(peerPermissions.allowInboundBonding(any())).thenReturn(true);
when(peerPermissions.allowOutboundBonding(any())).thenReturn(true);
when(peerPermissions.allowInboundNeighborsRequest(any())).thenReturn(true);
when(peerPermissions.allowOutboundNeighborsRequest(any())).thenReturn(true);
}
@Test
@ -268,19 +270,38 @@ public class RecursivePeerRefreshStateTest {
completeBonding(peer1);
completeBonding(peer2);
verify(neighborFinder).findNeighbours(peer1, TARGET);
verify(neighborFinder).findNeighbours(peer2, TARGET);
verify(neighborFinder, times(1)).findNeighbours(peer1, TARGET);
verify(neighborFinder, times(1)).findNeighbours(peer2, TARGET);
recursivePeerRefreshState.onNeighboursReceived(peer1, singletonList(peer2));
recursivePeerRefreshState.onNeighboursReceived(peer2, emptyList());
verify(bondingAgent, times(1)).performBonding(peer1);
verify(bondingAgent, times(1)).performBonding(peer2);
verify(neighborFinder).findNeighbours(peer1, TARGET);
verify(neighborFinder).findNeighbours(peer2, TARGET);
verify(neighborFinder, times(1)).findNeighbours(peer1, TARGET);
verify(neighborFinder, times(1)).findNeighbours(peer2, TARGET);
verifyNoMoreInteractions(bondingAgent, neighborFinder);
}
@Test
public void shouldNotQueryNodeMissingPermissions() {
peer1.setStatus(PeerDiscoveryStatus.KNOWN);
peer2.setStatus(PeerDiscoveryStatus.KNOWN);
when(peerPermissions.allowOutboundNeighborsRequest(peer1)).thenReturn(false);
recursivePeerRefreshState.start(asList(peer1, peer2), TARGET);
verify(bondingAgent).performBonding(peer2);
verify(bondingAgent).performBonding(peer2);
completeBonding(peer1);
completeBonding(peer2);
verify(neighborFinder, times(0)).findNeighbours(peer1, TARGET);
verify(neighborFinder, times(1)).findNeighbours(peer2, TARGET);
}
@Test
public void shouldBondWithNewNeighboursWhenSomeRequestsTimeOut() {
final BytesValue id0 =
@ -435,7 +456,7 @@ public class RecursivePeerRefreshStateTest {
final DiscoveryPeer peerA = createPeer(1, "127.0.0.1", 1, 1);
final DiscoveryPeer peerB = createPeer(2, "127.0.0.2", 2, 2);
when(peerPermissions.isPermitted(peerB)).thenReturn(false);
when(peerPermissions.allowOutboundBonding(peerB)).thenReturn(false);
recursivePeerRefreshState =
new RecursivePeerRefreshState(

@ -16,6 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Java6Assertions.assertThatThrownBy;
import static org.assertj.core.api.Java6Assertions.catchThrowable;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
@ -44,6 +45,8 @@ import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryEvent.PeerBonde
import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryStatus;
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions.Action;
import tech.pegasys.pantheon.ethereum.p2p.wire.Capability;
import tech.pegasys.pantheon.ethereum.p2p.wire.PeerInfo;
import tech.pegasys.pantheon.ethereum.p2p.wire.SubProtocol;
@ -55,7 +58,9 @@ import tech.pegasys.pantheon.util.enode.EnodeURL;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
@ -76,6 +81,7 @@ import org.mockito.junit.MockitoJUnitRunner;
public final class DefaultP2PNetworkTest {
@Mock private NodePermissioningController nodePermissioningController;
private TestPeerPermissions peerPermissions = spy(new TestPeerPermissions());
@Mock private Blockchain blockchain;
@ -92,6 +98,8 @@ public final class DefaultP2PNetworkTest {
@Before
public void before() {
when(blockchain.observeBlockAdded(observerCaptor.capture())).thenReturn(1L);
// Make permissions lenient by default
lenient().when(nodePermissioningController.isPermitted(any(), any())).thenReturn(true);
}
@After
@ -100,7 +108,7 @@ public final class DefaultP2PNetworkTest {
}
@Test
public void addingMaintainedNetworkPeerStartsConnection() {
public void addingMaintainedConnectionPeer_startsConnection() {
final DefaultP2PNetwork network = mockNetwork();
network.start();
final Peer peer = mockPeer();
@ -111,6 +119,24 @@ public final class DefaultP2PNetworkTest {
verify(network, times(1)).connect(peer);
}
@Test
public void addingMaintainedConnectionPeer_forDisallowedPeer() {
final DefaultP2PNetwork network = mockNetwork();
network.start();
final Peer localNode = DefaultPeer.fromEnodeURL(network.getLocalEnode().get());
final Peer peer = mockPeer();
doReturn(false)
.when(peerPermissions)
.isPermitted(eq(localNode), eq(peer), eq(Action.RLPX_ALLOW_NEW_OUTBOUND_CONNECTION));
assertThat(network.addMaintainConnectionPeer(peer)).isTrue();
// Add peer but do not connect
assertThat(network.peerMaintainConnectionList).contains(peer);
verify(network, times(0)).connect(peer);
}
@Test
public void addMaintainConnectionPeer_beforeStartingNetwork() {
final DefaultP2PNetwork network = mockNetwork();
@ -143,7 +169,7 @@ public final class DefaultP2PNetworkTest {
}
@Test
public void addingRepeatMaintainedPeersReturnsFalse() {
public void addingMaintainedConnectionPeer_repeatInvocationReturnsFalse() {
final P2PNetwork network = network();
network.start();
final Peer peer = mockPeer();
@ -164,15 +190,19 @@ public final class DefaultP2PNetworkTest {
}
@Test
public void checkMaintainedConnectionPeersDoesNotConnectToDisallowedPeer() {
public void checkMaintainedConnectionPeers_doesNotConnectToDisallowedPeer() {
final DefaultP2PNetwork network = mockNetwork();
network.start();
// Add peer that is not permitted
final Peer localNode = DefaultPeer.fromEnodeURL(network.getLocalEnode().get());
final Peer peer = mockPeer();
lenient().when(nodePermissioningController.isPermitted(any(), any())).thenReturn(false);
network.peerMaintainConnectionList.add(peer);
doReturn(false)
.when(peerPermissions)
.isPermitted(eq(localNode), eq(peer), eq(Action.RLPX_ALLOW_NEW_OUTBOUND_CONNECTION));
assertThat(network.peerMaintainConnectionList.add(peer)).isTrue();
network.checkMaintainedConnectionPeers();
verify(network, never()).connect(peer);
}
@ -251,13 +281,18 @@ public final class DefaultP2PNetworkTest {
}
@Test
public void whenStoppingNetworkWithNodePermissioningShouldUnsubscribeBlockAddedEvents() {
public void stop_removesListeners() {
final P2PNetwork network = network();
network.start();
verify(blockchain, never()).removeObserver(anyLong());
verify(nodePermissioningController, never()).unsubscribeFromUpdates(anyLong());
network.stop();
network.awaitStop();
verify(blockchain).removeObserver(eq(1L));
verify(blockchain, times(1)).removeObserver(anyLong());
verify(nodePermissioningController, times(1)).unsubscribeFromUpdates(anyLong());
}
@Test
@ -273,11 +308,14 @@ public final class DefaultP2PNetworkTest {
network.start();
network.connect(remotePeer1).complete(peerConnection1);
network.connect(remotePeer2).complete(peerConnection2);
// Permissions are checked on connection
verify(nodePermissioningController, times(2)).isPermitted(any(), any());
final BlockAddedObserver blockAddedObserver = observerCaptor.getValue();
blockAddedObserver.onBlockAdded(blockAddedEvent, blockchain);
verify(nodePermissioningController, times(2)).isPermitted(any(), any());
// Permissions should be checked again after block is added
verify(nodePermissioningController, times(4)).isPermitted(any(), any());
}
@Test
@ -315,7 +353,105 @@ public final class DefaultP2PNetworkTest {
}
@Test
public void removePeerReturnsTrueIfNodeWasInMaintaineConnectionsAndDisconnectsIfInPending() {
public void onPermissionsUpdate_permissionsRestrictedWithNoListOfPeers() {
final P2PNetwork network = network();
final Peer permittedPeer = mockPeer("127.0.0.2", 30302);
final Peer notPermittedPeer = mockPeer("127.0.0.3", 30303);
final PeerConnection permittedPeerConnection = mockPeerConnection(permittedPeer);
final PeerConnection notPermittedPeerConnection = mockPeerConnection(notPermittedPeer);
network.start();
final Peer localNode = DefaultPeer.fromEnodeURL(network.getLocalEnode().get());
network.connect(permittedPeer).complete(permittedPeerConnection);
network.connect(notPermittedPeer).complete(notPermittedPeerConnection);
verify(peerPermissions, times(2)).isPermitted(any(), any(), any());
doReturn(false)
.when(peerPermissions)
.isPermitted(eq(localNode), eq(notPermittedPeer), eq(Action.RLPX_ALLOW_ONGOING_CONNECTION));
peerPermissions.testDispatchUpdate(true, Optional.empty());
verify(peerPermissions, times(4)).isPermitted(any(), any(), any());
verify(notPermittedPeerConnection).disconnect(eq(DisconnectReason.REQUESTED));
verify(permittedPeerConnection, never()).disconnect(any());
}
@Test
public void onPermissionsUpdate_permissionsRestrictedWithListOfPeers() {
final P2PNetwork network = network();
final Peer permittedPeer = mockPeer("127.0.0.2", 30302);
final Peer notPermittedPeer = mockPeer("127.0.0.3", 30303);
final PeerConnection permittedPeerConnection = mockPeerConnection(permittedPeer);
final PeerConnection notPermittedPeerConnection = mockPeerConnection(notPermittedPeer);
network.start();
final Peer localNode = DefaultPeer.fromEnodeURL(network.getLocalEnode().get());
network.connect(permittedPeer).complete(permittedPeerConnection);
network.connect(notPermittedPeer).complete(notPermittedPeerConnection);
verify(peerPermissions, times(2)).isPermitted(any(), any(), any());
doReturn(false)
.when(peerPermissions)
.isPermitted(eq(localNode), eq(notPermittedPeer), eq(Action.RLPX_ALLOW_ONGOING_CONNECTION));
peerPermissions.testDispatchUpdate(
true, Optional.of(Collections.singletonList(notPermittedPeer)));
verify(peerPermissions, times(3)).isPermitted(any(), any(), any());
verify(notPermittedPeerConnection).disconnect(eq(DisconnectReason.REQUESTED));
verify(permittedPeerConnection, never()).disconnect(any());
}
@Test
public void onPermissionsUpdate_permissionsRelaxedWithNoListOfPeers() {
final P2PNetwork network = network();
final Peer permittedPeer = mockPeer("127.0.0.2", 30302);
final Peer notPermittedPeer = mockPeer("127.0.0.3", 30303);
final PeerConnection permittedPeerConnection = mockPeerConnection(permittedPeer);
final PeerConnection notPermittedPeerConnection = mockPeerConnection(notPermittedPeer);
network.start();
network.connect(permittedPeer).complete(permittedPeerConnection);
network.connect(notPermittedPeer).complete(notPermittedPeerConnection);
verify(peerPermissions, times(2)).isPermitted(any(), any(), any());
peerPermissions.testDispatchUpdate(false, Optional.empty());
verify(peerPermissions, times(2)).isPermitted(any(), any(), any());
verify(notPermittedPeerConnection, never()).disconnect(any());
verify(permittedPeerConnection, never()).disconnect(any());
}
@Test
public void onPermissionsUpdate_permissionsRelaxedWithListOfPeers() {
final P2PNetwork network = network();
final Peer permittedPeer = mockPeer("127.0.0.2", 30302);
final Peer notPermittedPeer = mockPeer("127.0.0.3", 30303);
final PeerConnection permittedPeerConnection = mockPeerConnection(permittedPeer);
final PeerConnection notPermittedPeerConnection = mockPeerConnection(notPermittedPeer);
network.start();
network.connect(permittedPeer).complete(permittedPeerConnection);
network.connect(notPermittedPeer).complete(notPermittedPeerConnection);
verify(peerPermissions, times(2)).isPermitted(any(), any(), any());
peerPermissions.testDispatchUpdate(
false, Optional.of(Collections.singletonList(notPermittedPeer)));
verify(peerPermissions, times(2)).isPermitted(any(), any(), any());
verify(notPermittedPeerConnection, never()).disconnect(any());
verify(permittedPeerConnection, never()).disconnect(any());
}
@Test
public void removePeerReturnsTrueIfNodeWasInMaintainedConnectionsAndDisconnectsIfInPending() {
final DefaultP2PNetwork network = network();
network.start();
@ -502,6 +638,8 @@ public final class DefaultP2PNetworkTest {
mockNetwork(() -> RlpxConfiguration.create().setMaxPeers(maxPeers));
network.start();
final Peer localNode = DefaultPeer.fromEnodeURL(network.getLocalEnode().get());
doReturn(2).when(network).connectionCount();
final List<DiscoveryPeer> peers =
Stream.iterate(1, n -> n + 1)
@ -521,10 +659,12 @@ public final class DefaultP2PNetworkTest {
// Set up the highest value peer to lack permissions
DiscoveryPeer highestValueNonPermittedPeer = peers.get(0);
highestValueNonPermittedPeer.setLastAttemptedConnection(1);
when(nodePermissioningController.isPermitted(any(), any())).thenReturn(true);
when(nodePermissioningController.isPermitted(
any(), eq(highestValueNonPermittedPeer.getEnodeURL())))
.thenReturn(false);
doReturn(false)
.when(peerPermissions)
.isPermitted(
eq(localNode),
eq(highestValueNonPermittedPeer),
eq(Action.RLPX_ALLOW_NEW_OUTBOUND_CONNECTION));
doReturn(peers.stream()).when(network).streamDiscoveredPeers();
final ArgumentCaptor<DiscoveryPeer> peerCapture = ArgumentCaptor.forClass(DiscoveryPeer.class);
@ -609,6 +749,29 @@ public final class DefaultP2PNetworkTest {
assertThat(peer.getLastAttemptedConnection()).isGreaterThan(0);
}
@Test
public void connect_toDisallowedPeer() {
final DefaultP2PNetwork network = network();
network.start();
final Peer localNode = DefaultPeer.fromEnodeURL(network.getLocalEnode().get());
final DiscoveryPeer peer = createDiscoveryPeer();
// Setup permissions to deny peer
doReturn(false)
.when(peerPermissions)
.isPermitted(eq(localNode), eq(peer), eq(Action.RLPX_ALLOW_NEW_OUTBOUND_CONNECTION));
assertThat(peer.getLastAttemptedConnection()).isEqualTo(0);
final CompletableFuture<PeerConnection> result = network.connect(peer);
assertThat(result).isCompletedExceptionally();
assertThatThrownBy(result::get)
.hasCauseInstanceOf(IllegalStateException.class)
.hasMessageContaining("Unable to connect to disallowed peer");
// Last contacted should not be updated.
assertThat(peer.getLastAttemptedConnection()).isEqualTo(0);
}
private DiscoveryPeer createDiscoveryPeer() {
return createDiscoveryPeer(Peer.randomId(), 999);
}
@ -663,6 +826,10 @@ public final class DefaultP2PNetworkTest {
}
private DefaultP2PNetwork network(final Supplier<RlpxConfiguration> rlpxConfig) {
return (DefaultP2PNetwork) builder(rlpxConfig).build();
}
private DefaultP2PNetwork.Builder builder(final Supplier<RlpxConfiguration> rlpxConfig) {
final DiscoveryConfiguration noDiscovery = DiscoveryConfiguration.create().setActive(false);
final NetworkingConfiguration networkingConfiguration =
NetworkingConfiguration.create()
@ -670,23 +837,16 @@ public final class DefaultP2PNetworkTest {
.setSupportedProtocols(subProtocol())
.setRlpx(rlpxConfig.get().setBindPort(0));
lenient().when(nodePermissioningController.isPermitted(any(), any())).thenReturn(true);
return (DefaultP2PNetwork)
builder()
.config(networkingConfiguration)
.nodePermissioningController(nodePermissioningController)
.blockchain(blockchain)
.build();
}
private DefaultP2PNetwork.Builder builder() {
return DefaultP2PNetwork.builder()
.vertx(vertx)
.config(config)
.keyPair(KeyPair.generate())
.metricsSystem(new NoOpMetricsSystem())
.supportedCapabilities(Arrays.asList(Capability.create("eth", 63)));
.supportedCapabilities(Arrays.asList(Capability.create("eth", 63)))
.config(networkingConfiguration)
.nodePermissioningController(nodePermissioningController)
.peerPermissions(peerPermissions)
.blockchain(blockchain);
}
private Peer mockPeer() {
@ -755,4 +915,17 @@ public final class DefaultP2PNetworkTest {
}
};
}
private static class TestPeerPermissions extends PeerPermissions {
@Override
public boolean isPermitted(final Peer localNode, final Peer remotePeer, final Action action) {
return true;
}
public void testDispatchUpdate(
final boolean permissionsRestricted, final Optional<List<Peer>> affectedPeers) {
this.dispatchUpdate(permissionsRestricted, affectedPeers);
}
}
}

@ -0,0 +1,370 @@
/*
* Copyright 2019 ConsenSys AG.
*
* 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.
*/
package tech.pegasys.pantheon.ethereum.p2p.network;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import tech.pegasys.pantheon.ethereum.chain.Blockchain;
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions.Action;
import tech.pegasys.pantheon.ethereum.permissioning.node.NodePermissioningController;
import tech.pegasys.pantheon.ethereum.permissioning.node.provider.SyncStatusNodePermissioningProvider;
import tech.pegasys.pantheon.util.enode.EnodeURL;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.junit.Test;
public class NodePermissioningAdapterTest {
private final Peer localNode = createPeer();
private final Peer remoteNode = createPeer();
private final NodePermissioningController nodePermissioningController =
mock(NodePermissioningController.class);
private final Blockchain blockchain = mock(Blockchain.class);
private final List<EnodeURL> bootNodes = new ArrayList<>();
private final NodePermissioningAdapter adapter =
new NodePermissioningAdapter(nodePermissioningController, bootNodes, blockchain);
@Test
public void allowInPeerTable() {
final Action action = Action.DISCOVERY_ALLOW_IN_PEER_TABLE;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
}
@Test
public void allowOutboundBonding_inSyncRemoteIsBootnode() {
mockSyncStatusNodePermissioning(true, true);
bootNodes.add(remoteNode.getEnodeURL());
final Action action = Action.DISCOVERY_ALLOW_OUTBOUND_BONDING;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowOutboundBonding_inSyncRemoteIsNotABootnode() {
mockSyncStatusNodePermissioning(true, true);
final Action action = Action.DISCOVERY_ALLOW_OUTBOUND_BONDING;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowOutboundBonding_outOfSyncRemoteIsNotABootnode() {
mockSyncStatusNodePermissioning(true, false);
final Action action = Action.DISCOVERY_ALLOW_OUTBOUND_BONDING;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowOutboundBonding_outOfSyncRemoteIsABootnode() {
mockSyncStatusNodePermissioning(true, false);
bootNodes.add(remoteNode.getEnodeURL());
final Action action = Action.DISCOVERY_ALLOW_OUTBOUND_BONDING;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowOutboundBonding_noSyncPermissioning() {
mockSyncStatusNodePermissioning(false, false);
final Action action = Action.DISCOVERY_ALLOW_OUTBOUND_BONDING;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowInboundBonding() {
final Action action = Action.DISCOVERY_ACCEPT_INBOUND_BONDING;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowOutboundNeighborsRequest_inSyncRemoteIsBootnode() {
mockSyncStatusNodePermissioning(true, true);
bootNodes.add(remoteNode.getEnodeURL());
final Action action = Action.DISCOVERY_ALLOW_OUTBOUND_NEIGHBORS_REQUEST;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowOutboundNeighborsRequest_inSyncRemoteIsNotABootnode() {
mockSyncStatusNodePermissioning(true, true);
final Action action = Action.DISCOVERY_ALLOW_OUTBOUND_NEIGHBORS_REQUEST;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowOutboundNeighborsRequest_outOfSyncRemoteIsABootnode() {
mockSyncStatusNodePermissioning(true, false);
bootNodes.add(remoteNode.getEnodeURL());
final Action action = Action.DISCOVERY_ALLOW_OUTBOUND_NEIGHBORS_REQUEST;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowOutboundNeighborsRequest_outOfSyncRemoteIsNotABootnode() {
mockSyncStatusNodePermissioning(true, false);
final Action action = Action.DISCOVERY_ALLOW_OUTBOUND_NEIGHBORS_REQUEST;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowOutboundNeighborsRequest_noSyncPermissioning() {
mockSyncStatusNodePermissioning(false, false);
final Action action = Action.DISCOVERY_ALLOW_OUTBOUND_NEIGHBORS_REQUEST;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowInboundNeighborsRequest() {
final Action action = Action.DISCOVERY_SERVE_INBOUND_NEIGHBORS_REQUEST;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowLocallyInitiatedConnection() {
final Action action = Action.RLPX_ALLOW_NEW_OUTBOUND_CONNECTION;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowRemotelyInitiatedConnection() {
final Action action = Action.RLPX_ALLOW_NEW_INBOUND_CONNECTION;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
@Test
public void allowOngoingConnection() {
final Action action = Action.RLPX_ALLOW_ONGOING_CONNECTION;
mockControllerPermissions(true, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
mockControllerPermissions(true, true);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isTrue();
mockControllerPermissions(false, false);
assertThat(adapter.isPermitted(localNode, remoteNode, action)).isFalse();
}
private void mockSyncStatusNodePermissioning(final boolean isPresent, final boolean isInSync) {
if (!isPresent) {
when(nodePermissioningController.getSyncStatusNodePermissioningProvider())
.thenReturn(Optional.empty());
return;
}
final SyncStatusNodePermissioningProvider syncStatus =
mock(SyncStatusNodePermissioningProvider.class);
when(syncStatus.hasReachedSync()).thenReturn(isInSync);
when(nodePermissioningController.getSyncStatusNodePermissioningProvider())
.thenReturn(Optional.of(syncStatus));
}
private void mockControllerPermissions(
final boolean allowLocalToRemote, final boolean allowRemoteToLocal) {
when(nodePermissioningController.isPermitted(
eq(localNode.getEnodeURL()), eq(remoteNode.getEnodeURL())))
.thenReturn(allowLocalToRemote);
when(nodePermissioningController.isPermitted(
eq(remoteNode.getEnodeURL()), eq(localNode.getEnodeURL())))
.thenReturn(allowRemoteToLocal);
}
private static Peer createPeer() {
return DefaultPeer.fromEnodeURL(createEnode());
}
private static EnodeURL createEnode() {
return EnodeURL.builder()
.ipAddress("127.0.0.1")
.useDefaultPorts()
.nodeId(Peer.randomId())
.build();
}
}

@ -14,6 +14,9 @@ package tech.pegasys.pantheon.ethereum.p2p.network;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import tech.pegasys.pantheon.crypto.SECP256K1;
@ -28,6 +31,8 @@ import tech.pegasys.pantheon.ethereum.p2p.config.RlpxConfiguration;
import tech.pegasys.pantheon.ethereum.p2p.network.exceptions.IncompatiblePeerException;
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions.Action;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissionsBlacklist;
import tech.pegasys.pantheon.ethereum.p2p.wire.Capability;
import tech.pegasys.pantheon.ethereum.p2p.wire.SubProtocol;
@ -322,6 +327,49 @@ public class P2PNetworkTest {
}
}
@Test
public void rejectIncomingConnectionFromDisallowedPeer() throws Exception {
final PeerPermissions peerPermissions = mock(PeerPermissions.class);
when(peerPermissions.isPermitted(any(), any(), any())).thenReturn(true);
try (final P2PNetwork localNetwork =
builder().peerPermissions(peerPermissions).blockchain(blockchain).build();
final P2PNetwork remoteNetwork = builder().build()) {
localNetwork.start();
remoteNetwork.start();
final EnodeURL localEnode = localNetwork.getLocalEnode().get();
final Peer localPeer = DefaultPeer.fromEnodeURL(localEnode);
final Peer remotePeer = DefaultPeer.fromEnodeURL(remoteNetwork.getLocalEnode().get());
// Deny incoming connection permissions for remotePeer
when(peerPermissions.isPermitted(
eq(localPeer), eq(remotePeer), eq(Action.RLPX_ALLOW_NEW_INBOUND_CONNECTION)))
.thenReturn(false);
// Setup disconnect listener
final CompletableFuture<PeerConnection> peerFuture = new CompletableFuture<>();
final CompletableFuture<DisconnectReason> reasonFuture = new CompletableFuture<>();
remoteNetwork.subscribeDisconnect(
(peerConnection, reason, initiatedByPeer) -> {
peerFuture.complete(peerConnection);
reasonFuture.complete(reason);
});
// Remote connect to local
final CompletableFuture<PeerConnection> connectFuture = remoteNetwork.connect(localPeer);
// Check connection is made, and then a disconnect is registered at remote
final BytesValue localId = localEnode.getNodeId();
assertThat(connectFuture.get(5L, TimeUnit.SECONDS).getPeerInfo().getNodeId())
.isEqualTo(localId);
assertThat(peerFuture.get(5L, TimeUnit.SECONDS).getPeerInfo().getNodeId()).isEqualTo(localId);
assertThat(reasonFuture.get(5L, TimeUnit.SECONDS))
.isEqualByComparingTo(DisconnectReason.UNKNOWN);
}
}
private Peer createPeer(final BytesValue nodeId, final int listenPort) {
return DefaultPeer.fromEnodeURL(
EnodeURL.builder()

@ -19,6 +19,7 @@ import static org.mockito.Mockito.when;
import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection;
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions.Action;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissionsBlacklist;
import tech.pegasys.pantheon.ethereum.p2p.wire.PeerInfo;
import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason;
@ -28,6 +29,7 @@ import tech.pegasys.pantheon.util.enode.EnodeURL;
import org.junit.Test;
public class PeerReputationManagerTest {
private final Peer localNode = generatePeer();
private final PeerReputationManager peerReputationManager;
private final PeerPermissionsBlacklist blacklist;
@ -40,63 +42,65 @@ public class PeerReputationManagerTest {
public void doesNotBlacklistPeerForNormalDisconnect() {
final PeerConnection peer = generatePeerConnection();
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue();
checkPermissions(blacklist, peer.getPeer(), true);
peerReputationManager.onDisconnect(peer, DisconnectReason.TOO_MANY_PEERS, false);
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue();
checkPermissions(blacklist, peer.getPeer(), true);
}
@Test
public void blacklistPeerForBadBehavior() {
final PeerConnection peer = generatePeerConnection();
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue();
checkPermissions(blacklist, peer.getPeer(), true);
peerReputationManager.onDisconnect(peer, DisconnectReason.BREACH_OF_PROTOCOL, false);
assertThat(blacklist.isPermitted(peer.getPeer())).isFalse();
checkPermissions(blacklist, peer.getPeer(), false);
}
@Test
public void doesNotBlacklistPeerForOurBadBehavior() {
final PeerConnection peer = generatePeerConnection();
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue();
checkPermissions(blacklist, peer.getPeer(), true);
peerReputationManager.onDisconnect(peer, DisconnectReason.BREACH_OF_PROTOCOL, true);
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue();
checkPermissions(blacklist, peer.getPeer(), true);
}
@Test
public void blacklistIncompatiblePeer() {
final PeerConnection peer = generatePeerConnection();
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue();
checkPermissions(blacklist, peer.getPeer(), true);
peerReputationManager.onDisconnect(
peer, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION, false);
assertThat(blacklist.isPermitted(peer.getPeer())).isFalse();
checkPermissions(blacklist, peer.getPeer(), false);
}
@Test
public void blacklistIncompatiblePeerWhoIssuesDisconnect() {
final PeerConnection peer = generatePeerConnection();
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue();
checkPermissions(blacklist, peer.getPeer(), true);
peerReputationManager.onDisconnect(
peer, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION, true);
assertThat(blacklist.isPermitted(peer.getPeer())).isFalse();
checkPermissions(blacklist, peer.getPeer(), false);
}
private void checkPermissions(
final PeerPermissionsBlacklist blacklist,
final Peer remotePeer,
final boolean expectedResult) {
for (Action action : Action.values()) {
assertThat(blacklist.isPermitted(localNode, remotePeer, action)).isEqualTo(expectedResult);
}
}
private PeerConnection generatePeerConnection() {
final BytesValue nodeId = Peer.randomId();
final PeerConnection conn = mock(PeerConnection.class);
final PeerInfo peerInfo = mock(PeerInfo.class);
final Peer peer =
DefaultPeer.fromEnodeURL(
EnodeURL.builder()
.nodeId(Peer.randomId())
.ipAddress("10.9.8.7")
.discoveryPort(65535)
.listeningPort(65534)
.build());
final Peer peer = generatePeer();
when(peerInfo.getNodeId()).thenReturn(nodeId);
when(conn.getPeerInfo()).thenReturn(peerInfo);
@ -104,4 +108,14 @@ public class PeerReputationManagerTest {
return conn;
}
private Peer generatePeer() {
return DefaultPeer.fromEnodeURL(
EnodeURL.builder()
.nodeId(Peer.randomId())
.ipAddress("10.9.8.7")
.discoveryPort(65535)
.listeningPort(65534)
.build());
}
}

@ -16,6 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions.Action;
import tech.pegasys.pantheon.util.enode.EnodeURL;
import java.util.Collections;
@ -28,6 +29,8 @@ import org.junit.Test;
public class PeerPermissionsBlacklistTest {
private final Peer localNode = createPeer();
@Test
public void add_peer() {
PeerPermissionsBlacklist blacklist = PeerPermissionsBlacklist.create();
@ -111,13 +114,13 @@ public class PeerPermissionsBlacklistTest {
PeerPermissionsBlacklist blacklist = PeerPermissionsBlacklist.create();
Peer peer = createPeer();
assertThat(blacklist.isPermitted(peer)).isTrue();
checkPermissions(blacklist, peer, true);
blacklist.add(peer);
assertThat(blacklist.isPermitted(peer)).isFalse();
checkPermissions(blacklist, peer, false);
blacklist.remove(peer);
assertThat(blacklist.isPermitted(peer)).isTrue();
checkPermissions(blacklist, peer, true);
}
@Test
@ -135,7 +138,7 @@ public class PeerPermissionsBlacklistTest {
}
});
assertThat(blacklist.isPermitted(peer)).isTrue();
checkPermissions(blacklist, peer, true);
assertThat(callbackCount).hasValue(0);
assertThat(restrictedCallbackCount).hasValue(0);
@ -168,28 +171,37 @@ public class PeerPermissionsBlacklistTest {
Peer peerC = createPeer();
// All peers are initially permitted
assertThat(blacklist.isPermitted(peerA)).isTrue();
assertThat(blacklist.isPermitted(peerB)).isTrue();
assertThat(blacklist.isPermitted(peerC)).isTrue();
checkPermissions(blacklist, peerA, true);
checkPermissions(blacklist, peerB, true);
checkPermissions(blacklist, peerC, true);
// Add peerA
blacklist.add(peerA);
assertThat(blacklist.isPermitted(peerA)).isFalse();
assertThat(blacklist.isPermitted(peerB)).isTrue();
assertThat(blacklist.isPermitted(peerC)).isTrue();
checkPermissions(blacklist, peerA, false);
checkPermissions(blacklist, peerB, true);
checkPermissions(blacklist, peerC, true);
// Add peerB
blacklist.add(peerB);
assertThat(blacklist.isPermitted(peerA)).isFalse();
assertThat(blacklist.isPermitted(peerB)).isFalse();
assertThat(blacklist.isPermitted(peerC)).isTrue();
checkPermissions(blacklist, peerA, false);
checkPermissions(blacklist, peerB, false);
checkPermissions(blacklist, peerC, true);
// Add peerC
// Limit is exceeded and peerA should drop off of the list and be allowed
blacklist.add(peerC);
assertThat(blacklist.isPermitted(peerA)).isTrue();
assertThat(blacklist.isPermitted(peerB)).isFalse();
assertThat(blacklist.isPermitted(peerC)).isFalse();
checkPermissions(blacklist, peerA, true);
checkPermissions(blacklist, peerB, false);
checkPermissions(blacklist, peerC, false);
}
private void checkPermissions(
final PeerPermissionsBlacklist blacklist,
final Peer remotePeer,
final boolean expectedResult) {
for (Action action : Action.values()) {
assertThat(blacklist.isPermitted(localNode, remotePeer, action)).isEqualTo(expectedResult);
}
}
@Test
@ -199,12 +211,12 @@ public class PeerPermissionsBlacklistTest {
final List<Peer> peers =
Stream.generate(this::createPeer).limit(peerCount).collect(Collectors.toList());
peers.forEach(p -> assertThat(blacklist.isPermitted(p)).isTrue());
peers.forEach(p -> checkPermissions(blacklist, p, true));
peers.forEach(blacklist::add);
peers.forEach(p -> assertThat(blacklist.isPermitted(p)).isFalse());
peers.forEach(p -> checkPermissions(blacklist, p, false));
peers.forEach(blacklist::remove);
peers.forEach(p -> assertThat(blacklist.isPermitted(p)).isTrue());
peers.forEach(p -> checkPermissions(blacklist, p, true));
}
private Peer createPeer() {

@ -13,9 +13,14 @@
package tech.pegasys.pantheon.ethereum.p2p.permissions;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissions.Action;
import tech.pegasys.pantheon.util.enode.EnodeURL;
import java.util.Optional;
@ -24,6 +29,8 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
public class PeerPermissionsTest {
final Peer localPeer = createPeer();
@Test
public void subscribeUpdate() {
TestPeerPermissions peerPermissions = new TestPeerPermissions(false);
@ -75,57 +82,103 @@ public class PeerPermissionsTest {
@Test
public void isPermitted_forCombinedPermissions() {
final PeerPermissions allowPeers = new TestPeerPermissions(true);
final PeerPermissions disallowPeers = new TestPeerPermissions(false);
final PeerPermissions allowAll = new TestPeerPermissions(true);
final PeerPermissions disallowAll = new TestPeerPermissions(false);
final PeerPermissions noop = PeerPermissions.NOOP;
final PeerPermissions combinedPermissive = PeerPermissions.combine(noop, allowPeers);
final PeerPermissions combinedRestrictive = PeerPermissions.combine(disallowPeers, allowPeers);
Peer peer =
DefaultPeer.fromEnodeURL(
EnodeURL.builder()
.listeningPort(30303)
.discoveryPort(30303)
.nodeId(Peer.randomId())
.ipAddress("127.0.0.1")
.build());
assertThat(PeerPermissions.combine(allowPeers, disallowPeers).isPermitted(peer)).isFalse();
assertThat(PeerPermissions.combine(disallowPeers, disallowPeers).isPermitted(peer)).isFalse();
assertThat(PeerPermissions.combine(disallowPeers, disallowPeers).isPermitted(peer)).isFalse();
assertThat(PeerPermissions.combine(allowPeers, disallowPeers).isPermitted(peer)).isFalse();
assertThat(PeerPermissions.combine(allowPeers, allowPeers).isPermitted(peer)).isTrue();
assertThat(PeerPermissions.combine(combinedPermissive, allowPeers).isPermitted(peer)).isTrue();
assertThat(PeerPermissions.combine(combinedPermissive, disallowPeers).isPermitted(peer))
final PeerPermissions combinedPermissive = PeerPermissions.combine(noop, allowAll);
final PeerPermissions combinedRestrictive = PeerPermissions.combine(disallowAll, allowAll);
Peer remotePeer = createPeer();
Action action = Action.RLPX_ALLOW_NEW_OUTBOUND_CONNECTION;
assertThat(
PeerPermissions.combine(allowAll, disallowAll)
.isPermitted(localPeer, remotePeer, action))
.isFalse();
assertThat(
PeerPermissions.combine(disallowAll, disallowAll)
.isPermitted(localPeer, remotePeer, action))
.isFalse();
assertThat(
PeerPermissions.combine(disallowAll, disallowAll)
.isPermitted(localPeer, remotePeer, action))
.isFalse();
assertThat(PeerPermissions.combine(combinedRestrictive, allowPeers).isPermitted(peer))
assertThat(
PeerPermissions.combine(allowAll, disallowAll)
.isPermitted(localPeer, remotePeer, action))
.isFalse();
assertThat(PeerPermissions.combine(combinedRestrictive, disallowPeers).isPermitted(peer))
assertThat(
PeerPermissions.combine(allowAll, allowAll).isPermitted(localPeer, remotePeer, action))
.isTrue();
assertThat(
PeerPermissions.combine(combinedPermissive, allowAll)
.isPermitted(localPeer, remotePeer, action))
.isTrue();
assertThat(
PeerPermissions.combine(combinedPermissive, disallowAll)
.isPermitted(localPeer, remotePeer, action))
.isFalse();
assertThat(PeerPermissions.combine(combinedRestrictive).isPermitted(peer)).isFalse();
assertThat(PeerPermissions.combine(combinedPermissive).isPermitted(peer)).isTrue();
assertThat(
PeerPermissions.combine(combinedRestrictive, allowAll)
.isPermitted(localPeer, remotePeer, action))
.isFalse();
assertThat(
PeerPermissions.combine(combinedRestrictive, disallowAll)
.isPermitted(localPeer, remotePeer, action))
.isFalse();
assertThat(
PeerPermissions.combine(combinedRestrictive).isPermitted(localPeer, remotePeer, action))
.isFalse();
assertThat(
PeerPermissions.combine(combinedPermissive).isPermitted(localPeer, remotePeer, action))
.isTrue();
assertThat(PeerPermissions.combine(noop).isPermitted(localPeer, remotePeer, action)).isTrue();
assertThat(PeerPermissions.combine().isPermitted(localPeer, remotePeer, action)).isTrue();
}
@Test
public void close_forCombinedPermissions() {
TestPeerPermissions peerPermissionsA = spy(new TestPeerPermissions(false));
TestPeerPermissions peerPermissionsB = spy(new TestPeerPermissions(false));
PeerPermissions combined = PeerPermissions.combine(peerPermissionsA, peerPermissionsB);
verify(peerPermissionsA, never()).close();
verify(peerPermissionsB, never()).close();
combined.close();
verify(peerPermissionsA, times(1)).close();
verify(peerPermissionsB, times(1)).close();
}
assertThat(PeerPermissions.combine(noop).isPermitted(peer)).isTrue();
assertThat(PeerPermissions.combine().isPermitted(peer)).isTrue();
private Peer createPeer() {
return DefaultPeer.fromEnodeURL(
EnodeURL.builder()
.listeningPort(30303)
.discoveryPort(30303)
.nodeId(Peer.randomId())
.ipAddress("127.0.0.1")
.build());
}
private static class TestPeerPermissions extends PeerPermissions {
private boolean allowPeers;
private boolean allowAll;
public TestPeerPermissions(final boolean allowPeers) {
this.allowPeers = allowPeers;
public TestPeerPermissions(final boolean allowAll) {
this.allowAll = allowAll;
}
public void allowPeers(final boolean doAllowPeers) {
this.allowPeers = doAllowPeers;
this.allowAll = doAllowPeers;
dispatchUpdate(!doAllowPeers, Optional.empty());
}
@Override
public boolean isPermitted(final Peer peer) {
return allowPeers;
public boolean isPermitted(final Peer localNode, final Peer remotePeer, final Action action) {
return allowAll;
}
}
}

@ -70,14 +70,6 @@ public class NodePermissioningController {
return true;
}
public void startPeerDiscoveryCallback(final Runnable peerDiscoveryCallback) {
if (syncStatusNodePermissioningProvider.isPresent()) {
syncStatusNodePermissioningProvider.get().setHasReachedSyncCallback(peerDiscoveryCallback);
} else {
peerDiscoveryCallback.run();
}
}
public void setInsufficientPeersPermissioningProvider(
final ContextualNodePermissioningProvider insufficientPeersPermissioningProvider) {
insufficientPeersPermissioningProvider.subscribeToUpdates(

@ -21,7 +21,6 @@ import tech.pegasys.pantheon.util.enode.EnodeURL;
import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.OptionalLong;
public class SyncStatusNodePermissioningProvider implements NodePermissioningProvider {
@ -30,7 +29,6 @@ public class SyncStatusNodePermissioningProvider implements NodePermissioningPro
private final Collection<EnodeURL> fixedNodes = new HashSet<>();
private OptionalLong syncStatusObserverId;
private boolean hasReachedSync = false;
private Optional<Runnable> hasReachedSyncCallback = Optional.empty();
public SyncStatusNodePermissioningProvider(
final Synchronizer synchronizer, final Collection<EnodeURL> fixedNodes) {
@ -47,7 +45,6 @@ public class SyncStatusNodePermissioningProvider implements NodePermissioningPro
if (blocksBehind <= 0) {
synchronized (this) {
if (!hasReachedSync) {
runCallback();
syncStatusObserverId.ifPresent(
id -> {
synchronizer.removeObserver(id);
@ -60,19 +57,6 @@ public class SyncStatusNodePermissioningProvider implements NodePermissioningPro
}
}
public synchronized void setHasReachedSyncCallback(final Runnable runnable) {
if (hasReachedSync) {
runCallback();
} else {
this.hasReachedSyncCallback = Optional.of(runnable);
}
}
private synchronized void runCallback() {
hasReachedSyncCallback.ifPresent(Runnable::run);
hasReachedSyncCallback = Optional.empty();
}
/**
* Before reaching a sync'd state, the node will only be allowed to talk to its fixedNodes
* (outgoing connections). After reaching a sync'd state, it is expected that other providers will

@ -26,7 +26,6 @@ import tech.pegasys.pantheon.ethereum.permissioning.node.provider.SyncStatusNode
import tech.pegasys.pantheon.util.enode.EnodeURL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@ -69,23 +68,6 @@ public class NodePermissioningControllerTest {
verify(syncStatusNodePermissioningProvider, atLeast(1)).isPermitted(eq(enode1), eq(enode2));
}
@Test
public void peerDiscoveryCallbackShouldBeDelegatedToSyncStatusNodePermissioningProvider() {
controller.startPeerDiscoveryCallback(() -> {});
verify(syncStatusNodePermissioningProvider).setHasReachedSyncCallback(any(Runnable.class));
}
@Test
public void peerDiscoveryCallbackShouldRunWhenSyncStatusProviderDoesNotExist() {
final Runnable callback = mock(Runnable.class);
controller = new NodePermissioningController(Optional.empty(), Collections.emptyList());
controller.startPeerDiscoveryCallback(callback);
verify(callback).run();
}
@Test
public void whenNoSyncStatusProviderWeShouldDelegateToLocalConfigNodePermissioningProvider() {
List<NodePermissioningProvider> providers = new ArrayList<>();

@ -14,10 +14,7 @@ package tech.pegasys.pantheon.ethereum.permissioning.node.provider;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import tech.pegasys.pantheon.ethereum.core.SyncStatus;
@ -93,28 +90,6 @@ public class SyncStatusNodePermissioningProviderTest {
assertThat(provider.hasReachedSync()).isTrue();
}
@Test
public void whenNotInSyncShouldNotExecuteCallback() {
final Runnable callbackFunction = mock(Runnable.class);
provider.setHasReachedSyncCallback(callbackFunction);
syncStatusListener.onSyncStatus(new SyncStatus(0, 1, 2));
verifyZeroInteractions(callbackFunction);
}
@Test
public void whenInSyncShouldExecuteCallback() {
final Runnable callbackFunction = mock(Runnable.class);
provider.setHasReachedSyncCallback(callbackFunction);
syncStatusListener.onSyncStatus(new SyncStatus(0, 1, 1));
verify(callbackFunction).run();
// after executing callback, it should unsubscribe from the SyncStatus updates
verify(synchronizer).removeObserver(eq(syncStatusObserverId));
}
@Test
public void whenHasNotSyncedNonBootnodeShouldNotBePermitted() {
syncStatusListener.onSyncStatus(new SyncStatus(0, 1, 2));

Loading…
Cancel
Save