Skip to main content

ethrex_p2p/discovery/
discv5_handlers.rs

1use crate::{
2    discovery::lookup::{IterativeLookup, LOOKUP_ALPHA, LOOKUP_BUCKET_SIZE},
3    discv5::{
4        messages::{
5            DISTANCES_PER_FIND_NODE_MSG, FindNodeMessage, Handshake, HandshakeAuthdata, Message,
6            NodesMessage, Ordinary, Packet, PacketTrait as _, PingMessage, PongMessage,
7            TalkResMessage, WhoAreYou, decrypt_message,
8        },
9        server::{Discv5Message, Discv5State, SessionSource, update_local_ip},
10        session::{
11            build_challenge_data, create_id_signature, derive_session_keys, verify_id_signature,
12        },
13    },
14    metrics::METRICS,
15    peer_table::{ContactValidation, DiscoveryProtocol, PeerTableServerProtocol as _},
16    rlpx::utils::compress_pubkey,
17    types::{Node, NodeRecord},
18    utils::{distance, node_id},
19};
20use bytes::{Bytes, BytesMut};
21use ethrex_common::{H256, H512};
22use rand::{Rng, rngs::OsRng};
23use secp256k1::{PublicKey, SecretKey, ecdsa::Signature};
24use std::{
25    net::SocketAddr,
26    time::{Duration, Instant},
27};
28use tracing::{debug, trace, warn};
29
30use super::server::{DiscoveryServer, DiscoveryServerError};
31
32/// Maximum number of ENRs per NODES message (limited by UDP packet size).
33const MAX_ENRS_PER_MESSAGE: usize = 3;
34/// Nodes not validated within this interval are candidates for revalidation.
35const REVALIDATION_INTERVAL: Duration = Duration::from_secs(12 * 60 * 60); // 12 hours
36/// Minimum interval between WHOAREYOU packets to the same IP address.
37const WHOAREYOU_RATE_LIMIT: Duration = Duration::from_secs(1);
38/// Maximum number of WHOAREYOU packets sent globally per second.
39const GLOBAL_WHOAREYOU_RATE_LIMIT: u32 = 100;
40
41impl DiscoveryServer {
42    pub(crate) async fn discv5_handle_packet(
43        &mut self,
44        Discv5Message { packet, from }: Discv5Message,
45    ) -> Result<(), DiscoveryServerError> {
46        #[cfg(feature = "metrics")]
47        {
48            use ethrex_metrics::p2p::METRICS_P2P;
49            match packet.header.flag {
50                0x01 => METRICS_P2P.inc_discv5_incoming("WhoAreYou"),
51                0x02 => METRICS_P2P.inc_discv5_incoming("Handshake"),
52                _ => {}
53            }
54        }
55        match packet.header.flag {
56            0x00 => self.discv5_handle_ordinary(packet, from).await,
57            0x01 => self.discv5_handle_who_are_you(packet, from).await,
58            0x02 => self.discv5_handle_handshake(packet, from).await,
59            f => {
60                tracing::trace!(protocol = "discv5", "Unexpected flag {f}");
61                Err(crate::discv5::messages::PacketCodecError::MalformedData.into())
62            }
63        }
64    }
65
66    async fn discv5_handle_ordinary(
67        &mut self,
68        packet: Packet,
69        addr: SocketAddr,
70    ) -> Result<(), DiscoveryServerError> {
71        let src_id = H256::from_slice(&packet.header.authdata);
72
73        let decrypt_key = self
74            .peer_table
75            .get_session_info(src_id)
76            .await?
77            .map(|s| s.inbound_key);
78
79        let discv5 = self.discv5.as_mut().expect("discv5 state must exist");
80
81        let ordinary = match decrypt_key {
82            Some(key) => match Ordinary::decode(&packet, &key) {
83                Ok(ordinary) => {
84                    if let Some(SessionSource { ip: session_ip, .. }) =
85                        discv5.session_ips.get(&src_id)
86                        && addr.ip() != *session_ip
87                    {
88                        trace!(
89                            protocol = "discv5",
90                            from = %src_id,
91                            %addr,
92                            expected_ip = %session_ip,
93                            "IP mismatch for existing session, sending WhoAreYou"
94                        );
95                        discv5.whoareyou_rate_limit.pop(&(addr.ip(), src_id));
96                        return self
97                            .discv5_send_who_are_you(packet.header.nonce, src_id, addr)
98                            .await;
99                    }
100                    ordinary
101                }
102                Err(_) => {
103                    trace!(protocol = "discv5", from = %src_id, %addr, "Decryption failed, sending WhoAreYou");
104                    return self
105                        .discv5_send_who_are_you(packet.header.nonce, src_id, addr)
106                        .await;
107                }
108            },
109            None => {
110                trace!(protocol = "discv5", from = %src_id, %addr, "No session, sending WhoAreYou");
111                return self
112                    .discv5_send_who_are_you(packet.header.nonce, src_id, addr)
113                    .await;
114            }
115        };
116
117        tracing::trace!(protocol = "discv5", received = %ordinary.message, from = %src_id, %addr);
118
119        self.discv5_handle_message(ordinary, addr, None).await
120    }
121
122    async fn discv5_handle_who_are_you(
123        &mut self,
124        packet: Packet,
125        addr: SocketAddr,
126    ) -> Result<(), DiscoveryServerError> {
127        let nonce = packet.header.nonce;
128        let discv5 = self.discv5.as_mut().expect("discv5 state must exist");
129        let Some((node, message, _)) = discv5.pending_by_nonce.remove(&nonce) else {
130            tracing::trace!(
131                protocol = "discv5",
132                "Received unexpected WhoAreYou packet. Ignoring it"
133            );
134            return Ok(());
135        };
136        tracing::trace!(protocol = "discv5", received = "WhoAreYou", from = %node.node_id(), %addr);
137
138        let challenge_data = build_challenge_data(
139            &packet.masking_iv,
140            &packet.header.static_header,
141            &packet.header.authdata,
142        );
143
144        let ephemeral_key = SecretKey::new(&mut OsRng);
145        let ephemeral_pubkey = ephemeral_key.public_key(secp256k1::SECP256K1).serialize();
146
147        let Some(dest_pubkey) = compress_pubkey(node.public_key) else {
148            return Err(DiscoveryServerError::CryptographyError(
149                "Invalid public key".to_string(),
150            ));
151        };
152
153        let session = derive_session_keys(
154            &ephemeral_key,
155            &dest_pubkey,
156            &self.local_node.node_id(),
157            &node.node_id(),
158            &challenge_data,
159            true,
160        );
161
162        let signature = create_id_signature(
163            &self.signer,
164            &challenge_data,
165            &ephemeral_pubkey,
166            &node.node_id(),
167        );
168
169        self.peer_table.set_session_info(node.node_id(), session)?;
170
171        let whoareyou = WhoAreYou::decode(&packet)?;
172        let record = (self.local_node_record.seq != whoareyou.enr_seq)
173            .then(|| self.local_node_record.clone());
174        self.discv5_send_handshake(message, signature, &ephemeral_pubkey, node, record)
175            .await
176    }
177
178    async fn discv5_handle_handshake(
179        &mut self,
180        packet: Packet,
181        addr: SocketAddr,
182    ) -> Result<(), DiscoveryServerError> {
183        let authdata = HandshakeAuthdata::decode(&packet.header.authdata)?;
184        let src_id = authdata.src_id;
185
186        let discv5 = self.discv5.as_mut().expect("discv5 state must exist");
187        let Some((challenge_data, _, _)) = discv5.pending_challenges.remove(&src_id) else {
188            trace!(protocol = "discv5", from = %src_id, %addr, "Received unexpected Handshake packet");
189            return Ok(());
190        };
191
192        let eph_pubkey = PublicKey::from_slice(&authdata.eph_pubkey).map_err(|_| {
193            DiscoveryServerError::CryptographyError("Invalid ephemeral pubkey".into())
194        })?;
195
196        let src_pubkey = if let Some(contact) = self.peer_table.get_contact(src_id).await? {
197            compress_pubkey(contact.node.public_key)
198        } else if let Some(record) = &authdata.record {
199            if !record.verify_signature() {
200                trace!(from = %src_id, "Handshake ENR signature verification failed");
201                return Ok(());
202            }
203            let pairs = record.pairs();
204            let pubkey = pairs
205                .secp256k1
206                .and_then(|pk| PublicKey::from_slice(pk.as_bytes()).ok());
207
208            if let Some(pk) = &pubkey {
209                let uncompressed = pk.serialize_uncompressed();
210                let derived_node_id = node_id(&H512::from_slice(&uncompressed[1..]));
211                if derived_node_id != src_id {
212                    trace!(from = %src_id, "Handshake ENR node_id mismatch");
213                    return Ok(());
214                }
215            }
216
217            pubkey
218        } else {
219            None
220        };
221
222        let Some(src_pubkey) = src_pubkey else {
223            trace!(protocol = "discv5", from = %src_id, "Cannot verify handshake: unknown sender public key");
224            return Ok(());
225        };
226
227        let signature = Signature::from_compact(&authdata.id_signature).map_err(|_| {
228            DiscoveryServerError::CryptographyError("Invalid signature format".into())
229        })?;
230
231        if !verify_id_signature(
232            &src_pubkey,
233            &challenge_data,
234            &authdata.eph_pubkey,
235            &self.local_node.node_id(),
236            &signature,
237        ) {
238            trace!(protocol = "discv5", from = %src_id, "Handshake signature verification failed");
239            return Ok(());
240        }
241
242        if let Some(record) = &authdata.record {
243            self.peer_table.new_contact_records(vec![record.clone()])?;
244        }
245
246        let session = derive_session_keys(
247            &self.signer,
248            &eph_pubkey,
249            &src_id,
250            &self.local_node.node_id(),
251            &challenge_data,
252            false,
253        );
254
255        self.peer_table.set_session_info(src_id, session.clone())?;
256        let discv5 = self.discv5.as_mut().expect("discv5 state must exist");
257        discv5.session_ips.insert(
258            src_id,
259            SessionSource {
260                ip: addr.ip(),
261                established_at: std::time::Instant::now(),
262            },
263        );
264
265        let mut encrypted = packet.encrypted_message.clone();
266        decrypt_message(&session.inbound_key, &packet, &mut encrypted)?;
267        let message = Message::decode(&encrypted)?;
268        trace!(protocol = "discv5", received = %message, from = %src_id, %addr, "Handshake completed");
269
270        let ordinary = Ordinary { src_id, message };
271        self.discv5_handle_message(ordinary, addr, Some(session.outbound_key))
272            .await
273    }
274
275    pub(crate) async fn discv5_revalidate(&mut self) -> Result<(), DiscoveryServerError> {
276        if let Some(contact) = self
277            .peer_table
278            .get_contact_to_revalidate(REVALIDATION_INTERVAL, DiscoveryProtocol::Discv5)
279            .await?
280            && let Err(e) = self.discv5_send_ping(&contact.node).await
281        {
282            trace!(protocol = "discv5", node = %contact.node.node_id(), err = ?e, "Failed to send revalidation PING");
283        }
284        Ok(())
285    }
286
287    pub(crate) async fn discv5_lookup(&mut self) -> Result<(), DiscoveryServerError> {
288        if self.discv5.is_none() {
289            return Ok(());
290        }
291
292        // Remove finished lookups
293        self.discv5
294            .as_mut()
295            .expect("discv5 state must exist")
296            .active_lookups
297            .retain(|l| !l.is_finished());
298
299        // If a lookup is already active, advance it instead of starting a new
300        // one. Lookups are timer-driven: each tick sends the next alpha queries.
301        // Responses feed results into the lookup but don't trigger new queries,
302        // which naturally throttles traffic.
303        if !self
304            .discv5
305            .as_ref()
306            .expect("discv5 state must exist")
307            .active_lookups
308            .is_empty()
309        {
310            return self.advance_v5_lookup().await;
311        }
312
313        let mut rng = OsRng;
314        let target_id: H256 = rng.r#gen();
315
316        // Seed with closest known nodes from the connection pool
317        let seed = self
318            .peer_table
319            .get_closest_from_pool(target_id, LOOKUP_BUCKET_SIZE)
320            .await?;
321        if seed.is_empty() {
322            trace!(
323                protocol = "discv5",
324                "No seeds for lookup, connection pool empty"
325            );
326            return Ok(());
327        }
328
329        trace!(
330            protocol = "discv5",
331            seeds = seed.len(),
332            "Starting new iterative lookup"
333        );
334        let lookup = IterativeLookup::new(target_id, seed);
335        let discv5 = self.discv5.as_mut().expect("discv5 state must exist");
336        discv5.active_lookups.push(lookup);
337
338        // Fire the initial queries for the new lookup
339        self.advance_v5_lookup().await
340    }
341
342    async fn advance_v5_lookup(&mut self) -> Result<(), DiscoveryServerError> {
343        let discv5 = match &mut self.discv5 {
344            Some(s) => s,
345            None => return Ok(()),
346        };
347
348        if discv5.active_lookups.is_empty() {
349            return Ok(());
350        }
351
352        // Collect queries from all active lookups
353        let mut queries: Vec<(usize, H256, H256, Node)> = Vec::new();
354        for (idx, lookup) in discv5.active_lookups.iter_mut().enumerate() {
355            let target = lookup.target;
356            for (node_id, node) in lookup.next_to_query(LOOKUP_ALPHA) {
357                queries.push((idx, target, node_id, node));
358            }
359        }
360
361        for (idx, target, node_id, node) in queries {
362            let find_node_msg = self.discv5_build_find_node_for_target(target, &node);
363            if let Err(e) = self.discv5_send_ordinary(find_node_msg, &node).await {
364                debug!(protocol = "discv5", sending = "FindNode", addr = ?node.udp_addr(), err=?e, "Error sending message");
365                self.peer_table.set_disposable(node_id)?;
366                METRICS.record_new_discarded_node();
367                if let Some(discv5) = &mut self.discv5
368                    && let Some(lookup) = discv5.active_lookups.get_mut(idx)
369                {
370                    lookup.record_timeout();
371                }
372            }
373        }
374        Ok(())
375    }
376
377    fn discv5_build_find_node_for_target(&self, target: H256, node: &Node) -> Message {
378        let center_distance = distance(&target, &node.node_id()) as u8;
379        let mut distances = Vec::new();
380        distances.push(center_distance as u32);
381        for i in 0..DISTANCES_PER_FIND_NODE_MSG / 2 {
382            if let Some(d) = center_distance.checked_add(i + 1) {
383                distances.push(d as u32)
384            }
385            if let Some(d) = center_distance.checked_sub(i + 1) {
386                distances.push(d as u32)
387            }
388        }
389        Message::FindNode(FindNodeMessage {
390            req_id: generate_req_id(),
391            distances,
392        })
393    }
394
395    async fn discv5_handle_ping(
396        &mut self,
397        ping_message: PingMessage,
398        sender_id: H256,
399        sender_addr: SocketAddr,
400        outbound_key: Option<[u8; 16]>,
401    ) -> Result<(), DiscoveryServerError> {
402        trace!(protocol = "discv5", from = %sender_id, enr_seq = ping_message.enr_seq, "Received PING");
403
404        let pong = Message::Pong(PongMessage {
405            req_id: ping_message.req_id,
406            enr_seq: self.local_node_record.seq,
407            recipient_addr: sender_addr,
408        });
409
410        if outbound_key.is_none()
411            && let Some(contact) = self.peer_table.get_contact(sender_id).await?
412        {
413            return self.discv5_send_ordinary(pong, &contact.node).await;
414        }
415        let key = self
416            .discv5_resolve_outbound_key(&sender_id, outbound_key)
417            .await?;
418        self.discv5_send_ordinary_to(pong, &sender_id, sender_addr, &key)
419            .await?;
420
421        Ok(())
422    }
423
424    pub async fn discv5_handle_pong(
425        &mut self,
426        pong_message: PongMessage,
427        sender_id: H256,
428    ) -> Result<(), DiscoveryServerError> {
429        self.peer_table
430            .record_pong_received(sender_id, pong_message.req_id)?;
431
432        if let Some(contact) = self.peer_table.get_contact(sender_id).await? {
433            let cached_seq = contact.record.as_ref().map_or(0, |r| r.seq);
434            if pong_message.enr_seq > cached_seq {
435                trace!(
436                    protocol = "discv5",
437                    from = %sender_id,
438                    cached_seq,
439                    pong_seq = pong_message.enr_seq,
440                    "ENR seq mismatch, requesting updated ENR (FINDNODE distance 0)"
441                );
442                let find_node = Message::FindNode(FindNodeMessage {
443                    req_id: generate_req_id(),
444                    distances: vec![0],
445                });
446                self.discv5_send_ordinary(find_node, &contact.node).await?;
447            }
448        }
449
450        let discv5 = self.discv5.as_mut().expect("discv5 state must exist");
451        if let Some(winning_ip) = discv5.record_ip_vote(pong_message.recipient_addr.ip(), sender_id)
452            && winning_ip != self.local_node.ip
453        {
454            tracing::info!(
455                protocol = "discv5",
456                old_ip = %self.local_node.ip,
457                new_ip = %winning_ip,
458                "External IP detected via PONG voting, updating local ENR"
459            );
460            update_local_ip(
461                &mut self.local_node,
462                &mut self.local_node_record,
463                &self.signer,
464                winning_ip,
465            );
466        }
467
468        Ok(())
469    }
470
471    async fn discv5_handle_find_node(
472        &mut self,
473        find_node_message: FindNodeMessage,
474        sender_id: H256,
475        sender_addr: SocketAddr,
476        outbound_key: Option<[u8; 16]>,
477    ) -> Result<(), DiscoveryServerError> {
478        let send_to_contact = match self
479            .peer_table
480            .validate_contact(sender_id, sender_addr.ip())
481            .await?
482        {
483            ContactValidation::Valid(contact) => Some(*contact),
484            ContactValidation::UnknownContact => None,
485            reason => {
486                trace!(from = %sender_id, ?reason, "Rejected FINDNODE");
487                return Ok(());
488            }
489        };
490
491        let mut nodes = self
492            .peer_table
493            .get_nodes_at_distances(find_node_message.distances.clone())
494            .await?;
495        if find_node_message.distances.contains(&0) {
496            nodes.push(self.local_node_record.clone());
497        }
498
499        let key = self
500            .discv5_resolve_outbound_key(&sender_id, outbound_key)
501            .await?;
502
503        let chunks: Vec<_> = nodes.chunks(MAX_ENRS_PER_MESSAGE).collect();
504        if chunks.is_empty() {
505            let nodes_message = Message::Nodes(NodesMessage {
506                req_id: find_node_message.req_id,
507                total: 1,
508                nodes: vec![],
509            });
510            if let Some(contact) = &send_to_contact {
511                self.discv5_send_ordinary(nodes_message, &contact.node)
512                    .await?;
513            } else {
514                self.discv5_send_ordinary_to(nodes_message, &sender_id, sender_addr, &key)
515                    .await?;
516            }
517        } else {
518            for chunk in &chunks {
519                let nodes_message = Message::Nodes(NodesMessage {
520                    req_id: find_node_message.req_id.clone(),
521                    total: chunks.len() as u64,
522                    nodes: chunk.to_vec(),
523                });
524                if let Some(contact) = &send_to_contact {
525                    self.discv5_send_ordinary(nodes_message, &contact.node)
526                        .await?;
527                } else {
528                    self.discv5_send_ordinary_to(nodes_message, &sender_id, sender_addr, &key)
529                        .await?;
530                }
531            }
532        }
533
534        Ok(())
535    }
536
537    async fn discv5_handle_nodes_message(
538        &mut self,
539        nodes_message: NodesMessage,
540    ) -> Result<(), DiscoveryServerError> {
541        self.peer_table
542            .new_contact_records(nodes_message.nodes.clone())?;
543
544        // Feed results into ALL active lookups (but don't advance — the timer
545        // drives lookup progress so that traffic stays controlled).
546        if let Some(discv5) = &mut self.discv5 {
547            let entries: Vec<(H256, Node)> = nodes_message
548                .nodes
549                .iter()
550                .filter_map(|r| Node::from_enr(r).ok().map(|n| (n.node_id(), n)))
551                .collect();
552            for lookup in &mut discv5.active_lookups {
553                lookup.feed_results(entries.clone());
554            }
555            if let Some(lookup) = discv5.active_lookups.first_mut() {
556                lookup.record_response();
557            }
558        }
559
560        Ok(())
561    }
562
563    async fn discv5_send_ping(&mut self, node: &Node) -> Result<(), DiscoveryServerError> {
564        let req_id = generate_req_id();
565
566        let ping = Message::Ping(PingMessage {
567            req_id: req_id.clone(),
568            enr_seq: self.local_node_record.seq,
569        });
570
571        self.discv5_send_ordinary(ping, node).await?;
572        self.peer_table.record_ping_sent(node.node_id(), req_id)?;
573
574        Ok(())
575    }
576
577    async fn discv5_send_ordinary(
578        &mut self,
579        message: Message,
580        node: &Node,
581    ) -> Result<(), DiscoveryServerError> {
582        #[cfg(feature = "metrics")]
583        {
584            use ethrex_metrics::p2p::METRICS_P2P;
585            METRICS_P2P.inc_discv5_outgoing(message.metric_label());
586        }
587        let ordinary = Ordinary {
588            src_id: self.local_node.node_id(),
589            message: message.clone(),
590        };
591        let encrypt_key = match self.peer_table.get_session_info(node.node_id()).await? {
592            Some(s) => s.outbound_key,
593            None => {
594                trace!(
595                    protocol = "discv5",
596                    node = %node.node_id(),
597                    "No session found in send_ordinary, using zeroed key to trigger handshake"
598                );
599                [0; 16]
600            }
601        };
602
603        let discv5 = self.discv5.as_mut().expect("discv5 state must exist");
604        let mut rng = OsRng;
605        let masking_iv: u128 = rng.r#gen();
606        let nonce = discv5.next_nonce(&mut rng);
607
608        let packet = ordinary.encode(&nonce, masking_iv.to_be_bytes(), &encrypt_key)?;
609
610        self.discv5_send_packet(&packet, &node.node_id(), node.udp_addr())
611            .await?;
612        let discv5 = self.discv5.as_mut().expect("discv5 state must exist");
613        discv5
614            .pending_by_nonce
615            .insert(nonce, (node.clone(), message, Instant::now()));
616        Ok(())
617    }
618
619    async fn discv5_resolve_outbound_key(
620        &self,
621        node_id: &H256,
622        key: Option<[u8; 16]>,
623    ) -> Result<[u8; 16], DiscoveryServerError> {
624        if let Some(key) = key {
625            return Ok(key);
626        }
627        match self.peer_table.get_session_info(*node_id).await? {
628            Some(s) => Ok(s.outbound_key),
629            None => {
630                trace!(
631                    protocol = "discv5",
632                    node = %node_id,
633                    "No session found in resolve_outbound_key, using zeroed key"
634                );
635                Ok([0; 16])
636            }
637        }
638    }
639
640    async fn discv5_send_ordinary_to(
641        &mut self,
642        message: Message,
643        dest_id: &H256,
644        addr: SocketAddr,
645        encrypt_key: &[u8; 16],
646    ) -> Result<(), DiscoveryServerError> {
647        #[cfg(feature = "metrics")]
648        {
649            use ethrex_metrics::p2p::METRICS_P2P;
650            METRICS_P2P.inc_discv5_outgoing(message.metric_label());
651        }
652        let ordinary = Ordinary {
653            src_id: self.local_node.node_id(),
654            message,
655        };
656
657        let discv5 = self.discv5.as_mut().expect("discv5 state must exist");
658        let mut rng = OsRng;
659        let masking_iv: u128 = rng.r#gen();
660        let nonce = discv5.next_nonce(&mut rng);
661
662        let packet = ordinary.encode(&nonce, masking_iv.to_be_bytes(), encrypt_key)?;
663
664        self.discv5_send_packet(&packet, dest_id, addr).await?;
665        Ok(())
666    }
667
668    async fn discv5_send_handshake(
669        &mut self,
670        message: Message,
671        signature: Signature,
672        eph_pubkey: &[u8],
673        node: Node,
674        record: Option<NodeRecord>,
675    ) -> Result<(), DiscoveryServerError> {
676        #[cfg(feature = "metrics")]
677        {
678            use ethrex_metrics::p2p::METRICS_P2P;
679            METRICS_P2P.inc_discv5_outgoing("Handshake");
680        }
681        let handshake = Handshake {
682            src_id: self.local_node.node_id(),
683            id_signature: signature.serialize_compact().to_vec(),
684            eph_pubkey: eph_pubkey.to_vec(),
685            record,
686            message: message.clone(),
687        };
688        let encrypt_key = match self.peer_table.get_session_info(node.node_id()).await? {
689            Some(s) => s.outbound_key,
690            None => {
691                trace!(
692                    protocol = "discv5",
693                    node = %node.node_id(),
694                    "No session found in send_handshake, using zeroed key"
695                );
696                [0; 16]
697            }
698        };
699
700        let discv5 = self.discv5.as_mut().expect("discv5 state must exist");
701        let mut rng = OsRng;
702        let masking_iv: u128 = rng.r#gen();
703        let nonce = discv5.next_nonce(&mut rng);
704
705        let packet = handshake.encode(&nonce, masking_iv.to_be_bytes(), &encrypt_key)?;
706
707        self.discv5_send_packet(&packet, &node.node_id(), node.udp_addr())
708            .await?;
709        let discv5 = self.discv5.as_mut().expect("discv5 state must exist");
710        discv5
711            .pending_by_nonce
712            .insert(nonce, (node, message, Instant::now()));
713        Ok(())
714    }
715
716    pub async fn discv5_send_who_are_you(
717        &mut self,
718        nonce: [u8; 12],
719        src_id: H256,
720        addr: SocketAddr,
721    ) -> Result<(), DiscoveryServerError> {
722        #[cfg(feature = "metrics")]
723        {
724            use ethrex_metrics::p2p::METRICS_P2P;
725            METRICS_P2P.inc_discv5_outgoing("WhoAreYou");
726        }
727        let discv5 = self.discv5.as_mut().expect("discv5 state must exist");
728
729        let rate_key = (addr.ip(), src_id);
730        let now = Instant::now();
731
732        // Global rate limit
733        if now.duration_since(discv5.whoareyou_global_window_start) >= Duration::from_secs(1) {
734            discv5.whoareyou_global_count = 0;
735            discv5.whoareyou_global_window_start = now;
736        }
737        if discv5.whoareyou_global_count >= GLOBAL_WHOAREYOU_RATE_LIMIT {
738            if discv5.whoareyou_global_count == GLOBAL_WHOAREYOU_RATE_LIMIT {
739                discv5.whoareyou_global_count = GLOBAL_WHOAREYOU_RATE_LIMIT + 1;
740                warn!(
741                    protocol = "discv5",
742                    "Global WHOAREYOU rate limit reached ({GLOBAL_WHOAREYOU_RATE_LIMIT}/s), \
743                     dropping excess packets. This is normal during initial discovery or \
744                     network churn; persistent occurrences may indicate a DoS attempt"
745                );
746            }
747            return Ok(());
748        }
749
750        // Resend existing challenge if pending
751        if let Some((_, _, raw_bytes)) = discv5.pending_challenges.get(&src_id) {
752            trace!(
753                protocol = "discv5",
754                to = %src_id,
755                %addr,
756                "Resending existing WhoAreYou challenge"
757            );
758            self.udp_socket.send_to(raw_bytes, addr).await?;
759            return Ok(());
760        }
761
762        // Per-(IP, node) rate limit
763        if !Discv5State::is_private_ip(addr.ip())
764            && let Some(last_sent) = discv5.whoareyou_rate_limit.get(&rate_key)
765            && now.duration_since(*last_sent) < WHOAREYOU_RATE_LIMIT
766        {
767            trace!(
768                protocol = "discv5",
769                to_ip = %addr.ip(),
770                "Rate limiting WHOAREYOU packet (amplification attack prevention)"
771            );
772            return Ok(());
773        }
774
775        discv5.whoareyou_rate_limit.push(rate_key, now);
776        discv5.whoareyou_global_count += 1;
777
778        let mut rng = OsRng;
779
780        let enr_seq = self
781            .peer_table
782            .get_contact(src_id)
783            .await?
784            .map_or(0, |c| c.record.as_ref().map_or(0, |r| r.seq));
785
786        let who_are_you = WhoAreYou {
787            id_nonce: rng.r#gen(),
788            enr_seq,
789        };
790
791        let masking_iv: u128 = rng.r#gen();
792        let packet = who_are_you.encode(&nonce, masking_iv.to_be_bytes(), &[0; 16])?;
793
794        let mut raw_buf = BytesMut::new();
795        packet.encode(&mut raw_buf, &src_id)?;
796        let raw_bytes = raw_buf.to_vec();
797
798        let challenge_data = build_challenge_data(
799            &masking_iv.to_be_bytes(),
800            &packet.header.static_header,
801            &packet.header.authdata,
802        );
803        let discv5 = self.discv5.as_mut().expect("discv5 state must exist");
804        discv5
805            .pending_challenges
806            .insert(src_id, (challenge_data, Instant::now(), raw_bytes.clone()));
807
808        self.udp_socket.send_to(&raw_bytes, addr).await?;
809        trace!(protocol = "discv5", to = %src_id, %addr, flag = packet.header.flag, "Sent packet");
810
811        Ok(())
812    }
813
814    async fn discv5_send_packet(
815        &self,
816        packet: &Packet,
817        dest_id: &H256,
818        addr: SocketAddr,
819    ) -> Result<(), DiscoveryServerError> {
820        let mut buf = BytesMut::new();
821        packet.encode(&mut buf, dest_id)?;
822        self.udp_socket.send_to(&buf, addr).await?;
823        trace!(protocol = "discv5", to = %dest_id, %addr, flag = packet.header.flag, "Sent packet");
824        Ok(())
825    }
826
827    async fn discv5_handle_message(
828        &mut self,
829        ordinary: Ordinary,
830        sender_addr: SocketAddr,
831        outbound_key: Option<[u8; 16]>,
832    ) -> Result<(), DiscoveryServerError> {
833        let sender_id = ordinary.src_id;
834        if sender_id == self.local_node.node_id() {
835            return Ok(());
836        }
837        #[cfg(feature = "metrics")]
838        {
839            use ethrex_metrics::p2p::METRICS_P2P;
840            METRICS_P2P.inc_discv5_incoming(ordinary.message.metric_label());
841        }
842        match ordinary.message {
843            Message::Ping(ping_message) => {
844                if ping_message.req_id.len() > 8 {
845                    trace!(protocol = "discv5", from = %sender_id, "Dropping PING with oversized req_id");
846                    return Ok(());
847                }
848                self.discv5_handle_ping(ping_message, sender_id, sender_addr, outbound_key)
849                    .await?
850            }
851            Message::Pong(pong_message) => {
852                self.discv5_handle_pong(pong_message, sender_id).await?;
853            }
854            Message::FindNode(find_node_message) => {
855                if find_node_message.req_id.len() > 8 {
856                    trace!(protocol = "discv5", from = %sender_id, "Dropping FINDNODE with oversized req_id");
857                    return Ok(());
858                }
859                self.discv5_handle_find_node(
860                    find_node_message,
861                    sender_id,
862                    sender_addr,
863                    outbound_key,
864                )
865                .await?;
866            }
867            Message::Nodes(nodes_message) => {
868                self.discv5_handle_nodes_message(nodes_message).await?;
869            }
870            Message::TalkReq(talk_req_message) => {
871                if talk_req_message.req_id.len() > 8 {
872                    trace!(protocol = "discv5", from = %sender_id, "Dropping TALKREQ with oversized req_id");
873                    return Ok(());
874                }
875                let talk_res = Message::TalkRes(TalkResMessage {
876                    req_id: talk_req_message.req_id,
877                    response: vec![],
878                });
879                let key = self
880                    .discv5_resolve_outbound_key(&sender_id, outbound_key)
881                    .await?;
882                self.discv5_send_ordinary_to(talk_res, &sender_id, sender_addr, &key)
883                    .await?;
884            }
885            Message::TalkRes(_talk_res_message) => (),
886            Message::Ticket(_ticket_message) => (),
887        }
888        Ok(())
889    }
890}
891
892fn generate_req_id() -> Bytes {
893    let mut rng = OsRng;
894    Bytes::from(rng.r#gen::<u64>().to_be_bytes().to_vec())
895}