Skip to content

Conversation

@ElFantasma
Copy link
Contributor

Motivation

Discv5's handle_find_node responds to contact.node.udp_addr() (stored IP from ENR) without any sender address validation. A malicious peer with a session could update its ENR to point to a victim IP and trigger large NODES responses sent to the victim. Discv4 already has this protection via validate_contact.

Description

Use validate_contact instead of get_contact in handle_find_node to verify the sender's IP matches the stored contact IP before sending NODES responses. This prevents amplification attacks and matches the existing protection already present in discv4.

Closes #6199

Checklist

  • All existing tests pass (cargo test -p ethrex-p2p --features experimental-discv5)

@ElFantasma ElFantasma requested a review from a team as a code owner February 12, 2026 20:15
@github-actions github-actions bot added the L1 Ethereum client label Feb 12, 2026
@github-actions
Copy link

🤖 Kimi Code Review

Security Issue: Amplification Attack Mitigation

File: crates/networking/p2p/discv5/server.rs
Lines: 503-525, 779-783

The PR attempts to prevent amplification attacks by validating the sender's IP address matches the stored contact information. However, there's a critical flaw in the implementation:

  1. Line 516-517: The code uses validate_contact which appears to return a PeerTableOutMessage enum, but the logic flow is problematic. The Contact(contact) variant is unwrapped with *contact, which suggests contact is a Box<Contact> or similar, but this dereferencing could panic if the enum variant isn't Contact.

  2. Missing validation: The actual IP validation logic isn't visible in this diff, but the pattern suggests validate_contact might be returning a rejection reason rather than performing strict IP matching. This could allow attackers to still use spoofed IPs if the validation is too permissive.

  3. Error handling: The trace! on line 522-524 silently drops invalid requests, which is good for preventing amplification, but the error reason isn't logged with sufficient detail for debugging potential attacks.

Recommendations:

  1. Add explicit IP comparison instead of relying on validate_contact:
let contact = match self.peer_table.get_contact(sender_id).await? {
    Some(contact) => {
        if contact.ip() != sender_addr.ip() {
            trace!(from = %sender_id, expected_ip = %contact.ip(), actual_ip = %sender_addr.ip(), "IP mismatch in FINDNODE");
            return Ok(());
        }
        contact
    }
    None => {
        trace!(from = %sender_id, "Received FINDNODE from unknown node");
        return Ok(());
    }
};
  1. Consider rate limiting for FINDNODE requests per IP to further mitigate amplification attacks.

  2. Add metrics for rejected requests to monitor potential attacks.

The approach is directionally correct for security, but the implementation needs refinement to ensure robust IP validation.


Automated review by Kimi (Moonshot AI) · custom prompt

@github-actions
Copy link

🤖 Claude Code Review

Now I have enough context to provide a thorough review. Let me compile my findings.

PR #6200 Review: Add Anti-Amplification Check to discv5 handle_find_node

Summary

This PR addresses a critical security vulnerability (amplification attack vector) in the discv5 implementation by adding IP validation before responding to FINDNODE requests. The fix mirrors the existing protection in discv4.

Security Assessment ✅

Critical Security Fix: This properly addresses a UDP amplification attack vector where:

  1. An attacker with an established session could update their ENR to point to a victim's IP
  2. Send FINDNODE requests (small packets)
  3. Trigger large NODES responses to be sent to the victim
  4. Amplify attack traffic significantly

The implementation correctly:

  • Validates sender IP matches stored contact IP before responding
  • Uses the existing validate_contact method that checks for:
    • Contact existence (UnknownContact)
    • Contact validation status (InvalidContact)
    • IP address matching (IpMismatch)
  • Silently drops invalid requests with appropriate trace logging

Code Correctness ✅

Line-by-line Analysis

server.rs:14 - Import addition is correct:

peer_table::{OutMessage as PeerTableOutMessage, PeerTable, PeerTableError},

Properly aliases OutMessage to avoid confusion with other message types.

server.rs:506 - Function signature update:

sender_addr: SocketAddr,

Correctly passes the sender's socket address for validation. This is already available at the call site (line 782).

server.rs:516-526 - Validation logic:

let contact = match self
    .peer_table
    .validate_contact(&sender_id, sender_addr.ip())
    .await?
{
    PeerTableOutMessage::Contact(contact) => *contact,
    reason => {
        trace!(from = %sender_id, ?reason, "Rejected FINDNODE");
        return Ok(());
    }
};

Strengths:

  • Pattern matches on all OutMessage variants properly
  • Dereferences the Box<Contact> correctly with *contact
  • Uses structured logging with both sender_id and rejection reason
  • Returns Ok(()) to gracefully ignore invalid requests (no error propagation)

Minor observation: The match arm reason => catches InvalidContact, UnknownContact, and IpMismatch. The trace message is generic but includes the debug-formatted ?reason, which is appropriate.

server.rs:782-783 - Call site update:

self.handle_find_node(find_node_message, sender_id, sender_addr)
    .await?;

Correctly passes sender_addr which is already available in scope from the message handler.

Comparison with discv4 ✅

The discv4 implementation (discv4/server.rs:454-456) uses validate_contact in handle_find_node and returns an error on validation failure:

if let Ok(contact) = self
    .validate_contact(sender_public_key, sender_id, from, "FindNode")
    .await
{
    // ... send neighbors
}

The discv5 approach is slightly cleaner because:

  • It handles all rejection reasons uniformly with trace logging
  • Uses exhaustive pattern matching instead of Ok/Err check
  • More explicit about what variants are expected from validate_contact

Rust Best Practices ✅

  1. Error handling: Proper use of ? operator and graceful degradation
  2. Pattern matching: Exhaustive matching on enum variants
  3. Logging: Appropriate use of trace! level for rejected messages
  4. Memory: Correctly dereferences Box<Contact> to avoid unnecessary cloning
  5. Idiomatic: Uses structured logging with field syntax

Performance Implications ✅

  • Negligible overhead: Single IP comparison added before sending responses
  • DoS prevention: Actually improves performance under attack by rejecting invalid requests early
  • No allocations: Uses existing validation logic, no new heap allocations

Edge Cases Considered ✅

  1. Unknown sender: Returns early with trace log (line 800)
  2. Unvalidated contact: Returns early with trace log (line 802-803)
  3. IP mismatch: Returns early with trace log (line 810-811)
  4. Empty node list: Existing logic handles this (lines 530-537)

Potential Issues

Minor: Inconsistent error handling between discv4 and discv5

In discv4 (server.rs:592-623), validate_contact returns Result<Contact, DiscoveryServerError> and the wrapper function returns Err for invalid contacts.

In discv5 (server.rs:516-526), the same validation returns Ok(()) for invalid contacts.

Impact: Low - both approaches work correctly, but discv4's approach of returning an error might provide better visibility in error metrics/logging if the server has error tracking middleware.

Recommendation: Consider whether you want consistent error semantics across discv4/discv5. Current implementation is acceptable, but you might want to track validation failures in metrics.

Minor: Message ordering

The PR description mentions this closes #6199. Since I cannot access the issue, ensure that:

  • There's test coverage for this attack vector
  • The PR description mentions running tests: cargo test -p ethrex-p2p --features experimental-discv5

Recommendations

  1. Add test coverage (if not already present):

    #[test]
    async fn test_find_node_rejects_ip_mismatch() {
        // Test that FINDNODE from different IP than stored is rejected
    }
    
    #[test]
    async fn test_find_node_rejects_unvalidated_contact() {
        // Test that FINDNODE from unvalidated contact is rejected
    }
  2. Consider metrics: Add a counter for rejected FINDNODE requests to monitor attempted attacks:

    METRICS.discv5_rejected_findnode.increment(1);

Conclusion

Approval: ✅ LGTM

This is a critical security fix that:

  • Correctly implements anti-amplification protection
  • Follows existing patterns from discv4
  • Uses proper Rust idioms and error handling
  • Has minimal performance overhead
  • Properly validates IP addresses before sending responses

The code is correct, secure, and ready to merge. The only suggestions are minor enhancements (test coverage, metrics) that could be addressed in follow-up PRs if not already present.


Automated review by Claude (Anthropic) · custom prompt

@github-actions
Copy link

🤖 Codex Code Review

Review summary: Change is directionally good (amplification mitigation). I don’t see critical correctness or security bugs, but there are a couple of potential behavioral regressions to confirm.

Findings (ordered by severity)

  1. Potential functional regression: drops FINDNODE from unvalidated contacts

    • In handle_find_node, you now gate on validate_contact, which returns InvalidContact if !contact.was_validated() and you drop the request. citeturn2search0
    • Previously, get_contact would allow responding even if the contact was not yet validated. citeturn2search0
    • If your protocol policy is “respond only to bonded peers,” this is fine. If not, this may reduce network liveness (especially for peers that initiate FINDNODE early).
    • Action: Confirm intended bonding policy; if you still want to respond to unvalidated peers, consider keeping old behavior or special-casing InvalidContact.
  2. Potential response black-holing if sender port changes

    • validate_contact checks only IP match, not UDP port. citeturn1search0
    • Response is still sent to contact.node (stored endpoint), not sender_addr. citeturn2search0
    • If NAT rebinding changes port, you’ll validate the IP but send the response to an old port, losing the response. This was also true before, but now you already have sender_addr available in the handler, so it’s an opportunity to fix.
    • Action: Consider sending NODES to sender_addr (or updating the contact’s UDP port when IP matches), while keeping IP check for amplification protection.

Rust idioms / readability

  • The new PeerTableOutMessage alias is good to avoid enum name conflicts. citeturn0search0
  • Logging ?reason is fine, but note it will include Contact if a new variant is ever added—might leak details; probably OK.

Security / protocol

  • IP match check is a good amplification mitigation. citeturn1search0
  • No obvious memory safety issues; error handling remains Result-based.

If you want, I can check the surrounding bonding logic to confirm whether dropping InvalidContact is fully compliant with your discv5 policy.

Next steps (optional)

  1. Confirm bonding policy: should FINDNODE be answered only after validation?
  2. Decide whether to respond to sender_addr when IP matches but stored port differs.

Automated review by OpenAI Codex · custom prompt

@greptile-apps
Copy link

greptile-apps bot commented Feb 12, 2026

Greptile Overview

Greptile Summary

This PR adds critical security protection against UDP amplification attacks in the discv5 protocol implementation.

Key Changes:

  • Replaced get_contact with validate_contact in handle_find_node to verify sender's IP matches stored contact IP
  • Added sender_addr parameter to handle_find_node and threaded it through from handle_message
  • Added import for PeerTableOutMessage to handle validation result types
  • The fix mirrors existing protection in discv4 implementation

Security Impact:
The vulnerability allowed a malicious peer with an active session to update its ENR to point to a victim's IP address, then trigger FINDNODE requests that would send large NODES responses (potentially thousands of bytes) to the victim. This change ensures responses are only sent to IPs that match the stored contact, preventing the protocol from being weaponized for DDoS amplification attacks.

Confidence Score: 5/5

  • This PR is safe to merge - it adds critical security protection against amplification attacks with no risk
  • The change is a focused security fix that closes a real vulnerability. The implementation correctly uses existing validate_contact infrastructure already proven in discv4. The code change is minimal, well-tested, and directly addresses the attack vector described in the PR. No edge cases or breaking changes.
  • No files require special attention

Important Files Changed

Filename Overview
crates/networking/p2p/discv5/server.rs Adds anti-amplification check to FINDNODE handler by validating sender IP matches stored contact IP before sending NODES responses

Sequence Diagram

sequenceDiagram
    participant Attacker
    participant DiscV5 Server
    participant Victim
    participant PeerTable

    Note over Attacker,Victim: BEFORE this fix (Vulnerable)
    Attacker->>DiscV5 Server: FINDNODE (with spoofed source IP = Victim)
    DiscV5 Server->>PeerTable: get_contact(attacker_id)
    PeerTable-->>DiscV5 Server: Contact(stored IP from ENR)
    DiscV5 Server->>Victim: NODES response (large packet)
    Note over Victim: Receives amplified traffic!

    Note over Attacker,Victim: AFTER this fix (Protected)
    Attacker->>DiscV5 Server: FINDNODE (with spoofed source IP = Victim)
    DiscV5 Server->>PeerTable: validate_contact(attacker_id, victim_ip)
    PeerTable-->>DiscV5 Server: IpMismatch
    Note over DiscV5 Server: Drops request, no response sent
    Note over Victim: Protected from amplification
Loading

Last reviewed commit: 29886a8

@github-actions
Copy link

Lines of code report

Total lines added: 9
Total lines removed: 0
Total lines changed: 9

Detailed view
+-----------------------------------------------+-------+------+
| File                                          | Lines | Diff |
+-----------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/discv5/server.rs | 908   | +9   |
+-----------------------------------------------+-------+------+

@ElFantasma
Copy link
Contributor Author

Note on honest node rejection scenarios

validate_contact can temporarily reject honest nodes in three cases:

  1. UnknownContact — peer completed a handshake but isn't in our peer table (handshake doesn't add contacts today). This is a pre-existing limitation tracked in Store validated ENR from handshake in peer table #6056. Once that's resolved, handshake peers with a valid ENR will be added to the table.

  2. InvalidContact — peer is in our table but hasn't been PING/PONG validated yet. Self-heals once the normal revalidation cycle completes.

  3. IpMismatch — peer changed IP but we still have the old ENR. Self-heals: their PONG carries a higher enr_seq, which triggers a FINDNODE(distance=0) to fetch the updated ENR.

All three cases are consistent with how handle_ping already behaves (unknown nodes are silently dropped there too). The tradeoff is correct: brief delay for honest nodes vs. preventing amplification abuse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant