|
|
@ -13,9 +13,9 @@ |
|
|
|
package tech.pegasys.pantheon.ethereum.p2p.discovery; |
|
|
|
package tech.pegasys.pantheon.ethereum.p2p.discovery; |
|
|
|
|
|
|
|
|
|
|
|
import static com.google.common.base.Preconditions.checkArgument; |
|
|
|
import static com.google.common.base.Preconditions.checkArgument; |
|
|
|
|
|
|
|
import static com.google.common.base.Preconditions.checkNotNull; |
|
|
|
import static com.google.common.base.Preconditions.checkState; |
|
|
|
import static com.google.common.base.Preconditions.checkState; |
|
|
|
import static java.util.concurrent.TimeUnit.MILLISECONDS; |
|
|
|
import static java.util.concurrent.TimeUnit.MILLISECONDS; |
|
|
|
import static tech.pegasys.pantheon.util.Preconditions.checkGuard; |
|
|
|
|
|
|
|
import static tech.pegasys.pantheon.util.bytes.BytesValue.wrapBuffer; |
|
|
|
import static tech.pegasys.pantheon.util.bytes.BytesValue.wrapBuffer; |
|
|
|
|
|
|
|
|
|
|
|
import tech.pegasys.pantheon.crypto.SECP256K1; |
|
|
|
import tech.pegasys.pantheon.crypto.SECP256K1; |
|
|
@ -23,28 +23,26 @@ import tech.pegasys.pantheon.ethereum.p2p.api.DisconnectCallback; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryEvent.PeerBondedEvent; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryEvent.PeerBondedEvent; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryEvent.PeerDroppedEvent; |
|
|
|
|
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.Packet; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.Packet; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PacketData; |
|
|
|
|
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PacketType; |
|
|
|
|
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PeerDiscoveryController; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PeerDiscoveryController; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PeerRequirement; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PeerRequirement; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PeerTable; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PeerTable; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PingPacketData; |
|
|
|
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.DefaultPeerId; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeerId; |
|
|
|
|
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.peers.Endpoint; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.peers.PeerBlacklist; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.peers.PeerBlacklist; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage; |
|
|
|
import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage; |
|
|
|
import tech.pegasys.pantheon.util.NetworkUtility; |
|
|
|
import tech.pegasys.pantheon.util.NetworkUtility; |
|
|
|
|
|
|
|
import tech.pegasys.pantheon.util.Subscribers; |
|
|
|
import tech.pegasys.pantheon.util.bytes.BytesValue; |
|
|
|
import tech.pegasys.pantheon.util.bytes.BytesValue; |
|
|
|
|
|
|
|
|
|
|
|
import java.io.IOException; |
|
|
|
|
|
|
|
import java.net.BindException; |
|
|
|
|
|
|
|
import java.net.InetSocketAddress; |
|
|
|
import java.net.InetSocketAddress; |
|
|
|
import java.net.SocketException; |
|
|
|
|
|
|
|
import java.util.Collection; |
|
|
|
import java.util.Collection; |
|
|
|
import java.util.Collections; |
|
|
|
import java.util.Collections; |
|
|
|
import java.util.List; |
|
|
|
import java.util.List; |
|
|
|
|
|
|
|
import java.util.Optional; |
|
|
|
import java.util.OptionalInt; |
|
|
|
import java.util.OptionalInt; |
|
|
|
import java.util.concurrent.CompletableFuture; |
|
|
|
import java.util.concurrent.CompletableFuture; |
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
@ -53,282 +51,201 @@ import java.util.stream.Collectors; |
|
|
|
|
|
|
|
|
|
|
|
import com.google.common.annotations.VisibleForTesting; |
|
|
|
import com.google.common.annotations.VisibleForTesting; |
|
|
|
import com.google.common.net.InetAddresses; |
|
|
|
import com.google.common.net.InetAddresses; |
|
|
|
import io.vertx.core.Vertx; |
|
|
|
|
|
|
|
import io.vertx.core.datagram.DatagramPacket; |
|
|
|
|
|
|
|
import io.vertx.core.datagram.DatagramSocket; |
|
|
|
|
|
|
|
import io.vertx.core.datagram.DatagramSocketOptions; |
|
|
|
|
|
|
|
import org.apache.logging.log4j.LogManager; |
|
|
|
import org.apache.logging.log4j.LogManager; |
|
|
|
import org.apache.logging.log4j.Logger; |
|
|
|
import org.apache.logging.log4j.Logger; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* The peer discovery agent is the network component that sends and receives messages peer discovery |
|
|
|
* The peer discovery agent is the network component that sends and receives peer discovery messages |
|
|
|
* messages via UDP. It exposes methods for the {@link PeerDiscoveryController} to dispatch outbound |
|
|
|
* via UDP. |
|
|
|
* messages too. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <h3>How do the peer table and the discovery agent interact with one another?</h3> |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <ul> |
|
|
|
|
|
|
|
* <li>The agent acts like the transport layer, receiving messages from the wire and exposing |
|
|
|
|
|
|
|
* methods for the peer table to send packets too. |
|
|
|
|
|
|
|
* <li>The table stores and indexes peers in a Kademlia k-bucket table with 256 bins (where bin 0 |
|
|
|
|
|
|
|
* is not used as it's us, i.e. distance 0 == us). It reacts to messages based on its internal |
|
|
|
|
|
|
|
* state. It uses the agent whenever it needs to dispatch a message. |
|
|
|
|
|
|
|
* </ul> |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <h3>The flow</h3> |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <ol> |
|
|
|
|
|
|
|
* <li>The discovery agent dispatches all incoming messages that were properly decoded and whose |
|
|
|
|
|
|
|
* hash integrity check passes to the peer table. |
|
|
|
|
|
|
|
* <li>The peer table decides whether to store the Peer, change its state, send other messages, |
|
|
|
|
|
|
|
* etc. based on its internal state. |
|
|
|
|
|
|
|
* <li>The agent attaches a callback to the call to the Peer Table. When the Peer Table has |
|
|
|
|
|
|
|
* processed the message, it'll perform a callback passing in an Optional which is populated |
|
|
|
|
|
|
|
* if we recognised the Peer, and empty if we did not. |
|
|
|
|
|
|
|
* <li>The agent reacts to specific messages (PING->PONG, FIND_NEIGHBORS->NEIGHBORS), if the |
|
|
|
|
|
|
|
* Peer was recognised. Why doesn't the table send these messages itself? Because they don't |
|
|
|
|
|
|
|
* affect the state machine of the Peer, and the table is only concerned with storing peers, |
|
|
|
|
|
|
|
* keeping them alive and tracking their state. It is not bothered to service requests. |
|
|
|
|
|
|
|
* </ol> |
|
|
|
|
|
|
|
*/ |
|
|
|
*/ |
|
|
|
public class PeerDiscoveryAgent implements DisconnectCallback { |
|
|
|
public abstract class PeerDiscoveryAgent implements DisconnectCallback { |
|
|
|
private static final Logger LOG = LogManager.getLogger(); |
|
|
|
protected static final Logger LOG = LogManager.getLogger(); |
|
|
|
|
|
|
|
|
|
|
|
// The devp2p specification says only accept packets up to 1280, but some
|
|
|
|
// The devp2p specification says only accept packets up to 1280, but some
|
|
|
|
// clients ignore that, so we add in a little extra padding.
|
|
|
|
// clients ignore that, so we add in a little extra padding.
|
|
|
|
private static final int MAX_PACKET_SIZE_BYTES = 1600; |
|
|
|
private static final int MAX_PACKET_SIZE_BYTES = 1600; |
|
|
|
private static final long PEER_REFRESH_INTERVAL_MS = MILLISECONDS.convert(30, TimeUnit.MINUTES); |
|
|
|
private static final long PEER_REFRESH_INTERVAL_MS = MILLISECONDS.convert(30, TimeUnit.MINUTES); |
|
|
|
private final Vertx vertx; |
|
|
|
|
|
|
|
|
|
|
|
protected final List<DiscoveryPeer> bootstrapPeers; |
|
|
|
|
|
|
|
private final PeerRequirement peerRequirement; |
|
|
|
|
|
|
|
private final PeerBlacklist peerBlacklist; |
|
|
|
|
|
|
|
private final NodeWhitelistController nodeWhitelistController; |
|
|
|
/* The peer controller, which takes care of the state machine of peers. */ |
|
|
|
/* The peer controller, which takes care of the state machine of peers. */ |
|
|
|
private final PeerDiscoveryController controller; |
|
|
|
protected Optional<PeerDiscoveryController> controller = Optional.empty(); |
|
|
|
|
|
|
|
|
|
|
|
/* The keypair used to sign messages. */ |
|
|
|
/* The keypair used to sign messages. */ |
|
|
|
private final SECP256K1.KeyPair keyPair; |
|
|
|
protected final SECP256K1.KeyPair keyPair; |
|
|
|
|
|
|
|
private final BytesValue id; |
|
|
|
private final PeerTable peerTable; |
|
|
|
private final PeerTable peerTable; |
|
|
|
private final DiscoveryConfiguration config; |
|
|
|
protected final DiscoveryConfiguration config; |
|
|
|
|
|
|
|
|
|
|
|
/* This is the {@link tech.pegasys.pantheon.ethereum.p2p.Peer} object holding who we are. */ |
|
|
|
/* This is the {@link tech.pegasys.pantheon.ethereum.p2p.Peer} object holding who we are. */ |
|
|
|
private DiscoveryPeer advertisedPeer; |
|
|
|
private DiscoveryPeer advertisedPeer; |
|
|
|
/* The vert.x UDP socket. */ |
|
|
|
private InetSocketAddress localAddress; |
|
|
|
private DatagramSocket socket; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Is discovery enabled? */ |
|
|
|
/* Is discovery enabled? */ |
|
|
|
private boolean isActive = false; |
|
|
|
private boolean isActive = false; |
|
|
|
|
|
|
|
private final Subscribers<Consumer<PeerBondedEvent>> peerBondedObservers = new Subscribers<>(); |
|
|
|
|
|
|
|
|
|
|
|
public PeerDiscoveryAgent( |
|
|
|
public PeerDiscoveryAgent( |
|
|
|
final Vertx vertx, |
|
|
|
|
|
|
|
final SECP256K1.KeyPair keyPair, |
|
|
|
final SECP256K1.KeyPair keyPair, |
|
|
|
final DiscoveryConfiguration config, |
|
|
|
final DiscoveryConfiguration config, |
|
|
|
final PeerRequirement peerRequirement, |
|
|
|
final PeerRequirement peerRequirement, |
|
|
|
final PeerBlacklist peerBlacklist, |
|
|
|
final PeerBlacklist peerBlacklist, |
|
|
|
final NodeWhitelistController nodeWhitelistController) { |
|
|
|
final NodeWhitelistController nodeWhitelistController) { |
|
|
|
checkArgument(vertx != null, "vertx instance cannot be null"); |
|
|
|
|
|
|
|
checkArgument(keyPair != null, "keypair cannot be null"); |
|
|
|
checkArgument(keyPair != null, "keypair cannot be null"); |
|
|
|
checkArgument(config != null, "provided configuration cannot be null"); |
|
|
|
checkArgument(config != null, "provided configuration cannot be null"); |
|
|
|
|
|
|
|
|
|
|
|
validateConfiguration(config); |
|
|
|
validateConfiguration(config); |
|
|
|
|
|
|
|
|
|
|
|
final List<DiscoveryPeer> bootstrapPeers = |
|
|
|
this.peerRequirement = peerRequirement; |
|
|
|
|
|
|
|
this.peerBlacklist = peerBlacklist; |
|
|
|
|
|
|
|
this.nodeWhitelistController = nodeWhitelistController; |
|
|
|
|
|
|
|
this.bootstrapPeers = |
|
|
|
config.getBootstrapPeers().stream().map(DiscoveryPeer::new).collect(Collectors.toList()); |
|
|
|
config.getBootstrapPeers().stream().map(DiscoveryPeer::new).collect(Collectors.toList()); |
|
|
|
|
|
|
|
|
|
|
|
this.vertx = vertx; |
|
|
|
|
|
|
|
this.config = config; |
|
|
|
this.config = config; |
|
|
|
this.keyPair = keyPair; |
|
|
|
this.keyPair = keyPair; |
|
|
|
this.peerTable = new PeerTable(keyPair.getPublicKey().getEncodedBytes(), 16); |
|
|
|
this.peerTable = new PeerTable(keyPair.getPublicKey().getEncodedBytes(), 16); |
|
|
|
this.controller = |
|
|
|
|
|
|
|
new PeerDiscoveryController( |
|
|
|
id = keyPair.getPublicKey().getEncodedBytes(); |
|
|
|
vertx, |
|
|
|
|
|
|
|
this, |
|
|
|
|
|
|
|
peerTable, |
|
|
|
|
|
|
|
bootstrapPeers, |
|
|
|
|
|
|
|
PEER_REFRESH_INTERVAL_MS, |
|
|
|
|
|
|
|
peerRequirement, |
|
|
|
|
|
|
|
peerBlacklist, |
|
|
|
|
|
|
|
nodeWhitelistController); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public CompletableFuture<?> start(final int tcpPort) { |
|
|
|
protected abstract TimerUtil createTimer(); |
|
|
|
final CompletableFuture<?> completion = new CompletableFuture<>(); |
|
|
|
|
|
|
|
|
|
|
|
protected abstract CompletableFuture<InetSocketAddress> listenForConnections(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected abstract CompletableFuture<Void> sendOutgoingPacket( |
|
|
|
|
|
|
|
final DiscoveryPeer peer, final Packet packet); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public abstract CompletableFuture<?> stop(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public CompletableFuture<?> start() { |
|
|
|
|
|
|
|
final CompletableFuture<?> future = new CompletableFuture<>(); |
|
|
|
if (config.isActive()) { |
|
|
|
if (config.isActive()) { |
|
|
|
final String host = config.getBindHost(); |
|
|
|
final String host = config.getBindHost(); |
|
|
|
final int port = config.getBindPort(); |
|
|
|
final int port = config.getBindPort(); |
|
|
|
LOG.info("Starting peer discovery agent on host={}, port={}", host, port); |
|
|
|
LOG.info("Starting peer discovery agent on host={}, port={}", host, port); |
|
|
|
|
|
|
|
|
|
|
|
vertx |
|
|
|
listenForConnections() |
|
|
|
.createDatagramSocket( |
|
|
|
.thenAccept( |
|
|
|
new DatagramSocketOptions().setIpV6(NetworkUtility.isIPv6Available())) |
|
|
|
(InetSocketAddress localAddress) -> { |
|
|
|
.listen( |
|
|
|
// Once listener is set up, finish initializing
|
|
|
|
port, |
|
|
|
this.localAddress = localAddress; |
|
|
|
host, |
|
|
|
advertisedPeer = |
|
|
|
res -> { |
|
|
|
new DiscoveryPeer( |
|
|
|
if (res.failed()) { |
|
|
|
id, |
|
|
|
Throwable cause = res.cause(); |
|
|
|
config.getAdvertisedHost(), |
|
|
|
LOG.error("An exception occurred when starting the peer discovery agent", cause); |
|
|
|
localAddress.getPort(), |
|
|
|
|
|
|
|
localAddress.getPort()); |
|
|
|
if (cause instanceof BindException || cause instanceof SocketException) { |
|
|
|
isActive = true; |
|
|
|
cause = |
|
|
|
startController(); |
|
|
|
new PeerDiscoveryServiceException( |
|
|
|
}) |
|
|
|
String.format( |
|
|
|
.whenComplete( |
|
|
|
"Failed to bind Ethereum UDP discovery listener to %s:%d: %s", |
|
|
|
(res, err) -> { |
|
|
|
host, port, cause.getMessage())); |
|
|
|
// Finalize future
|
|
|
|
} |
|
|
|
if (err != null) { |
|
|
|
completion.completeExceptionally(cause); |
|
|
|
future.completeExceptionally(err); |
|
|
|
return; |
|
|
|
} else { |
|
|
|
|
|
|
|
future.complete(null); |
|
|
|
} |
|
|
|
} |
|
|
|
initialize(res.result(), res.result().localAddress().port()); |
|
|
|
|
|
|
|
this.isActive = true; |
|
|
|
|
|
|
|
completion.complete(null); |
|
|
|
|
|
|
|
}); |
|
|
|
}); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
this.isActive = false; |
|
|
|
this.isActive = false; |
|
|
|
completion.complete(null); |
|
|
|
future.complete(null); |
|
|
|
} |
|
|
|
} |
|
|
|
return completion; |
|
|
|
return future; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public CompletableFuture<?> stop() { |
|
|
|
private void startController() { |
|
|
|
if (socket == null) { |
|
|
|
PeerDiscoveryController controller = createController(); |
|
|
|
return CompletableFuture.completedFuture(null); |
|
|
|
this.controller = Optional.of(controller); |
|
|
|
} |
|
|
|
controller.start(); |
|
|
|
|
|
|
|
|
|
|
|
final CompletableFuture<?> completion = new CompletableFuture<>(); |
|
|
|
|
|
|
|
socket.close( |
|
|
|
|
|
|
|
ar -> { |
|
|
|
|
|
|
|
if (ar.succeeded()) { |
|
|
|
|
|
|
|
controller.stop(); |
|
|
|
|
|
|
|
socket = null; |
|
|
|
|
|
|
|
completion.complete(null); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
completion.completeExceptionally(ar.cause()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
return completion; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void initialize(final DatagramSocket socket, final int tcpPort) { |
|
|
|
private PeerDiscoveryController createController() { |
|
|
|
this.socket = socket; |
|
|
|
return new PeerDiscoveryController( |
|
|
|
|
|
|
|
keyPair, |
|
|
|
// TODO: when using wildcard hosts (0.0.0.0), we need to handle multiple addresses by selecting
|
|
|
|
advertisedPeer, |
|
|
|
// the
|
|
|
|
peerTable, |
|
|
|
// correct 'announce' address.
|
|
|
|
bootstrapPeers, |
|
|
|
final BytesValue id = keyPair.getPublicKey().getEncodedBytes(); |
|
|
|
this::handleOutgoingPacket, |
|
|
|
final String effectiveHost = socket.localAddress().host(); |
|
|
|
createTimer(), |
|
|
|
final int effectivePort = socket.localAddress().port(); |
|
|
|
PEER_REFRESH_INTERVAL_MS, |
|
|
|
advertisedPeer = new DiscoveryPeer(id, config.getAdvertisedHost(), effectivePort, tcpPort); |
|
|
|
peerRequirement, |
|
|
|
|
|
|
|
peerBlacklist, |
|
|
|
LOG.info( |
|
|
|
nodeWhitelistController, |
|
|
|
"Started peer discovery agent successfully, on effective host={} and port={}", |
|
|
|
peerBondedObservers); |
|
|
|
effectiveHost, |
|
|
|
|
|
|
|
effectivePort); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
socket.exceptionHandler(this::handleException); |
|
|
|
|
|
|
|
socket.handler(this::handlePacket); |
|
|
|
|
|
|
|
controller.start(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
protected boolean validatePacketSize(final int packetSize) { |
|
|
|
* For uncontrolled exceptions occurring in the packet handlers. |
|
|
|
return packetSize <= MAX_PACKET_SIZE_BYTES; |
|
|
|
* |
|
|
|
|
|
|
|
* @param exception the exception that was raised |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private void handleException(final Throwable exception) { |
|
|
|
|
|
|
|
if (exception instanceof IOException) { |
|
|
|
|
|
|
|
LOG.debug("Packet handler exception", exception); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
LOG.error("Packet handler exception", exception); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
protected void handleIncomingPacket(final Endpoint sourceEndpoint, final Packet packet) { |
|
|
|
* The UDP packet handler. This is the entrypoint for all received datagrams. |
|
|
|
OptionalInt tcpPort = OptionalInt.empty(); |
|
|
|
* |
|
|
|
if (packet.getPacketData(PingPacketData.class).isPresent()) { |
|
|
|
* @param datagram the received datagram. |
|
|
|
final PingPacketData ping = packet.getPacketData(PingPacketData.class).orElseGet(null); |
|
|
|
*/ |
|
|
|
if (ping != null && ping.getFrom() != null && ping.getFrom().getTcpPort().isPresent()) { |
|
|
|
private void handlePacket(final DatagramPacket datagram) { |
|
|
|
tcpPort = ping.getFrom().getTcpPort(); |
|
|
|
try { |
|
|
|
|
|
|
|
final int length = datagram.data().length(); |
|
|
|
|
|
|
|
checkGuard( |
|
|
|
|
|
|
|
length <= MAX_PACKET_SIZE_BYTES, |
|
|
|
|
|
|
|
PeerDiscoveryPacketDecodingException::new, |
|
|
|
|
|
|
|
"Packet too large. Actual size (bytes): %s", |
|
|
|
|
|
|
|
length); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// We allow exceptions to bubble up, as they'll be picked up by the exception handler.
|
|
|
|
|
|
|
|
final Packet packet = Packet.decode(datagram.data()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OptionalInt fromPort = OptionalInt.empty(); |
|
|
|
|
|
|
|
if (packet.getPacketData(PingPacketData.class).isPresent()) { |
|
|
|
|
|
|
|
final PingPacketData ping = packet.getPacketData(PingPacketData.class).orElseGet(null); |
|
|
|
|
|
|
|
if (ping != null && ping.getFrom() != null && ping.getFrom().getTcpPort().isPresent()) { |
|
|
|
|
|
|
|
fromPort = ping.getFrom().getTcpPort(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Acquire the senders coordinates to build a Peer representation from them.
|
|
|
|
|
|
|
|
final String addr = datagram.sender().host(); |
|
|
|
|
|
|
|
final int port = datagram.sender().port(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Notify the peer controller.
|
|
|
|
|
|
|
|
final DiscoveryPeer peer = new DiscoveryPeer(packet.getNodeId(), addr, port, fromPort); |
|
|
|
|
|
|
|
controller.onMessage(packet, peer); |
|
|
|
|
|
|
|
} catch (final PeerDiscoveryPacketDecodingException e) { |
|
|
|
|
|
|
|
LOG.debug("Discarding invalid peer discovery packet", e); |
|
|
|
|
|
|
|
} catch (final Throwable t) { |
|
|
|
|
|
|
|
LOG.error("Encountered error while handling packet", t); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Notify the peer controller.
|
|
|
|
|
|
|
|
String host = sourceEndpoint.getHost(); |
|
|
|
|
|
|
|
int port = sourceEndpoint.getUdpPort(); |
|
|
|
|
|
|
|
final DiscoveryPeer peer = new DiscoveryPeer(packet.getNodeId(), host, port, tcpPort); |
|
|
|
|
|
|
|
controller.ifPresent(c -> c.onMessage(packet, peer)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Allows package-private components to dispatch messages to peers. It updates the lastContacted |
|
|
|
* Send a packet to the given recipient. |
|
|
|
* timestamp of the {@link DiscoveryPeer}. This method wraps the data in a Packet, calculates its |
|
|
|
|
|
|
|
* hash and signs it with our private key. |
|
|
|
|
|
|
|
* |
|
|
|
* |
|
|
|
* @param peer the recipient |
|
|
|
* @param peer the recipient |
|
|
|
* @param type the type of message |
|
|
|
* @param packet the packet to send |
|
|
|
* @param data the data packet to send |
|
|
|
|
|
|
|
* @return the sent packet |
|
|
|
|
|
|
|
*/ |
|
|
|
*/ |
|
|
|
public Packet sendPacket(final DiscoveryPeer peer, final PacketType type, final PacketData data) { |
|
|
|
protected void handleOutgoingPacket(final DiscoveryPeer peer, final Packet packet) { |
|
|
|
final Packet packet = Packet.create(type, data, keyPair); |
|
|
|
|
|
|
|
LOG.trace( |
|
|
|
LOG.trace( |
|
|
|
">>> Sending {} discovery packet to {} ({}): {}", |
|
|
|
">>> Sending {} discovery packet to {} ({}): {}", |
|
|
|
type, |
|
|
|
packet.getType(), |
|
|
|
peer.getEndpoint(), |
|
|
|
peer.getEndpoint(), |
|
|
|
peer.getId().slice(0, 16), |
|
|
|
peer.getId().slice(0, 16), |
|
|
|
packet); |
|
|
|
packet); |
|
|
|
|
|
|
|
|
|
|
|
// Update the lastContacted timestamp on the peer if the dispatch succeeds.
|
|
|
|
sendOutgoingPacket(peer, packet) |
|
|
|
socket.send( |
|
|
|
.whenComplete( |
|
|
|
packet.encode(), |
|
|
|
(res, err) -> { |
|
|
|
peer.getEndpoint().getUdpPort(), |
|
|
|
if (err != null) { |
|
|
|
peer.getEndpoint().getHost(), |
|
|
|
LOG.warn( |
|
|
|
ar -> { |
|
|
|
"Sending to peer {} failed, packet: {}", |
|
|
|
if (ar.failed()) { |
|
|
|
peer, |
|
|
|
LOG.warn( |
|
|
|
wrapBuffer(packet.encode()), |
|
|
|
"Sending to peer {} failed, packet: {}", |
|
|
|
err); |
|
|
|
peer, |
|
|
|
return; |
|
|
|
wrapBuffer(packet.encode()), |
|
|
|
} |
|
|
|
ar.cause()); |
|
|
|
peer.setLastContacted(System.currentTimeMillis()); |
|
|
|
return; |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
if (ar.succeeded()) { |
|
|
|
|
|
|
|
peer.setLastContacted(System.currentTimeMillis()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return packet; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@VisibleForTesting |
|
|
|
public Collection<DiscoveryPeer> getPeers() { |
|
|
|
public Collection<DiscoveryPeer> getPeers() { |
|
|
|
return Collections.unmodifiableCollection(controller.getPeers()); |
|
|
|
return controller |
|
|
|
|
|
|
|
.map(PeerDiscoveryController::getPeers) |
|
|
|
|
|
|
|
.map(Collections::unmodifiableCollection) |
|
|
|
|
|
|
|
.orElse(Collections.emptyList()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public DiscoveryPeer getAdvertisedPeer() { |
|
|
|
public DiscoveryPeer getAdvertisedPeer() { |
|
|
|
return advertisedPeer; |
|
|
|
return advertisedPeer; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public BytesValue getId() { |
|
|
|
|
|
|
|
return id; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public InetSocketAddress localAddress() { |
|
|
|
public InetSocketAddress localAddress() { |
|
|
|
checkState(socket != null, "uninitialized discovery agent"); |
|
|
|
checkState(localAddress != null, "Uninitialized discovery agent"); |
|
|
|
return new InetSocketAddress(socket.localAddress().host(), socket.localAddress().port()); |
|
|
|
return localAddress; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
@ -341,19 +258,8 @@ public class PeerDiscoveryAgent implements DisconnectCallback { |
|
|
|
* @return A unique ID identifying this observer, to that it can be removed later. |
|
|
|
* @return A unique ID identifying this observer, to that it can be removed later. |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
public long observePeerBondedEvents(final Consumer<PeerBondedEvent> observer) { |
|
|
|
public long observePeerBondedEvents(final Consumer<PeerBondedEvent> observer) { |
|
|
|
return controller.observePeerBondedEvents(observer); |
|
|
|
checkNotNull(observer); |
|
|
|
} |
|
|
|
return peerBondedObservers.subscribe(observer); |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Adds an observer that will get called when a new peer is dropped from the peer table. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <p><i>No guarantees are made about the order in which observers are invoked.</i> |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param observer The observer to call. |
|
|
|
|
|
|
|
* @return A unique ID identifying this observer, to that it can be removed later. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
public long observePeerDroppedEvents(final Consumer<PeerDroppedEvent> observer) { |
|
|
|
|
|
|
|
return controller.observePeerDroppedEvents(observer); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
@ -363,17 +269,7 @@ public class PeerDiscoveryAgent implements DisconnectCallback { |
|
|
|
* @return Whether the observer was located and removed. |
|
|
|
* @return Whether the observer was located and removed. |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
public boolean removePeerBondedObserver(final long observerId) { |
|
|
|
public boolean removePeerBondedObserver(final long observerId) { |
|
|
|
return controller.removePeerBondedObserver(observerId); |
|
|
|
return peerBondedObservers.unsubscribe(observerId); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Removes an previously added peer dropped observer. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param observerId The unique ID identifying the observer to remove. |
|
|
|
|
|
|
|
* @return Whether the observer was located and removed. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
public boolean removePeerDroppedObserver(final long observerId) { |
|
|
|
|
|
|
|
return controller.removePeerDroppedObserver(observerId); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
@ -383,7 +279,7 @@ public class PeerDiscoveryAgent implements DisconnectCallback { |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
@VisibleForTesting |
|
|
|
@VisibleForTesting |
|
|
|
public int getObserverCount() { |
|
|
|
public int getObserverCount() { |
|
|
|
return controller.observerCount(); |
|
|
|
return peerBondedObservers.getSubscriberCount(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private static void validateConfiguration(final DiscoveryConfiguration config) { |
|
|
|
private static void validateConfiguration(final DiscoveryConfiguration config) { |
|
|
|