Repository: paradigmxyz/reth (Rust)

Issue:
Discv5 talkrequest cannot get a normal response
### Describe the bug

Hi, I'm working on a project to verify that five Ethereum excution clients devp2p network protocol implementations (Geth, Erigon, Nethermind, Besu and Reth) behave consistently with each other.

When we tested the discv5 protocol, we found that the reth client could not respond to talkrequest packets normally, but ping and findnode were normal. Other clients responded to all types of data normally. After I turned on the p2p and network-related log filters of reth, the log of the talkrequest packet was as follows.



### Steps to reproduce

You can use the [D2PFuzz](https://github.com/AgnopraxLab/D2PFuzz) or hive tool we developed to send talkrequest, and you need to replace --target with the correct reth enr or enode.

./D2PFuzz bench --protocol "discv5" --target ./test/enode_reth.txt --chain ./test/ethdata --count 1 --ptype "talkrequest"

### Node logs

```text
'''
2025-03-25T07:56:18.770470Z TRACE net::discv5: discovered unverifiable enr, source socket doesn't match socket advertised in ENR enr=Enr { id: Some("v4"), seq: 1742889412519, NodeId: 0xfebbbae24bfec58c7a5bd5d7aa46d7ce6b21d55c017ad7474612c6d0aa625e45, signature: "9e85341e9c6e7954f0e9fe04796cb66aa0a9d7ac7e16927c136c0a58f148588c1581c6f8c0729e3158dfa4a5b5bab9bf47e76d2701da9816980b8687131a1670", IpV4 UDP Socket: Some(192.168.31.160:63313), IpV6 UDP Socket: None, IpV4 TCP Socket: None, IpV6 TCP Socket: None, Other Pairs: [("secp256k1", "a102a1bcd58e15543ccb6e450fe4c52ee3c09bce96f6477440090129284fafb1160f")], .. } socket=10.130.145.81:63313
2025-03-25T07:56:18.770545Z TRACE net::discv5: discovered peer is unreachable err=RLPx TCP socket missing enr=Enr { id: Some("v4"), seq: 1742889412519, NodeId: 0xfebbbae24bfec58c7a5bd5d7aa46d7ce6b21d55c017ad7474612c6d0aa625e45, signature: "9e85341e9c6e7954f0e9fe04796cb66aa0a9d7ac7e16927c136c0a58f148588c1581c6f8c0729e3158dfa4a5b5bab9bf47e76d2701da9816980b8687131a1670", IpV4 UDP Socket: Some(192.168.31.160:63313), IpV6 UDP Socket: None, IpV4 TCP Socket: None, IpV6 TCP Socket: None, Other Pairs: [("secp256k1", "a102a1bcd58e15543ccb6e450fe4c52ee3c09bce96f6477440090129284fafb1160f")], .. }
2025-03-25T07:56:19.443204Z TRACE libmdbx: Putting transaction monitor to sleep sleep_duration=5s elapsed=19.861µs
2025-03-25T07:56:20.664692Z TRACE net::discv5: starting periodic lookup query target=0x2dee..1f2b lookup_interval="60s"
'''
```

### Platform(s)

_No response_

### Container Type

Docker

### What version/commit are you on?

Geth v1.13.8
Reth v1.1.4
Erigon v2.60.10
Nethermind v1.30.3
Besu-25.2.0

### What database version are you on?

Current database version: 2
Local database version: 2

### Which chain / network are you on?

hive test network

### What type of node are you running?

Archive (default)

### What prune config do you use, if any?

_No response_

### If you've built Reth from source, provide the full command you used

_No response_

### Code of Conduct

- [x] I agree to follow the Code of Conduct


Discussion: @emhane is this the enr enode thing?
@1033309821 the enr isn't configured properly in the example, it is advertising the wrong ip address. will look into if we can make reth more lenient of misconfigured enrs. we have already addressed this in the past since geth is less strict ab this.
I'm not sure we want to fix this to fit this test, because that would mean discv5 sends loads of enrs up to reth with little guarantee that they are actually operating rlpx also, since they aren't advertising a tcp port. it would just lead to a lot of peer churn for the rlpx swarm. most useful peers will be advertising a tcp port. could also be that this is not true for geth, that if the udp and tcp ports are same, geth only advertises the udp port. would need to look for proof of this in the geth discv5 code though. @mattsse wdyt?
> [@1033309821](https://github.com/1033309821) the enr isn't configured properly in the example, it is advertising the wrong ip address. will look into if we can make reth more lenient of misconfigured enrs. we have already addressed this in the past since geth is less strict ab this.

@emhane Reth does a great job in this regard. It does solve many potential security issues, which leads to inconsistent behavior with other clients. We have also found many similar problems. For example, in discv4, modifying the identity to reth and resending data will not receive a response (other clients will reply to discv4 messages normally). I am not sure whether this is also related to enr. But is this too strict for the discovery protocol? Compared with other clients, the geth team may think that this does not affect security.
> But is this too strict for the discovery protocol?

I'd rather relax this a bit, if possible, in the hope this improves peering
> discv5 sends loads of enrs up to reth with little guarantee that they are actually operating rlpx also, since they aren't advertising a tcp port. it would just lead to a lot of peer churn for the rlpx swarm. most useful peers will be advertising a tcp port

I'd be okay with relaxing this because peering on opstack is especially bad and if this gives us more peers to cycle through I think that would be better than being super strict here @emhane 



Fix (patch):
diff --git a/crates/net/discv5/src/error.rs b/crates/net/discv5/src/error.rs
index 73b05ee81b2..c373a17194c 100644
--- a/crates/net/discv5/src/error.rs
+++ b/crates/net/discv5/src/error.rs
@@ -1,7 +1,5 @@
 //! Errors interfacing with [`discv5::Discv5`].
 
-use discv5::IpMode;
-
 /// Errors interfacing with [`discv5::Discv5`].
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
@@ -23,12 +21,6 @@ pub enum Error {
     /// Peer is unreachable over discovery.
     #[error("discovery socket missing")]
     UnreachableDiscovery,
-    /// Peer is unreachable over rlpx.
-    #[error("RLPx TCP socket missing")]
-    UnreachableRlpx,
-    /// Peer is not using same IP version as local node in rlpx.
-    #[error("RLPx TCP socket is unsupported IP version, local ip mode: {0:?}")]
-    IpVersionMismatchRlpx(IpMode),
     /// Failed to initialize [`discv5::Discv5`].
     #[error("init failed, {0}")]
     InitFailure(&'static str),
diff --git a/crates/net/discv5/src/lib.rs b/crates/net/discv5/src/lib.rs
index 18e17e96f59..2767941deec 100644
--- a/crates/net/discv5/src/lib.rs
+++ b/crates/net/discv5/src/lib.rs
@@ -333,27 +333,41 @@ impl Discv5 {
         Some(DiscoveredPeer { node_record, fork_id })
     }
 
-    /// Tries to convert an [`Enr`](discv5::Enr) into the backwards compatible type [`NodeRecord`],
-    /// w.r.t. local `RLPx` [`IpMode`]. Uses source socket as udp socket.
+    /// Tries to recover an unreachable [`Enr`](discv5::Enr) received via
+    /// [`discv5::Event::UnverifiableEnr`], into a [`NodeRecord`] usable by `RLPx`.
+    ///
+    /// NOTE: Fallback solution to be compatible with Geth which includes peers into the discv5
+    /// WAN topology which, for example, advertise in their ENR that localhost is their UDP IP
+    /// address. These peers are only discovered if they initiate a connection attempt, and we by
+    /// such means learn their reachable IP address. If we receive their ENR from any other peer
+    /// as part of a lookup query, we won't find a reachable IP address on which to dial them by
+    /// reading their ENR.
     pub fn try_into_reachable(
         &self,
         enr: &discv5::Enr,
         socket: SocketAddr,
     ) -> Result<NodeRecord, Error> {
+        // ignore UDP socket advertised in ENR, use sender socket instead
+        let address = socket.ip();
+        let udp_port = socket.port();
+
         let id = enr_to_discv4_id(enr).ok_or(Error::IncompatibleKeyType)?;
 
-        if enr.tcp4().is_none() && enr.tcp6().is_none() {
-            return Err(Error::UnreachableRlpx)
-        }
-        let Some(tcp_port) = (match self.rlpx_ip_mode {
+        let tcp_port = (match self.rlpx_ip_mode {
             IpMode::Ip4 => enr.tcp4(),
             IpMode::Ip6 => enr.tcp6(),
-            _ => unimplemented!("dual-stack support not implemented for rlpx"),
-        }) else {
-            return Err(Error::IpVersionMismatchRlpx(self.rlpx_ip_mode))
-        };
+            IpMode::DualStack => unimplemented!("dual-stack support not implemented for rlpx"),
+        })
+        .unwrap_or(
+            // tcp socket is missing from ENR, or is wrong IP version.
+            //
+            // by default geth runs discv5 and discv4 behind the same udp port (the discv4 default
+            // port 30303), so rlpx has a chance of successfully dialing the peer on its discv5
+            // udp port if its running geth's p2p code.
+            udp_port,
+        );
 
-        Ok(NodeRecord { address: socket.ip(), tcp_port, udp_port: socket.port(), id })
+        Ok(NodeRecord { address, tcp_port, udp_port, id })
     }
 
     /// Applies filtering rules on an ENR. Returns [`Ok`](FilterOutcome::Ok) if peer should be