Fix: Circuit Relay v2 connect destination peer without active reservation.#1343
Fix: Circuit Relay v2 connect destination peer without active reservation.#1343sumanjeet0012 wants to merge 6 commits into
Conversation
… and separate source-side connection limits.
acul71
left a comment
There was a problem hiding this comment.
AI PR Review: #1343 — Fix Circuit Relay v2 connect destination peer without active reservation
PR: #1343
Author: @sumanjeet0012
Reviewed branch: relay-fix (checked out locally)
Related issue: #1342
Review date: 2026-06-05
1. Summary of Changes
This PR fixes a security-relevant bug in Circuit Relay v2 (libp2p/relay/circuit_v2/). In _handle_connect(), the relay admission gate checked the source peer's reservation (source_addr) instead of the destination peer's reservation (peer_id). That allowed a source with its own reservation to reach any relay-connected destination that had never sent RESERVE, bypassing the protocol's opt-in model described in #1342.
Changes:
| File | Change |
|---|---|
libp2p/relay/circuit_v2/protocol.py |
Destination reservation enforced via can_accept_connection(peer_id=peer_id); source connection limits enforced separately; new NO_RESERVATION rejection path |
libp2p/relay/circuit_v2/protocol_buffer.py |
Adds StatusCode.NO_RESERVATION = 103 |
newsfragments/1342.bugfix.rst |
User-facing release note |
tests/core/relay/test_circuit_v2_transport.py |
Integration test updated to have destination make a RESERVE before source dials |
Architecture context: Affects the relay hop protocol handler (CircuitV2Protocol) and its interaction with RelayResourceManager reservation tracking. No breaking API changes to public host/transport interfaces.
Breaking changes: None for public APIs. Wire behavior change: CONNECT requests to destinations without reservations are now correctly rejected (previously incorrectly accepted in some cases).
2. Branch Sync Status and Merge Conflicts
Branch Sync Status
- Status: ℹ️ Ahead of
origin/main(PR commits only; no divergence) - Details:
0 3— branch is 0 commits behind and 3 commits ahead oforigin/main- 2 feature commits from author + 1 merge commit (
Merge branch 'main' into relay-fixby @acul71)
- 2 feature commits from author + 1 merge commit (
Merge Conflict Analysis
✅ No merge conflicts detected. The PR branch merges cleanly into origin/main.
3. Strengths
- Correct root-cause fix: Swapping
source_addr→peer_idin the destination admission check directly addresses the vulnerability described in #1342. - Clear separation of concerns: Destination reservation requirement and source per-reservation connection limits are now distinct checks with appropriate status codes and messages.
- Semantically accurate status code: Adding
NO_RESERVATIONis aligned with the Circuit Relay v2 spec semantics (even though py-libp2p's numeric values differ from proto3 spec values — see Issues Found). - Newsfragment present:
newsfragments/1342.bugfix.rstcorrectly references issue #1342 with user-facing wording. - Test fix exposes prior test gap: The transport integration test now performs a destination
RESERVEstep, reflecting real protocol usage (destination must opt in before being reachable). - CI green: All GitHub Actions checks pass (core, lint, interop, docs, Windows matrix).
- Local validation:
make lint,make typecheck,make test, andmake linux-docsall pass on the checked-out branch.
4. Issues Found
Critical
None. The core fix is correct and addresses the reported vulnerability.
Major
-
File:
tests/core/relay/test_circuit_v2_transport.py -
Line(s): 377–390 (new reservation step); no corresponding negative test
-
Issue: The test update makes the happy path protocol-correct but does not assert that CONNECT is rejected when the destination lacks a reservation. Before this PR, the integration test implicitly relied on the buggy behavior (no destination
RESERVEyet dial succeeded). A dedicated negative test would lock in the fix and prevent regression. -
Suggestion: Add a test (in
test_circuit_v2_protocol.pyortest_circuit_v2_transport.py) that sends a CONNECT for a destination without reservation and asserts the relay responds withStatusCode.NO_RESERVATIONand resets the stream. -
File: GitHub PR description
-
Line(s): N/A
-
Issue: Issue #1342 is referenced narratively ("Issue #1342") but the PR body does not use a closing keyword (
Fixes #1342,Closes #1342, etc.). This prevents automatic issue closure on merge and is inconsistent with project convention. -
Suggestion: Add
Fixes #1342to the PR description.
Minor
- File:
libp2p/relay/circuit_v2/protocol.py - Line(s): 704–705
- Issue: Source connection limit check accesses the private attribute
self.resource_manager._reservationsdirectly instead of a public API onRelayResourceManager(which already exposescan_accept_connection,has_reservation, etc.). - Suggestion: Add a small public helper (e.g.
get_reservation(peer_id) -> Reservation | Noneorcan_source_accept_connection(peer_id) -> bool) to avoid reaching into_reservations.
libp2p/relay/circuit_v2/protocol.py — lines 703–705
# Separately enforce the source peer's per-reservation connection limit.
source_reservation = self.resource_manager._reservations.get(source_addr)
if source_reservation and not source_reservation.can_accept_connection():-
File:
libp2p/relay/circuit_v2/pb/circuit.proto -
Line(s): 44–54
-
Issue:
NO_RESERVATIONis added to the PythonStatusCodeIntEnum (103) but not to the protobufStatus.Codeenum.create_status()casts the int directly onto the protobuf object, so this works at runtime, but the.protodefinition remains out of sync with both the Python enum and the updated spec enum values (NO_RESERVATION = 204in proto3). Pre-existing drift, but this PR introduces a new code only in the Python layer. -
Suggestion: Consider updating
circuit.proto(and regenerating pb2) in a follow-up; document the numeric mapping if interop with go/rust/js implementations is a goal. -
File: PR author To-Do list (PR body)
-
Line(s): N/A
-
Issue: Author notes remaining tasks: commit history cleanup and documentation updates are unchecked.
-
Suggestion: Squash or rebase commits before merge; no user-facing docs strictly required for this internal fix, but a brief note in relay module docstring or
docs/examples.circuit_relayabout destination reservation being mandatory would help.
5. Security Review
This PR closes a security vulnerability rather than introducing one.
- Risk (pre-fix): Unauthorized relayed delivery — any source peer with a reservation could reach destinations that never opted in via
RESERVE, turning the relay into an unsolicited traffic vector. - Impact (pre-fix): High — violates Circuit Relay v2 opt-in model; enables reaching peers without consent.
- Mitigation (this PR): Destination reservation is now enforced before CONNECT proceeds. ✅
Post-fix assessment:
- Risk: Residual — no negative test means regression could go unnoticed; status code numeric mismatch with other implementations could cause confusing interop errors (not a bypass).
- Impact: None from the fix itself.
- Mitigation: Add negative test; consider proto alignment for wire compatibility.
6. Documentation and Examples
| Item | Status |
|---|---|
Newsfragment (1342.bugfix.rst) |
✅ Present, user-facing, ends with newline |
| Issue reference | Fixes #1342 keyword |
| Module docstrings / API docs | ❌ Not updated (acceptable for small bugfix; author flagged as TODO) |
Examples (docs/examples.circuit_relay) |
❌ Not updated — existing examples may already show reservation flow |
No blocking documentation gaps for a targeted bugfix, but a negative-path test would serve as living documentation of the security invariant.
7. Newsfragment Requirement
✅ Satisfied.
- Issue linked: #1342 (referenced in PR body and commit messages)
- Newsfragment file:
newsfragments/1342.bugfix.rst - Type:
bugfix— correct for this change - Content: User-facing, describes impact without implementation detail
- Newline: Present
Non-blocker: Add Fixes #1342 to PR body for auto-close on merge.
8. Tests and Validation
New / Updated Tests
test_circuit_v2_transport.py: DestinationRESERVEstep added before source dial — fixes the test to match correct protocol flow.- Gap: No test for CONNECT rejection when destination has no reservation.
Linting (make lint)
- Exit code: 0 ✅
- Result: All pre-commit hooks passed (ruff, mypy-local, pyrefly-local, mdformat, path audit, etc.)
- Errors/Warnings: None
Type Checking (make typecheck)
- Exit code: 0 ✅
- Result: mypy and pyrefly passed
- Errors/Warnings: None
Test Execution (make test)
- Exit code: 0 ✅
- Summary: 2795 passed, 16 skipped in ~105s
- Failures/Errors: None
- Notable warnings: Standard pytest duration report only; no relay-related failures
Documentation Build (make linux-docs)
- Exit code: 0 ✅
- Result: Sphinx HTML build succeeded (104 sources,
-Wwarnings-as-errors mode) - Errors/Warnings: None fatal
CI (GitHub Actions)
All checks pass including tox (3.10–3.13, core/lint/interop/docs), Windows matrix, and Read the Docs preview.
9. Recommendations for Improvement
- Add a negative unit/integration test asserting
NO_RESERVATIONwhen destination has no active reservation — highest-value follow-up. - Add
Fixes #1342to the PR description. - Replace
_reservationsdirect access with a publicRelayResourceManagermethod. - Clean up commit history (squash the two fix commits; drop or clarify the merge commit) per author's own TODO.
- Optional follow-up: Align
circuit.protoStatus enum with spec (includingNO_RESERVATION) for interop clarity.
10. Questions for the Author
- Was the absence of a negative test intentional, or would you add one asserting
StatusCode.NO_RESERVATIONbefore merge? - Should source peers without a reservation be allowed to initiate CONNECT (current behavior after this PR), or should the relay require both source and destination reservations? The pre-fix code accidentally required source reservation; this PR only mandates destination reservation per spec.
- Is
NO_RESERVATION = 103the intended wire value for py-libp2p interop, given other implementations may use204per the proto3 spec update?
11. Overall Assessment
| Metric | Rating |
|---|---|
| Quality Rating | Good |
| Security Impact | High (positive) — fixes a real opt-in bypass |
| Merge Readiness | Needs fixes — minor: negative test + PR body keyword; non-blocking: commit cleanup |
| Confidence | High |
Summary: This is a focused, correct fix for an important Circuit Relay v2 security bug (#1342). The logic change in _handle_connect() is sound, the newsfragment is in place, and all local/CI validation passes. The main gap is test coverage for the rejection path — the updated integration test would still pass even if the destination check were reverted to source_addr, so a negative test is strongly recommended before merge. With that addition and a Fixes #1342 line in the PR body, this should be ready to merge.
…e test for reservations and created get_reservation.
|
@sumanjeet0012 FULL AI REVIEW: AI PR Review: #1343 — Fix Circuit Relay v2 connect destination peer without active reservationPR: #1343 1. Summary of ChangesThis PR fixes a security-relevant bug in Circuit Relay v2 ( Fixes #1342 Since the initial review (2026-06-05), the author addressed @acul71's requested changes with three additional commits:
Architecture context: Affects the relay hop protocol handler ( Wire behavior change: CONNECT requests to destinations without active reservations are now correctly rejected with 2. Branch Sync Status and Merge ConflictsBranch Sync Status
Merge Conflict Analysis✅ No merge conflicts detected. The PR branch merges cleanly into 3. Strengths
4. Issues FoundCriticalNone. The core security fix is correct and the negative test prevents silent regression. Major
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: libp2p/relay/circuit_v2/pb/circuit.proto
# Protobuf Python Version: 4.25.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)Compare with Minor
5. Security ReviewThis PR closes a security vulnerability rather than introducing one.
Post-fix assessment:
6. Documentation and Examples
No blocking documentation gaps for a targeted security bugfix. 7. Newsfragment Requirement✅ Satisfied.
8. Tests and ValidationNew / Updated Tests
Linting (
|
| Requested change | Status |
|---|---|
Negative test for NO_RESERVATION |
✅ Addressed |
Fixes #1342 in PR body |
✅ Addressed |
Public get_reservation() instead of _reservations access |
✅ Addressed |
Spec-aligned NO_RESERVATION = 204 in protobuf |
✅ Addressed (with protobuf version concern noted above) |
| Commit history cleanup |
Awaiting re-review from @acul71.
9. Recommendations for Improvement
- Regenerate protobuf gencode with the project's standard protoc/protobuf version (6.x, matching
main) — only add theNO_RESERVATIONenum; do not downgrade runtime version validation. - Squash or split commits before merge: isolate relay security fix from QUIC logger and redis mock changes.
- Update PR description to reflect
NO_RESERVATION = 204(not 103). - Optional: Add a test for source
RESOURCE_LIMIT_EXCEEDEDwhen source reservation slots are full. - Optional: Refactor
RelayResourceManager.can_accept_connection()to useget_reservation()internally.
10. Questions for the Author
- Was the protobuf gencode regenerated with a local protoc 4.x install? Can you regenerate with the same toolchain as
main(6.32.1) to avoid removing_runtime_version.ValidateProtobufRuntimeVersion? - Should the QUIC logger fix and redis mock be moved to a separate PR, or squashed into a clearly labeled "CI/misc" commit?
- Is requiring only destination reservation (not source) the intended long-term behavior? This matches the Circuit Relay v2 spec, but differs from the pre-fix accidental behavior.
11. Overall Assessment
| Metric | Rating |
|---|---|
| Quality Rating | Good |
| Security Impact | High (positive) — fixes a real opt-in bypass |
| Merge Readiness | Needs re-review — substantive maintainer feedback addressed; protobuf gencode downgrade should be fixed before merge |
| Confidence | High |
Summary: This is a focused, correct fix for an important Circuit Relay v2 security bug (#1342). The author responded thoroughly to @acul71's review: negative regression test, public get_reservation() API, spec-aligned status code 204, and protobuf enum update. All CI checks pass. The remaining concern is protobuf gencode regeneration that downgrades from 6.32.1 to 4.25.1 — this should be corrected to match the rest of the repository before merge. Unrelated QUIC and redis changes should ideally be cleaned up in commit history. Once the protobuf tooling issue is resolved and @acul71 re-approves, this PR should be ready to merge.
What was wrong?
Fixes #1342
In
CircuitV2Protocol._handle_connect(), the admission gate that should verify the destination peer has an active reservation was accidentally checking the source peer's reservation instead:peer_idis the destination (frommsg.peer);source_addris the source (fromstream.muxed_conn.peer_id). Passingsource_addrmeant: if the source has a reservation, the check passes — regardless of whether the destination ever reserved. A source peer could therefore reach any relay-connected destination that had never opted in.How was it fixed?
The single incorrect gate was replaced with two clearly-separated checks:
can_accept_connection(peer_id=peer_id): rejects withNO_RESERVATIONif the destination has no active reservation.source_addr: rejects withRESOURCE_LIMIT_EXCEEDEDif the source has exhausted its own slot count.NO_RESERVATION = 103was also added toStatusCodeso the rejection path has a semantically accurate status code.To-Do