Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions common/src/main/java/org/tron/core/config/Parameter.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public class NodeConstant {
public class NetConstants {

public static final long SYNC_FETCH_BATCH_NUM = 2000;
public static final long HELLO_TIME_OUT = 10000L;
public static final long ADV_TIME_OUT = 20000L;
public static final long SYNC_TIME_OUT = 5000L;
public static final long NET_MAX_TRX_PER_SECOND = 700L;
Expand Down
12 changes: 12 additions & 0 deletions framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,18 @@ public void onMessage(Channel c, byte[] data) {
return;
}

if (data == null || data.length == 0) {
peerConnection.disconnect(Protocol.ReasonCode.BAD_PROTOCOL);
return;
}

if (peerConnection.getHelloMessageReceive() == null
&& data[0] != MessageTypes.P2P_HELLO.asByte()
&& data[0] != MessageTypes.P2P_DISCONNECT.asByte()) {
peerConnection.disconnect(Protocol.ReasonCode.BAD_PROTOCOL);
return;
}

if (MessageTypes.PBFT_MSG.asByte() == data[0]) {
PbftMessage message = null;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.tron.common.utils.ByteArray;
import org.tron.common.utils.DecodeUtil;
import org.tron.common.utils.Sha256Hash;
import org.tron.common.utils.StringUtil;
import org.tron.core.ChainBaseManager;
Expand All @@ -13,13 +12,16 @@
import org.tron.core.net.message.MessageTypes;
import org.tron.core.net.message.TronMessage;
import org.tron.p2p.discover.Node;
import org.tron.p2p.utils.NetUtil;
import org.tron.program.Version;
import org.tron.protos.Discover.Endpoint;
import org.tron.protos.Protocol;
import org.tron.protos.Protocol.HelloMessage.Builder;

public class HelloMessage extends TronMessage {

private static final int MAX_BYTE_SIZE = 200;

@Getter
private Protocol.HelloMessage helloMessage;

Expand Down Expand Up @@ -121,6 +123,10 @@ public Class<?> getAnswerMessage() {

@Override
public String toString() {
if (!valid()) {
return "P2P_HELLO: invalid hello message";
}
Comment on lines +126 to +128

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[SHOULD] HELLO logging can still trigger synchronous DNS resolution, so the valid() guard is incomplete. valid() does not validate from.address, meaning a HELLO message that passes the length checks can still reach getFrom().getPreferInetSocketAddress() at line 131. When the local p2pConfig.ip is non-empty and net INFO logging is enabled, a hostname supplied by the remote peer can trigger synchronous resolution through InetSocketAddress.

This logging occurs before processHelloMessage validation. The default AsyncAppender also formats the message before enqueueing it, so DNS resolution latency can block the current Netty EventLoop and other connections running on it.

Please make toString() output only bounded fields that do not require resolution, and add literal validation for a non-empty IP field as well as port validation.

This is an incomplete fix for a pre-existing path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 31861e5. valid() now validates the raw endpoint’s address lengths, IP literals, and port range before toString() calls getFrom(). This prevents peer-supplied hostnames from triggering DNS lookups during logging while preserving the existing log format.

Added regression tests for invalid endpoints, boundary values, and the AsyncAppender path, including verification that rejected endpoints never reach getFrom().


StringBuilder builder = new StringBuilder();

builder.append(super.toString())
Expand Down Expand Up @@ -156,6 +162,10 @@ public Protocol.HelloMessage getInstance() {
}

public boolean valid() {
if (!validEndPoint()) {
return false;
}

byte[] genesisBlockByte = this.helloMessage.getGenesisBlockId().getHash().toByteArray();
if (genesisBlockByte.length != Sha256Hash.LENGTH) {
return false;
Expand All @@ -171,25 +181,45 @@ public boolean valid() {
return false;
}

int maxByteSize = 200;
ByteString address = this.helloMessage.getAddress();
if (!address.isEmpty() && address.toByteArray().length > maxByteSize) {
if (!address.isEmpty() && address.toByteArray().length > MAX_BYTE_SIZE) {
return false;
}

ByteString sig = this.helloMessage.getSignature();
if (!sig.isEmpty() && sig.toByteArray().length > maxByteSize) {
if (!sig.isEmpty() && sig.toByteArray().length > MAX_BYTE_SIZE) {
return false;
}

ByteString codeVersion = this.helloMessage.getCodeVersion();
if (!codeVersion.isEmpty() && codeVersion.toByteArray().length > maxByteSize) {
if (!codeVersion.isEmpty() && codeVersion.toByteArray().length > MAX_BYTE_SIZE) {
return false;
}

return true;
}

public boolean validEndPoint() {
Endpoint from = this.helloMessage.getFrom();
ByteString ipv4 = from.getAddress();
ByteString ipv6 = from.getAddressIpv6();
if (from.getPort() <= 0 || from.getPort() > 0xFFFF

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[SHOULD] Add a rejection condition in validEndPoint() for from.getNodeId().size() != Constant.NODE_ID_LEN. Currently, empty and non-standard-length nodeIds can pass endpoint validation and then participate in connection deduplication.

Use the protocol constant and cover the 0, 63, 64, and 65-byte boundaries. Existing valid HELLO test data should also use a 64-byte nodeId.

|| ipv4.size() > MAX_BYTE_SIZE || ipv6.size() > MAX_BYTE_SIZE) {
return false;
}
if (ipv4.isEmpty() && ipv6.isEmpty()) {
return false;
}
// Validate raw literals before getFrom() constructs a Node during logging.
if (!ipv4.isEmpty() && !NetUtil.validIpV4(ByteArray.toStr(ipv4.toByteArray()))) {
return false;
}
if (!ipv6.isEmpty() && !NetUtil.validIpV6(ByteArray.toStr(ipv6.toByteArray()))) {
return false;
}
return true;
}

public static Endpoint getEndpointFromNode(Node node) {
Endpoint.Builder builder = Endpoint.newBuilder()
.setPort(node.getPort());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public class PeerConnection {

@Setter
@Getter
private HelloMessage helloMessageReceive;
private volatile HelloMessage helloMessageReceive;

@Setter
@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ public void statusCheck() {
isDisconnected = true;
}

if (!isDisconnected) {
isDisconnected = peer.getHelloMessageReceive() == null
&& peer.getChannel().getStartTime() <= now - NetConstants.HELLO_TIME_OUT;
if (isDisconnected) {
logger.warn("Peer {} hello message timeout", peer.getInetAddress());
}
}

if (!isDisconnected) {
isDisconnected = peer.getAdvInvRequest().values().stream()
.anyMatch(time -> time < now - NetConstants.ADV_TIME_OUT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.tron.common.utils.ByteArray;
import org.tron.core.ChainBaseManager;
import org.tron.core.ChainBaseManager.NodeType;
import org.tron.core.config.args.Args;
import org.tron.core.net.TronNetService;
import org.tron.core.net.message.handshake.HelloMessage;
Expand Down Expand Up @@ -49,15 +47,17 @@ public void processHelloMessage(PeerConnection peer, HelloMessage msg) {
}

if (!msg.valid()) {
logger.warn("Peer {} invalid hello message parameters, GenesisBlockId: {}, SolidBlockId: {}, "
+ "HeadBlockId: {}, address: {}, sig: {}, codeVersion: {}",
logger.warn("Peer {} invalid hello message parameters, genesisHashLength: {}, "
Comment on lines 49 to +50

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[SHOULD] Move the entire msg.valid() check before getFrom() / updateNodeId(). updateNodeId() can close the later-established connection with the same nodeId, so an invalid HELLO may affect other peers before being rejected.

Add a regression test to ensure an invalid HELLO does not call getFrom() / updateNodeId() or close other connections.

+ "solidHashLength: {}, headHashLength: {}, address: {}, sig: {}, codeVersion: {}, "
+ "endpointValid: {}",
peer.getInetSocketAddress(),
ByteArray.toHexString(msg.getInstance().getGenesisBlockId().getHash().toByteArray()),
ByteArray.toHexString(msg.getInstance().getSolidBlockId().getHash().toByteArray()),
ByteArray.toHexString(msg.getInstance().getHeadBlockId().getHash().toByteArray()),
msg.getInstance().getAddress().toByteArray().length,
msg.getInstance().getSignature().toByteArray().length,
msg.getInstance().getCodeVersion().toByteArray().length);
msg.getInstance().getGenesisBlockId().getHash().size(),
msg.getInstance().getSolidBlockId().getHash().size(),
msg.getInstance().getHeadBlockId().getHash().size(),
msg.getInstance().getAddress().size(),
msg.getInstance().getSignature().size(),
msg.getInstance().getCodeVersion().size(),
msg.validEndPoint());
peer.disconnect(ReasonCode.INCOMPATIBLE_PROTOCOL);
return;
}
Expand Down
202 changes: 202 additions & 0 deletions framework/src/test/java/org/tron/core/net/P2pHelloGateTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package org.tron.core.net;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import java.util.Collections;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.tron.common.utils.Sha256Hash;
import org.tron.consensus.pbft.message.PbftMessage;
import org.tron.core.ChainBaseManager;
import org.tron.core.ChainBaseManager.NodeType;
import org.tron.core.capsule.BlockCapsule.BlockId;
import org.tron.core.net.message.MessageTypes;
import org.tron.core.net.message.PbftMessageFactory;
import org.tron.core.net.message.TronMessageFactory;
import org.tron.core.net.message.adv.InventoryMessage;
import org.tron.core.net.message.base.DisconnectMessage;
import org.tron.core.net.message.handshake.HelloMessage;
import org.tron.core.net.message.keepalive.PingMessage;
import org.tron.core.net.messagehandler.BlockMsgHandler;
import org.tron.core.net.messagehandler.ChainInventoryMsgHandler;
import org.tron.core.net.messagehandler.FetchInvDataMsgHandler;
import org.tron.core.net.messagehandler.InventoryMsgHandler;
import org.tron.core.net.messagehandler.PbftDataSyncHandler;
import org.tron.core.net.messagehandler.PbftMsgHandler;
import org.tron.core.net.messagehandler.SyncBlockChainMsgHandler;
import org.tron.core.net.messagehandler.TransactionsMsgHandler;
import org.tron.core.net.peer.PeerConnection;
import org.tron.core.net.peer.PeerManager;
import org.tron.core.net.service.handshake.HandshakeService;
import org.tron.core.net.service.keepalive.KeepAliveService;
import org.tron.core.net.service.statistics.NodeStatistics;
import org.tron.core.net.service.statistics.PeerStatistics;
import org.tron.p2p.connection.Channel;
import org.tron.p2p.discover.Node;
import org.tron.protos.Protocol.Inventory.InventoryType;
import org.tron.protos.Protocol.ReasonCode;

public class P2pHelloGateTest {

@InjectMocks
private P2pEventHandlerImpl handler;
@Mock
private HandshakeService handshakeService;
@Mock
private KeepAliveService keepAliveService;
@Mock
private SyncBlockChainMsgHandler syncBlockChainMsgHandler;
@Mock
private ChainInventoryMsgHandler chainInventoryMsgHandler;
@Mock
private InventoryMsgHandler inventoryMsgHandler;
@Mock
private FetchInvDataMsgHandler fetchInvDataMsgHandler;
@Mock
private BlockMsgHandler blockMsgHandler;
@Mock
private TransactionsMsgHandler transactionsMsgHandler;
@Mock
private PbftDataSyncHandler pbftDataSyncHandler;
@Mock
private PbftMsgHandler pbftMsgHandler;
@Mock
private PeerConnection peer;
@Mock
private Channel channel;

private AutoCloseable mocks;
private MockedStatic<PeerManager> peerManager;

@Before
public void setUp() {
mocks = MockitoAnnotations.openMocks(this);
peerManager = Mockito.mockStatic(PeerManager.class);
peerManager.when(() -> PeerManager.getPeerConnection(channel)).thenReturn(peer);
}

@After
public void tearDown() throws Exception {
peerManager.close();
mocks.close();
}

@Test
public void testRejectAllOtherMessageTypesBeforeParsing() {
try (MockedStatic<TronMessageFactory> tronFactory =
Mockito.mockStatic(TronMessageFactory.class);
MockedStatic<PbftMessageFactory> pbftFactory =
Mockito.mockStatic(PbftMessageFactory.class)) {
int rejected = 0;
for (MessageTypes type : MessageTypes.values()) {
if (type != MessageTypes.P2P_HELLO && type != MessageTypes.P2P_DISCONNECT) {
handler.onMessage(channel, new byte[] {type.asByte()});
rejected++;
}
}
verify(peer, times(rejected)).disconnect(ReasonCode.BAD_PROTOCOL);
tronFactory.verifyNoInteractions();
pbftFactory.verifyNoInteractions();
}
verify(peer, never()).getPeerStatistics();
verifyNoInteractions(handshakeService, keepAliveService, syncBlockChainMsgHandler,
chainInventoryMsgHandler, inventoryMsgHandler, fetchInvDataMsgHandler, blockMsgHandler,
transactionsMsgHandler, pbftDataSyncHandler, pbftMsgHandler);
}

@Test
public void testEmptyFramesAreRejected() {
handler.onMessage(channel, new byte[0]);
handler.onMessage(channel, null);
verify(peer, times(2)).disconnect(ReasonCode.BAD_PROTOCOL);
}

@Test
public void testUnknownPeerIsIgnored() {
peerManager.when(() -> PeerManager.getPeerConnection(channel)).thenReturn(null);
handler.onMessage(channel, new PingMessage().getSendBytes());
verifyNoInteractions(peer, keepAliveService);
}

@Test
public void testHelloIsDispatchedBeforeHandshake() throws Exception {
ChainBaseManager chain = mock(ChainBaseManager.class);
when(chain.getGenesisBlockId()).thenReturn(new BlockId());
when(chain.getSolidBlockId()).thenReturn(new BlockId());
when(chain.getHeadBlockId()).thenReturn(new BlockId());
when(chain.getNodeType()).thenReturn(NodeType.FULL);
HelloMessage hello = new HelloMessage(
new Node(new byte[64], "127.0.0.1", null, 18888), 0, chain);
when(peer.getPeerStatistics()).thenReturn(new PeerStatistics());

handler.onMessage(channel, hello.getSendBytes());

verify(handshakeService).processHelloMessage(eq(peer), any(HelloMessage.class));
verify(peer, never()).disconnect(any());
}

@Test
public void testDisconnectIsDispatchedBeforeHandshake() {
NodeStatistics stats = mock(NodeStatistics.class);
when(peer.getPeerStatistics()).thenReturn(new PeerStatistics());
when(peer.getP2pRateLimiter()).thenReturn(new P2pRateLimiter());
when(peer.getChannel()).thenReturn(channel);
when(peer.getNodeStatistics()).thenReturn(stats);

handler.onMessage(channel, new DisconnectMessage(ReasonCode.PEER_QUITING).getSendBytes());

verify(channel).close();
verify(stats).nodeDisconnectedRemote(ReasonCode.PEER_QUITING);
verify(peer, never()).disconnect(any());
}

@Test
public void testKeepAliveIsDispatchedAfterHello() throws Exception {
when(peer.getHelloMessageReceive()).thenReturn(mock(HelloMessage.class));
when(peer.getPeerStatistics()).thenReturn(new PeerStatistics());

handler.onMessage(channel, new PingMessage().getSendBytes());

verify(keepAliveService).processMessage(eq(peer), any(PingMessage.class));
verify(peer, never()).disconnect(any());
}

@Test
public void testInventoryIsDispatchedAfterHello() throws Exception {
when(peer.getHelloMessageReceive()).thenReturn(mock(HelloMessage.class));
when(peer.getPeerStatistics()).thenReturn(new PeerStatistics());
InventoryMessage inventory = new InventoryMessage(
Collections.singletonList(Sha256Hash.ZERO_HASH), InventoryType.BLOCK);

handler.onMessage(channel, inventory.getSendBytes());

verify(inventoryMsgHandler).processMessage(eq(peer), any(InventoryMessage.class));
verify(peer, never()).disconnect(any());
}

@Test
public void testPbftIsDispatchedAfterHello() throws Exception {
when(peer.getHelloMessageReceive()).thenReturn(mock(HelloMessage.class));
byte[] data = new byte[] {MessageTypes.PBFT_MSG.asByte()};
PbftMessage message = mock(PbftMessage.class);
try (MockedStatic<PbftMessageFactory> factory = Mockito.mockStatic(PbftMessageFactory.class)) {
factory.when(() -> PbftMessageFactory.create(data)).thenReturn(message);
handler.onMessage(channel, data);
verify(pbftMsgHandler).processMessage(peer, message);
verify(peer, never()).disconnect(any());
}
}
}
Loading
Loading