mirror of https://github.com/hyperledger/besu
When tls is on, use plain framer and handshaker to avoid double encryption (#3008)
* When tls is on, use passthrough framer and passthrough handshaker to avoid double encryption Signed-off-by: Saravana Perumal Shanmugam <perusworld@linux.com>pull/3014/head
parent
a2f517ce32
commit
bba8032883
@ -0,0 +1,67 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.p2p.plain; |
||||
|
||||
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; |
||||
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; |
||||
import org.hyperledger.besu.ethereum.rlp.RLPInput; |
||||
|
||||
import io.netty.buffer.ByteBuf; |
||||
import org.apache.tuweni.bytes.Bytes; |
||||
|
||||
public class MessageHandler { |
||||
|
||||
public static Bytes buildMessage(final PlainMessage message) { |
||||
final BytesValueRLPOutput ret = new BytesValueRLPOutput(); |
||||
ret.startList(); |
||||
ret.writeInt(message.getMessageType().getValue()); |
||||
if (MessageType.DATA.equals(message.getMessageType())) { |
||||
ret.writeInt(message.getCode()); |
||||
} |
||||
ret.writeBytes(message.getData()); |
||||
ret.endList(); |
||||
return ret.encoded(); |
||||
} |
||||
|
||||
public static Bytes buildMessage(final MessageType messageType, final Bytes data) { |
||||
return buildMessage(new PlainMessage(messageType, data)); |
||||
} |
||||
|
||||
public static Bytes buildMessage( |
||||
final MessageType messageType, final int code, final Bytes data) { |
||||
return buildMessage(new PlainMessage(messageType, code, data)); |
||||
} |
||||
|
||||
public static Bytes buildMessage(final MessageType messageType, final byte[] data) { |
||||
return buildMessage(new PlainMessage(messageType, data)); |
||||
} |
||||
|
||||
public static PlainMessage parseMessage(final ByteBuf buf) { |
||||
PlainMessage ret = null; |
||||
final ByteBuf bufferedBytes = buf.readSlice(buf.readableBytes()); |
||||
final byte[] byteArr = new byte[bufferedBytes.readableBytes()]; |
||||
bufferedBytes.getBytes(0, byteArr); |
||||
Bytes bytes = Bytes.wrap(byteArr); |
||||
final RLPInput input = new BytesValueRLPInput(bytes, true); |
||||
input.enterList(); |
||||
MessageType type = MessageType.forNumber(input.readInt()); |
||||
if (MessageType.DATA.equals(type)) { |
||||
ret = new PlainMessage(type, input.readInt(), input.readBytes()); |
||||
} else { |
||||
ret = new PlainMessage(type, input.readBytes()); |
||||
} |
||||
return ret; |
||||
} |
||||
} |
@ -0,0 +1,46 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.p2p.plain; |
||||
|
||||
public enum MessageType { |
||||
PING(0), |
||||
PONG(1), |
||||
DATA(2), |
||||
UNRECOGNIZED(-1), |
||||
; |
||||
|
||||
private final int value; |
||||
|
||||
private MessageType(final int value) { |
||||
this.value = value; |
||||
} |
||||
|
||||
public int getValue() { |
||||
return value; |
||||
} |
||||
|
||||
public static MessageType forNumber(final int value) { |
||||
switch (value) { |
||||
case 0: |
||||
return PING; |
||||
case 1: |
||||
return PONG; |
||||
case 2: |
||||
return DATA; |
||||
default: |
||||
return null; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,54 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.p2p.plain; |
||||
|
||||
import static com.google.common.base.Preconditions.checkState; |
||||
|
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.framing.Framer; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.framing.FramingException; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; |
||||
|
||||
import io.netty.buffer.ByteBuf; |
||||
import org.apache.logging.log4j.LogManager; |
||||
import org.apache.logging.log4j.Logger; |
||||
|
||||
public class PlainFramer extends Framer { |
||||
private static final Logger LOG = LogManager.getLogger(); |
||||
|
||||
public PlainFramer() { |
||||
LOG.trace("Initialising PlainFramer"); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized MessageData deframe(final ByteBuf buf) throws FramingException { |
||||
LOG.trace("Deframing Message"); |
||||
if (buf == null || !buf.isReadable()) { |
||||
return null; |
||||
} |
||||
PlainMessage message = MessageHandler.parseMessage(buf); |
||||
checkState( |
||||
MessageType.DATA.equals(message.getMessageType()), "unexpected message: needs to be data"); |
||||
return new RawMessage(message.getCode(), message.getData()); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized void frame(final MessageData message, final ByteBuf output) { |
||||
LOG.trace("Framing Message"); |
||||
output.writeBytes( |
||||
MessageHandler.buildMessage(MessageType.DATA, message.getCode(), message.getData()) |
||||
.toArray()); |
||||
} |
||||
} |
@ -0,0 +1,162 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.p2p.plain; |
||||
|
||||
import static com.google.common.base.Preconditions.checkState; |
||||
|
||||
import org.hyperledger.besu.crypto.NodeKey; |
||||
import org.hyperledger.besu.crypto.SECPPublicKey; |
||||
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.handshake.HandshakeException; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.handshake.HandshakeSecrets; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.handshake.Handshaker; |
||||
|
||||
import java.util.Optional; |
||||
import java.util.concurrent.atomic.AtomicReference; |
||||
|
||||
import io.netty.buffer.ByteBuf; |
||||
import io.netty.buffer.Unpooled; |
||||
import org.apache.logging.log4j.LogManager; |
||||
import org.apache.logging.log4j.Logger; |
||||
import org.apache.tuweni.bytes.Bytes; |
||||
|
||||
public class PlainHandshaker implements Handshaker { |
||||
|
||||
private static final Logger LOG = LogManager.getLogger(); |
||||
|
||||
private final AtomicReference<Handshaker.HandshakeStatus> status = |
||||
new AtomicReference<>(Handshaker.HandshakeStatus.UNINITIALIZED); |
||||
|
||||
private boolean initiator; |
||||
private NodeKey nodeKey; |
||||
private SECPPublicKey partyPubKey; |
||||
private Bytes initiatorMsg; |
||||
private Bytes responderMsg; |
||||
|
||||
@Override |
||||
public void prepareInitiator(final NodeKey nodeKey, final SECPPublicKey theirPubKey) { |
||||
checkState( |
||||
status.compareAndSet( |
||||
Handshaker.HandshakeStatus.UNINITIALIZED, Handshaker.HandshakeStatus.PREPARED), |
||||
"handshake was already prepared"); |
||||
|
||||
this.initiator = true; |
||||
this.nodeKey = nodeKey; |
||||
this.partyPubKey = theirPubKey; |
||||
LOG.trace( |
||||
"Prepared plain handshake with node {}... under INITIATOR role", |
||||
theirPubKey.getEncodedBytes().slice(0, 16)); |
||||
} |
||||
|
||||
@Override |
||||
public void prepareResponder(final NodeKey nodeKey) { |
||||
checkState( |
||||
status.compareAndSet( |
||||
Handshaker.HandshakeStatus.UNINITIALIZED, Handshaker.HandshakeStatus.IN_PROGRESS), |
||||
"handshake was already prepared"); |
||||
|
||||
this.initiator = false; |
||||
this.nodeKey = nodeKey; |
||||
LOG.trace("Prepared plain handshake under RESPONDER role"); |
||||
} |
||||
|
||||
@Override |
||||
public ByteBuf firstMessage() throws HandshakeException { |
||||
checkState(initiator, "illegal invocation of firstMessage on non-initiator end of handshake"); |
||||
checkState( |
||||
status.compareAndSet( |
||||
Handshaker.HandshakeStatus.PREPARED, Handshaker.HandshakeStatus.IN_PROGRESS), |
||||
"illegal invocation of firstMessage, handshake had already started"); |
||||
initiatorMsg = |
||||
MessageHandler.buildMessage(MessageType.PING, nodeKey.getPublicKey().getEncoded()); |
||||
|
||||
LOG.trace("First plain handshake message under INITIATOR role"); |
||||
|
||||
return Unpooled.wrappedBuffer(initiatorMsg.toArray()); |
||||
} |
||||
|
||||
@Override |
||||
public Optional<ByteBuf> handleMessage(final ByteBuf buf) throws HandshakeException { |
||||
checkState( |
||||
status.get() == Handshaker.HandshakeStatus.IN_PROGRESS, |
||||
"illegal invocation of onMessage on handshake that is not in progress"); |
||||
|
||||
PlainMessage message = MessageHandler.parseMessage(buf); |
||||
|
||||
Optional<Bytes> nextMsg = Optional.empty(); |
||||
if (initiator) { |
||||
checkState( |
||||
responderMsg == null, |
||||
"unexpected message: responder message had " + "already been received"); |
||||
|
||||
checkState( |
||||
message.getMessageType().equals(MessageType.PONG), |
||||
"unexpected message: needs to be a pong"); |
||||
responderMsg = message.getData(); |
||||
|
||||
LOG.trace( |
||||
"Received responder's plain handshake message from node {}...: {}", |
||||
partyPubKey.getEncodedBytes().slice(0, 16), |
||||
responderMsg); |
||||
|
||||
} else { |
||||
checkState( |
||||
initiatorMsg == null, |
||||
"unexpected message: initiator message " + "had already been received"); |
||||
checkState( |
||||
message.getMessageType().equals(MessageType.PING), |
||||
"unexpected message: needs to be a ping"); |
||||
|
||||
initiatorMsg = message.getData(); |
||||
LOG.trace( |
||||
"[{}] Received initiator's plain handshake message: {}", |
||||
nodeKey.getPublicKey().getEncodedBytes(), |
||||
initiatorMsg); |
||||
|
||||
partyPubKey = SignatureAlgorithmFactory.getInstance().createPublicKey(message.getData()); |
||||
|
||||
responderMsg = |
||||
MessageHandler.buildMessage(MessageType.PONG, nodeKey.getPublicKey().getEncoded()); |
||||
|
||||
LOG.trace( |
||||
"Generated responder's plain handshake message against peer {}...: {}", |
||||
partyPubKey.getEncodedBytes().slice(0, 16), |
||||
responderMsg); |
||||
|
||||
nextMsg = Optional.of(Bytes.wrap(responderMsg.toArray())); |
||||
} |
||||
|
||||
status.set(Handshaker.HandshakeStatus.SUCCESS); |
||||
LOG.trace("Handshake status set to {}", status.get()); |
||||
return nextMsg.map(bv -> Unpooled.wrappedBuffer(bv.toArray())); |
||||
} |
||||
|
||||
void computeSecrets() {} |
||||
|
||||
@Override |
||||
public HandshakeStatus getStatus() { |
||||
return status.get(); |
||||
} |
||||
|
||||
@Override |
||||
public HandshakeSecrets secrets() { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public SECPPublicKey partyPubKey() { |
||||
return partyPubKey; |
||||
} |
||||
} |
@ -0,0 +1,51 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.p2p.plain; |
||||
|
||||
import org.apache.tuweni.bytes.Bytes; |
||||
|
||||
public class PlainMessage { |
||||
|
||||
private final MessageType messageType; |
||||
private final int code; |
||||
private final Bytes data; |
||||
|
||||
public PlainMessage(final MessageType messageType, final byte[] data) { |
||||
this(messageType, Bytes.wrap(data)); |
||||
} |
||||
|
||||
public PlainMessage(final MessageType messageType, final Bytes data) { |
||||
this(messageType, -1, data); |
||||
} |
||||
|
||||
public PlainMessage(final MessageType messageType, final int code, final Bytes data) { |
||||
super(); |
||||
this.messageType = messageType; |
||||
this.code = code; |
||||
this.data = data; |
||||
} |
||||
|
||||
public MessageType getMessageType() { |
||||
return messageType; |
||||
} |
||||
|
||||
public Bytes getData() { |
||||
return data; |
||||
} |
||||
|
||||
public int getCode() { |
||||
return code; |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.p2p.rlpx.framing; |
||||
|
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.handshake.HandshakeSecrets; |
||||
|
||||
public interface FramerProvider { |
||||
|
||||
public Framer buildFramer(HandshakeSecrets secrets); |
||||
} |
@ -0,0 +1,20 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.p2p.rlpx.handshake; |
||||
|
||||
public interface HandshakerProvider { |
||||
|
||||
public Handshaker buildInstance(); |
||||
} |
@ -0,0 +1,48 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.p2p.network; |
||||
|
||||
import static java.util.Objects.requireNonNull; |
||||
|
||||
import java.io.IOException; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.util.function.Supplier; |
||||
import java.util.stream.Stream; |
||||
|
||||
public class FileBasedPasswordProvider implements Supplier<String> { |
||||
private final Path passwordFile; |
||||
|
||||
public FileBasedPasswordProvider(final Path passwordFile) { |
||||
requireNonNull(passwordFile, "Password file path cannot be null"); |
||||
this.passwordFile = passwordFile; |
||||
} |
||||
|
||||
@Override |
||||
public String get() { |
||||
try (final Stream<String> fileStream = Files.lines(passwordFile)) { |
||||
return fileStream |
||||
.findFirst() |
||||
.orElseThrow( |
||||
() -> |
||||
new RuntimeException( |
||||
String.format( |
||||
"Unable to read keystore password from %s", passwordFile.toString()))); |
||||
} catch (final IOException e) { |
||||
throw new RuntimeException( |
||||
String.format("Unable to read keystore password file %s", passwordFile.toString()), e); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,444 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.p2p.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 org.hyperledger.besu.crypto.NodeKey; |
||||
import org.hyperledger.besu.crypto.NodeKeyUtils; |
||||
import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; |
||||
import org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration; |
||||
import org.hyperledger.besu.ethereum.p2p.config.NetworkingConfiguration; |
||||
import org.hyperledger.besu.ethereum.p2p.config.RlpxConfiguration; |
||||
import org.hyperledger.besu.ethereum.p2p.network.exceptions.IncompatiblePeerException; |
||||
import org.hyperledger.besu.ethereum.p2p.peers.DefaultPeer; |
||||
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURLImpl; |
||||
import org.hyperledger.besu.ethereum.p2p.peers.Peer; |
||||
import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissions; |
||||
import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissionsDenylist; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.connections.netty.TLSConfiguration; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; |
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; |
||||
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; |
||||
import org.hyperledger.besu.pki.keystore.KeyStoreWrapper; |
||||
import org.hyperledger.besu.plugin.data.EnodeURL; |
||||
|
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.net.InetAddress; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.Optional; |
||||
import java.util.concurrent.CompletableFuture; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
import com.google.common.base.Charsets; |
||||
import io.vertx.core.Vertx; |
||||
import org.apache.tuweni.bytes.Bytes; |
||||
import org.assertj.core.api.Assertions; |
||||
import org.junit.After; |
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.mockito.junit.MockitoJUnitRunner; |
||||
|
||||
@RunWith(MockitoJUnitRunner.StrictStubs.class) |
||||
public class P2PPlainNetworkTest { |
||||
private final Vertx vertx = Vertx.vertx(); |
||||
private final NetworkingConfiguration config = |
||||
NetworkingConfiguration.create() |
||||
.setDiscovery(DiscoveryConfiguration.create().setActive(false)) |
||||
.setRlpx(RlpxConfiguration.create().setBindPort(0).setSupportedProtocols(subProtocol())); |
||||
|
||||
@After |
||||
public void closeVertx() { |
||||
vertx.close(); |
||||
} |
||||
|
||||
@Test |
||||
public void handshaking() throws Exception { |
||||
final NodeKey nodeKey = NodeKeyUtils.generate(); |
||||
try (final P2PNetwork listener = builder("partner1client1").nodeKey(nodeKey).build(); |
||||
final P2PNetwork connector = builder("partner2client1").build()) { |
||||
|
||||
listener.start(); |
||||
connector.start(); |
||||
final EnodeURL listenerEnode = listener.getLocalEnode().get(); |
||||
final Bytes listenId = listenerEnode.getNodeId(); |
||||
final int listenPort = listenerEnode.getListeningPort().get(); |
||||
|
||||
Assertions.assertThat( |
||||
connector |
||||
.connect(createPeer(listenId, listenPort)) |
||||
.get(30L, TimeUnit.SECONDS) |
||||
.getPeerInfo() |
||||
.getNodeId()) |
||||
.isEqualTo(listenId); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void preventMultipleConnections() throws Exception { |
||||
final NodeKey listenNodeKey = NodeKeyUtils.generate(); |
||||
try (final P2PNetwork listener = builder("partner1client1").nodeKey(listenNodeKey).build(); |
||||
final P2PNetwork connector = builder("partner2client1").build()) { |
||||
|
||||
listener.start(); |
||||
connector.start(); |
||||
final EnodeURL listenerEnode = listener.getLocalEnode().get(); |
||||
final Bytes listenId = listenerEnode.getNodeId(); |
||||
final int listenPort = listenerEnode.getListeningPort().get(); |
||||
|
||||
final CompletableFuture<PeerConnection> firstFuture = |
||||
connector.connect(createPeer(listenId, listenPort)); |
||||
final CompletableFuture<PeerConnection> secondFuture = |
||||
connector.connect(createPeer(listenId, listenPort)); |
||||
|
||||
final PeerConnection firstConnection = firstFuture.get(30L, TimeUnit.SECONDS); |
||||
final PeerConnection secondConnection = secondFuture.get(30L, TimeUnit.SECONDS); |
||||
Assertions.assertThat(firstConnection.getPeerInfo().getNodeId()).isEqualTo(listenId); |
||||
|
||||
// Connections should reference the same instance - i.e. we shouldn't create 2 distinct
|
||||
// connections
|
||||
assertThat(firstConnection == secondConnection).isTrue(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Tests that max peers setting is honoured and inbound connections that would exceed the limit |
||||
* are correctly disconnected. |
||||
* |
||||
* @throws Exception On Failure |
||||
*/ |
||||
@Test |
||||
public void limitMaxPeers() throws Exception { |
||||
final NodeKey nodeKey = NodeKeyUtils.generate(); |
||||
final int maxPeers = 1; |
||||
final NetworkingConfiguration listenerConfig = |
||||
NetworkingConfiguration.create() |
||||
.setDiscovery(DiscoveryConfiguration.create().setActive(false)) |
||||
.setRlpx( |
||||
RlpxConfiguration.create() |
||||
.setBindPort(0) |
||||
.setMaxPeers(maxPeers) |
||||
.setSupportedProtocols(subProtocol())); |
||||
try (final P2PNetwork listener = |
||||
builder("partner1client1").nodeKey(nodeKey).config(listenerConfig).build(); |
||||
final P2PNetwork connector1 = builder("partner1client1").build(); |
||||
final P2PNetwork connector2 = builder("partner2client1").build()) { |
||||
|
||||
// Setup listener and first connection
|
||||
listener.start(); |
||||
connector1.start(); |
||||
final EnodeURL listenerEnode = listener.getLocalEnode().get(); |
||||
final Bytes listenId = listenerEnode.getNodeId(); |
||||
final int listenPort = listenerEnode.getListeningPort().get(); |
||||
|
||||
final Peer listeningPeer = createPeer(listenId, listenPort); |
||||
Assertions.assertThat( |
||||
connector1 |
||||
.connect(listeningPeer) |
||||
.get(30L, TimeUnit.SECONDS) |
||||
.getPeerInfo() |
||||
.getNodeId()) |
||||
.isEqualTo(listenId); |
||||
|
||||
// Setup second connection and check that connection is not accepted
|
||||
final CompletableFuture<PeerConnection> peerFuture = new CompletableFuture<>(); |
||||
final CompletableFuture<DisconnectReason> reasonFuture = new CompletableFuture<>(); |
||||
connector2.subscribeDisconnect( |
||||
(peerConnection, reason, initiatedByPeer) -> { |
||||
peerFuture.complete(peerConnection); |
||||
reasonFuture.complete(reason); |
||||
}); |
||||
connector2.start(); |
||||
Assertions.assertThat( |
||||
connector2 |
||||
.connect(listeningPeer) |
||||
.get(30L, TimeUnit.SECONDS) |
||||
.getPeerInfo() |
||||
.getNodeId()) |
||||
.isEqualTo(listenId); |
||||
Assertions.assertThat(peerFuture.get(30L, TimeUnit.SECONDS).getPeerInfo().getNodeId()) |
||||
.isEqualTo(listenId); |
||||
assertThat(reasonFuture.get(30L, TimeUnit.SECONDS)) |
||||
.isEqualByComparingTo(DisconnectReason.TOO_MANY_PEERS); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void rejectPeerWithNoSharedCaps() throws Exception { |
||||
final NodeKey listenerNodeKey = NodeKeyUtils.generate(); |
||||
final NodeKey connectorNodeKey = NodeKeyUtils.generate(); |
||||
|
||||
final SubProtocol subprotocol1 = subProtocol("eth"); |
||||
final Capability cap1 = Capability.create(subprotocol1.getName(), 63); |
||||
final SubProtocol subprotocol2 = subProtocol("oth"); |
||||
final Capability cap2 = Capability.create(subprotocol2.getName(), 63); |
||||
try (final P2PNetwork listener = |
||||
builder("partner1client1") |
||||
.nodeKey(listenerNodeKey) |
||||
.supportedCapabilities(cap1) |
||||
.build(); |
||||
final P2PNetwork connector = |
||||
builder("partner2client1") |
||||
.nodeKey(connectorNodeKey) |
||||
.supportedCapabilities(cap2) |
||||
.build()) { |
||||
listener.start(); |
||||
connector.start(); |
||||
final EnodeURL listenerEnode = listener.getLocalEnode().get(); |
||||
final Bytes listenId = listenerEnode.getNodeId(); |
||||
final int listenPort = listenerEnode.getListeningPort().get(); |
||||
|
||||
final Peer listenerPeer = createPeer(listenId, listenPort); |
||||
final CompletableFuture<PeerConnection> connectFuture = connector.connect(listenerPeer); |
||||
assertThatThrownBy(connectFuture::get).hasCauseInstanceOf(IncompatiblePeerException.class); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void rejectIncomingConnectionFromBlacklistedPeer() throws Exception { |
||||
final PeerPermissionsDenylist localBlacklist = PeerPermissionsDenylist.create(); |
||||
|
||||
try (final P2PNetwork localNetwork = |
||||
builder("partner1client1").peerPermissions(localBlacklist).build(); |
||||
final P2PNetwork remoteNetwork = builder("partner2client1").build()) { |
||||
|
||||
localNetwork.start(); |
||||
remoteNetwork.start(); |
||||
|
||||
final EnodeURL localEnode = localNetwork.getLocalEnode().get(); |
||||
final Bytes localId = localEnode.getNodeId(); |
||||
final int localPort = localEnode.getListeningPort().get(); |
||||
|
||||
final EnodeURL remoteEnode = remoteNetwork.getLocalEnode().get(); |
||||
final Bytes remoteId = remoteEnode.getNodeId(); |
||||
final int remotePort = remoteEnode.getListeningPort().get(); |
||||
|
||||
final Peer localPeer = createPeer(localId, localPort); |
||||
final Peer remotePeer = createPeer(remoteId, remotePort); |
||||
|
||||
// Blacklist the remote peer
|
||||
localBlacklist.add(remotePeer); |
||||
|
||||
// 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
|
||||
Assertions.assertThat(connectFuture.get(5L, TimeUnit.SECONDS).getPeerInfo().getNodeId()) |
||||
.isEqualTo(localId); |
||||
Assertions.assertThat(peerFuture.get(5L, TimeUnit.SECONDS).getPeerInfo().getNodeId()) |
||||
.isEqualTo(localId); |
||||
assertThat(reasonFuture.get(5L, TimeUnit.SECONDS)) |
||||
.isEqualByComparingTo(DisconnectReason.UNKNOWN); |
||||
} |
||||
} |
||||
|
||||
@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("partner1client1").peerPermissions(peerPermissions).build(); |
||||
final P2PNetwork remoteNetwork = builder("partner2client1").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(PeerPermissions.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 Bytes localId = localEnode.getNodeId(); |
||||
Assertions.assertThat(connectFuture.get(5L, TimeUnit.SECONDS).getPeerInfo().getNodeId()) |
||||
.isEqualTo(localId); |
||||
Assertions.assertThat(peerFuture.get(5L, TimeUnit.SECONDS).getPeerInfo().getNodeId()) |
||||
.isEqualTo(localId); |
||||
assertThat(reasonFuture.get(5L, TimeUnit.SECONDS)) |
||||
.isEqualByComparingTo(DisconnectReason.UNKNOWN); |
||||
} |
||||
} |
||||
|
||||
private Peer createPeer(final Bytes nodeId, final int listenPort) { |
||||
return DefaultPeer.fromEnodeURL( |
||||
EnodeURLImpl.builder() |
||||
.ipAddress(InetAddress.getLoopbackAddress().getHostAddress()) |
||||
.nodeId(nodeId) |
||||
.discoveryAndListeningPorts(listenPort) |
||||
.build()); |
||||
} |
||||
|
||||
private static SubProtocol subProtocol() { |
||||
return subProtocol("eth"); |
||||
} |
||||
|
||||
private static SubProtocol subProtocol(final String name) { |
||||
return new SubProtocol() { |
||||
@Override |
||||
public String getName() { |
||||
return name; |
||||
} |
||||
|
||||
@Override |
||||
public int messageSpace(final int protocolVersion) { |
||||
return 8; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isValidMessageCode(final int protocolVersion, final int code) { |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public String messageName(final int protocolVersion, final int code) { |
||||
return INVALID_MESSAGE_NAME; |
||||
} |
||||
}; |
||||
} |
||||
|
||||
private Path initNSSConfigFile(final Path srcFilePath) { |
||||
Path ret = null; |
||||
try { |
||||
final String content = Files.readString(srcFilePath); |
||||
final String updated = |
||||
content.replaceAll( |
||||
"(nssSecmodDirectory\\W*)(\\.\\/.*)", |
||||
"$1".concat(srcFilePath.toAbsolutePath().toString().replace("nss.cfg", "nssdb"))); |
||||
final Path targetFilePath = createTemporaryFile("nsscfg"); |
||||
Files.write(targetFilePath, updated.getBytes(Charsets.UTF_8)); |
||||
ret = targetFilePath; |
||||
} catch (IOException e) { |
||||
throw new RuntimeException("Error populating nss config file", e); |
||||
} |
||||
return ret; |
||||
} |
||||
|
||||
private Path createTemporaryFile(final String suffix) { |
||||
final File tempFile; |
||||
try { |
||||
tempFile = File.createTempFile("temp", suffix); |
||||
tempFile.deleteOnExit(); |
||||
} catch (IOException e) { |
||||
throw new RuntimeException("Error creating temporary file", e); |
||||
} |
||||
return tempFile.toPath(); |
||||
} |
||||
|
||||
private static Path toPath(final String path) throws Exception { |
||||
return Path.of(P2PPlainNetworkTest.class.getResource(path).toURI()); |
||||
} |
||||
|
||||
public Optional<TLSConfiguration> p2pTLSEnabled(final String name, final String type) { |
||||
final TLSConfiguration.Builder builder = TLSConfiguration.Builder.tlsConfiguration(); |
||||
try { |
||||
final String nsspin = "/keys/%s/nsspin.txt"; |
||||
final String truststore = "/keys/%s/truststore.jks"; |
||||
final String crl = "/keys/%s/crl.pem"; |
||||
switch (type) { |
||||
case KeyStoreWrapper.KEYSTORE_TYPE_JKS: |
||||
builder |
||||
.withKeyStoreType(type) |
||||
.withKeyStorePath(toPath(String.format("/keys/%s/keystore.jks", name))) |
||||
.withKeyStorePasswordSupplier( |
||||
new FileBasedPasswordProvider(toPath(String.format(nsspin, name)))) |
||||
.withKeyStorePasswordPath(toPath(String.format(nsspin, name))) |
||||
.withTrustStoreType(type) |
||||
.withTrustStorePath(toPath(String.format(truststore, name))) |
||||
.withTrustStorePasswordSupplier( |
||||
new FileBasedPasswordProvider(toPath(String.format(nsspin, name)))) |
||||
.withTrustStorePasswordPath(toPath(String.format(nsspin, name))) |
||||
.withCrlPath(toPath(String.format(crl, name))); |
||||
break; |
||||
case KeyStoreWrapper.KEYSTORE_TYPE_PKCS12: |
||||
builder |
||||
.withKeyStoreType(type) |
||||
.withKeyStorePath(toPath(String.format("/keys/%s/keys.p12", name))) |
||||
.withKeyStorePasswordSupplier( |
||||
new FileBasedPasswordProvider(toPath(String.format(nsspin, name)))) |
||||
.withKeyStorePasswordPath(toPath(String.format(nsspin, name))) |
||||
.withTrustStoreType(KeyStoreWrapper.KEYSTORE_TYPE_JKS) |
||||
.withTrustStorePath(toPath(String.format(truststore, name))) |
||||
.withTrustStorePasswordSupplier( |
||||
new FileBasedPasswordProvider(toPath(String.format(nsspin, name)))) |
||||
.withTrustStorePasswordPath(toPath(String.format(nsspin, name))) |
||||
.withCrlPath(toPath(String.format(crl, name))); |
||||
break; |
||||
case KeyStoreWrapper.KEYSTORE_TYPE_PKCS11: |
||||
builder |
||||
.withKeyStoreType(type) |
||||
.withKeyStorePath(initNSSConfigFile(toPath(String.format("/keys/%s/nss.cfg", name)))) |
||||
.withKeyStorePasswordSupplier( |
||||
new FileBasedPasswordProvider(toPath(String.format(nsspin, name)))) |
||||
.withKeyStorePasswordPath(toPath(String.format(nsspin, name))) |
||||
.withCrlPath(toPath(String.format(crl, name))); |
||||
break; |
||||
} |
||||
} catch (Exception e) { |
||||
throw new RuntimeException(e); |
||||
} |
||||
return Optional.of(builder.build()); |
||||
} |
||||
|
||||
private DefaultP2PNetwork.Builder builder(final String name) { |
||||
return DefaultP2PNetwork.builder() |
||||
.vertx(vertx) |
||||
.config(config) |
||||
.nodeKey(NodeKeyUtils.generate()) |
||||
.p2pTLSConfiguration(p2pTLSEnabled(name, KeyStoreWrapper.KEYSTORE_TYPE_JKS)) |
||||
.metricsSystem(new NoOpMetricsSystem()) |
||||
.supportedCapabilities(Arrays.asList(Capability.create("eth", 63))) |
||||
.storageProvider(new InMemoryKeyValueStorageProvider()) |
||||
.forkIdSupplier(() -> Collections.singletonList(Bytes.EMPTY)); |
||||
} |
||||
} |
@ -0,0 +1,65 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.p2p.plain; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
import io.netty.buffer.Unpooled; |
||||
import org.apache.tuweni.bytes.Bytes; |
||||
import org.junit.Test; |
||||
|
||||
public class MessageHandlerTest { |
||||
|
||||
private static final byte[] PING_DATA = new byte[] {0, 1, 2, 3}; |
||||
|
||||
private static final byte[] PONG_DATA = new byte[] {3, 2, 1, 0}; |
||||
|
||||
private static final byte[] MESSAGE_DATA = new byte[] {0, 1, 1, 0}; |
||||
|
||||
private static final Bytes MESSAGE = Bytes.wrap(MESSAGE_DATA); |
||||
|
||||
private static final int DATA_CODE = 1000; |
||||
|
||||
@Test |
||||
public void buildPingMessage() throws Exception { |
||||
Bytes message = MessageHandler.buildMessage(MessageType.PING, PING_DATA); |
||||
assertThat(message).isNotNull(); |
||||
PlainMessage parsed = MessageHandler.parseMessage(Unpooled.wrappedBuffer(message.toArray())); |
||||
assertThat(parsed).isNotNull(); |
||||
assertThat(parsed.getMessageType()).isEqualTo(MessageType.PING); |
||||
assertThat(parsed.getData().toArray()).isEqualTo(PING_DATA); |
||||
} |
||||
|
||||
@Test |
||||
public void buildPongMessage() throws Exception { |
||||
Bytes message = MessageHandler.buildMessage(MessageType.PONG, PONG_DATA); |
||||
assertThat(message).isNotNull(); |
||||
PlainMessage parsed = MessageHandler.parseMessage(Unpooled.wrappedBuffer(message.toArray())); |
||||
assertThat(parsed).isNotNull(); |
||||
assertThat(parsed.getMessageType()).isEqualTo(MessageType.PONG); |
||||
assertThat(parsed.getData().toArray()).isEqualTo(PONG_DATA); |
||||
} |
||||
|
||||
@Test |
||||
public void buildDataMessage() throws Exception { |
||||
Bytes message = MessageHandler.buildMessage(MessageType.DATA, DATA_CODE, MESSAGE); |
||||
assertThat(message).isNotNull(); |
||||
PlainMessage parsed = MessageHandler.parseMessage(Unpooled.wrappedBuffer(message.toArray())); |
||||
assertThat(parsed).isNotNull(); |
||||
assertThat(parsed.getMessageType()).isEqualTo(MessageType.DATA); |
||||
assertThat(parsed.getCode()).isEqualTo(DATA_CODE); |
||||
assertThat(parsed.getData().toArray()).isEqualTo(MESSAGE_DATA); |
||||
} |
||||
} |
Loading…
Reference in new issue