Skip to content

Harden peer handshake — strict pstrlen + magic check + state machine#4

Merged
iksnerd merged 1 commit into
mainfrom
langsec-peer-wire
May 2, 2026
Merged

Harden peer handshake — strict pstrlen + magic check + state machine#4
iksnerd merged 1 commit into
mainfrom
langsec-peer-wire

Conversation

@iksnerd
Copy link
Copy Markdown
Owner

@iksnerd iksnerd commented May 2, 2026

Summary

Closes the last LangSec gap in the BitTorrent client. The BEP 3 handshake previously read pstrlen bytes blindly without validating the protocol magic string, and PeerConn had no explicit state guarding which methods were valid when. Now the recognizer enforces the full 68-byte handshake grammar before any state mutation, and a typed state field gates downstream calls.

This is Priority 3 of 3 from the LangSec analysis. P1 (announce recognizer, #2) and P2 (bencode validator, #3) are independent.

What's new

Strict handshake recognition (Handshake in internal/client/peer.go):

  • Read the peer's full 68 bytes in one io.ReadFull, then validate the entire structure before promoting any state.
  • pstrlen must be exactly 19 (length of "BitTorrent protocol" per BEP 3). The previous code rejected only pstrlen == 0 and accepted any other value.
  • Magic string must be exactly "BitTorrent protocol". Previously the bytes were read and indexed past without a single byte of comparison — a peer speaking a 19-byte non-BitTorrent protocol would have passed.
  • Info-hash check unchanged.

Typed state machine (peerState enum):

State Set by Required by
stateInit Connect Handshake
stateHandshook Handshake (post-validation) ReadMessage, WriteMessage
stateExtended Handshake (after BEP 10 ext handshake) RequestMetadata

Each precondition returns a typed error on misuse — calling RequestMetadata before the BEP 10 handshake or ReadMessage on a fresh connection now fails with RequestMetadata called in state N (expected extended) instead of writing/reading garbage on the wire.

Test plan

  • go test ./... — all packages green
  • go vet ./... — clean
  • Existing TestHandshake and TestHandshakeMismatch continue to pass (they always sent valid magic + pstrlen=19).
  • New tests:
    • TestHandshakeRejectsBadPstrlen — pstrlen=18 rejected with typed reason
    • TestHandshakeRejectsBadMagic — pstr="WrongTorrent magic!" (correct length, wrong content) rejected
    • TestReadMessageBeforeHandshakeRejectedReadMessage/WriteMessage in stateInit rejected
    • TestRequestMetadataBeforeExtendedRejectedRequestMetadata in stateHandshook rejected
  • Reviewer: confirm peerState is the right shape — single int with < comparisons, no exposed transition methods. Could be replaced with an interface-based state pattern, but felt over-engineered for 3 states.

🤖 Generated with Claude Code

…chine

Closes the last LangSec gap in the BitTorrent client: the BEP 3 handshake
previously read pstrlen bytes blindly without validating the protocol magic
string, and PeerConn had no explicit state guarding which methods were valid
when. Now the recognizer enforces the full handshake grammar before any
state mutation and a typed state field gates downstream calls.

- internal/client/peer.go:
  - protoMagic constant + handshakeLen = 68 (BEP 3 mandates exactly this).
  - Read the peer's full 68-byte handshake in one ReadFull, then validate:
    pstrlen == 19, pstr == "BitTorrent protocol", info_hash matches. Only
    then promote state to stateHandshook (and stateExtended after BEP 10).
  - peerState enum {Init, Handshook, Extended}. Handshake requires Init.
    ReadMessage/WriteMessage require >= Handshook. RequestMetadata requires
    Extended. Each precondition returns a typed error on misuse.
- internal/client/peer_test.go: new tests for bad pstrlen, wrong magic,
  ReadMessage/WriteMessage before handshake, RequestMetadata before
  extended handshake. fakePeer helper for handshake-rejection cases.
@iksnerd iksnerd merged commit 8f86b11 into main May 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant