Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

[PAN-2614] Add simple PeerPermissions interface #1446

Merged

Conversation

mbaxter
Copy link
Contributor

@mbaxter mbaxter commented May 14, 2019

PR description

This PR adds a simple PeerPermissions interface as a first step in trying to define a common interface for permission-related logic within the p2p package.

Summary of changes:

  • Create a basic PeerPermissions interface
  • Break up PeerBlacklist into several smaller classes and put a subset of these classes behind the new interface
    • Create a PeerPermissionsBlacklist class that just holds a set of peers that are not allowed. The list can be optionally capped with a max number of tracked peers.
    • Create a LimitedSet class to handle creating a capped set
    • Move logic for blacklisting peers based on disconnect into a PeerReputationManager class
    • Separate out lists of nodes that are banned because of bad behavior vs banned via the CLI
    • Delete a bunch of duplicate tests in the p2p test suite that were validating logic that now belongs in PeerReputationManager
  • Validate that arguments to --banned-node-ids are valid node ids
  • Don't indiscriminately drop peers from our peer table for any disconnect
    • Instead, drop peers when they are not allowed
    • Since we're no longer dropping peers based on any disconnect, when pulling discovery peers to connect to, sort them so that we start with the least recently contacted peers first
  • Delete dead code related to tracking dropped peer events
  • Add a PeerDiscoveryController Builder to make updates to the long list of constructor dependencies easier
  • Validate peers before initiating outbound connections

@@ -108,7 +107,6 @@ public void startNode(final PantheonNode node) {
.jsonRpcConfiguration(node.jsonRpcConfiguration())
.webSocketConfiguration(node.webSocketConfiguration())
.dataDir(node.homeDirectory())
.bannedNodeIds(Collections.emptySet())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer need to configure an empty set of banned nodes

*/
public long observePeerDroppedEvents(final Consumer<PeerDroppedEvent> observer) {
checkNotNull(observer);
return peerDroppedObservers.subscribe(observer);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was dead code - listeners were never actually notified of dropped peers

@@ -80,7 +72,6 @@
/* The keypair used to sign messages. */
protected final SECP256K1.KeyPair keyPair;
private final BytesValue id;
private final PeerTable peerTable;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PeerTable isn't used in PeerDiscoveryAgent - moved it into the PeerDiscoveryController where it is used.

@@ -367,27 +382,11 @@ private boolean addToPeerTable(final DiscoveryPeer peer) {
return true;
}

@VisibleForTesting
boolean dropFromPeerTable(final DiscoveryPeer peer) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code

@@ -348,6 +349,7 @@ protected void initChannel(final SocketChannel ch) {

if (!isPeerAllowed(connection)) {
connection.disconnect(DisconnectReason.UNKNOWN);
peerDiscoveryAgent.dropPeer(connection.getPeer());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously we were dropping all peers who disconnected. Now, we're explicitly dropping a subset of disconnected peers.

@@ -407,8 +411,9 @@ void attemptPeerConnections() {
streamDiscoveredPeers()
.filter(peer -> peer.getStatus() == PeerDiscoveryStatus.BONDED)
.filter(peer -> !isConnected(peer) && !isConnecting(peer))
.sorted(Comparator.comparing(DiscoveryPeer::getLastAttemptedConnection))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try connecting to discovery peers who haven't recently been contacted.


doReturn(EvictResult.absent()).when(peerTableSpy).tryEvict(any());

controller = getControllerBuilder().peerDroppedObservers(peerDroppedSubscribers).build();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test hit the dead code that was removed

@mbaxter mbaxter marked this pull request as ready for review May 14, 2019 22:28
protected static final Logger LOG = LogManager.getLogger();

// The devp2p specification says only accept packets up to 1280, but some
// clients ignore that, so we add in a little extra padding.
private static final int MAX_PACKET_SIZE_BYTES = 1600;
private static final long PEER_REFRESH_INTERVAL_MS = MILLISECONDS.convert(30, TimeUnit.MINUTES);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this constant into PeerDiscoveryController where it is used

Copy link
Contributor

@shemnon shemnon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Would some of the pre-review comments be better served as code comments?
  • all "consider" comments are truly optional.

}

// If we have an explicit list of peers, drop each peer from our discovery table
affectedPeers.ifPresent(peers -> peers.forEach(this::dropPeer));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they are affected why are we summarily dropping them instead of re-evaluating? I'm confused.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current interface just has a blanket rule where peers are fully allowed or fully disallowed. So if the permissions were updated to add restrictions and a list of peers were provided, it means those peers are no longer allowed. The follow-up PR will make these checks more granular such that we'll re-evaluate at this point.

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);
final boolean peerKnown = maybeKnownPeer.isPresent();
final boolean peerBlacklisted = peerBlacklist.contains(peer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the blacklist peers that are "bad" will still hit our peer table. Is this acceptable per what we need to do? Will we advertise that we've seen "blackbeard" even if we refuse to talk to him? Should we be tracking peer-no-grata? Or are we checking permissions at every time we would pull said peer out of the peer table?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The blacklist basically transformed into peerPermissions which gets checked in the method isPeerPermittedToSendMessages above on line 307. So we're still filtering out the bad peers here.


@FunctionalInterface
public interface OutboundDiscoveryMessagingPermissions {
boolean isPermitted(Peer remotePeer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe isRemotePermitted? In case we add this interface to an uber-permissions object with inbound permissions implicit on the local peer, so the names won't collide?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got a follow-up PR that will rework these interfaces. Right now I'm moving towards an API like: isPermitted(localNode, remotePeer, action).

@@ -778,7 +761,6 @@ private void validate() {
checkState(
supportedCapabilities != null && supportedCapabilities.size() > 0,
"Supported capabilities must be set and non-empty.");
checkState(peerBlacklist != null, "PeerBlacklist must be set.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we handle null peerPermissions correctly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so - peerPermissions gets initialized with a default value, and the setter does a null check so that it can't get overwritten with a null value.

}

private static class CombinedPeerPermissions extends PeerPermissions {
private final List<PeerPermissions> permissions;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider ImmutableList. Since we are allowing a list to shortcut other CombinedPeerPermissions by collapsing it into this list we should make it explicit that the object we are shortcutting won't mutate. There is an ImmutableList collector as part of Guava.

private PeerPermissionsBlacklist(final int initialCapacity, final OptionalInt maxSize) {
if (maxSize.isPresent()) {
blacklist =
LimitedSet.create(initialCapacity, maxSize.getAsInt(), Mode.DROP_LEAST_RECENTLY_ACCESSED);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider Guava's Cache instead: https://github.com/google/guava/wiki/CachesExplained - it doesn't do Least Recently Accessed 100% but it does have better concurrency then being wrapped in a synchronized set.

Under the covers Java's sets are Maps with the key being the entry and I can't remember what the value is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may follow-up on this suggestion in another PR. But skipping for now.

}
}

private static void validateNodeIdString(final String nodeId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this be part of ENodeURL?


assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}

@Test
public void callingWithBannedNodeidsOptionButNoValueMustDisplayErrorAndUsage() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these be moved to 986? It makes the diff more comprehensible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bannedNodeIds test was sandwiched in between some bootnode tests, so moved things around a bit for a more coherent test layout 🤷‍♀

@mbaxter mbaxter merged commit c227d1b into PegaSysEng:master May 17, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants