ssu2path

package module
v0.1.59999 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 8, 2026 License: MIT Imports: 11 Imported by: 0

README

go-i2p/path

SSU2 path management for the I2P network in Go. This package (ssu2path) implements NAT traversal, relay coordination, path validation, and peer testing for I2P's SSU2 (Secure Semi-reliable UDP v2) transport protocol.


Requirements

  • Go 1.26.1 or later
  • github.com/go-i2p/go-noise (local replace directive required — see go.mod)

Installation

go get github.com/go-i2p/path

Because the module uses a replace directive for github.com/go-i2p/go-noise, clone both repositories under the same parent directory:

git clone https://github.com/go-i2p/go-noise
git clone https://github.com/go-i2p/path
cd path
go build ./...

Usage

HolePunchCoordinator

Coordinates UDP hole punching with state tracking, retries, and signature verification. A PendingSessionRegistry (e.g. *RelayManager) must be provided at construction.

coordinator := ssu2path.NewHolePunchCoordinator(relayManager, verifyFn)
defer coordinator.Stop() // REQUIRED — stops the background cleanup goroutine

sessionID, err := coordinator.InitiateHolePunch(remoteAddr, introducerAddr, relayTag)
RelayManager

Manages relay tag allocation, introducer registration, and pending sessions for NAT traversal.

manager := ssu2path.NewRelayManager(listener)
defer manager.Stop() // REQUIRED

tag, err := manager.AllocateRelayTag(peerAddr)
err = manager.AddPendingSession(sessionID, remoteAddr, introducerAddr, relayTag)
PeerTestManager

Implements the seven-message NAT testing protocol to determine NAT type and external reachability.

ptm := ssu2path.NewPeerTestManager(listener)
defer ptm.Stop() // REQUIRED

nonce, err := ptm.InitiatePeerTest(bobAddr)
result := ptm.GetResult(remoteAddr)
PathValidator

Validates connection migration to a new UDP path using Path Challenge (Type 18) and Path Response (Type 19) blocks.

validator := ssu2path.NewPathValidator(conn, tokenCache, congestionController)
defer validator.Stop() // REQUIRED

err := validator.InitiatePathValidation(newAddr)
IntroducerRegistry

Maintains up to three introducers (per I2P specification) for RouterInfo publication.

registry := ssu2path.NewIntroducerRegistry(3)
err := registry.AddIntroducer(addr, routerHash, staticKey, introKey, relayTag)
introducers := registry.GetIntroducers()
NAT Detection Helpers

Stateless utilities for analyzing peer test results:

if ssu2path.IsPortConsistent(addr1, addr2) { /* NAT preserves port */ }
if ssu2path.IsIPConsistent(addr1, addr2)   { /* same external IP */ }
if ssu2path.IsDirectlyReachable(result)    { /* no restrictive NAT */ }
if ssu2path.IsReachableViaRelay(result)    { /* relay path works */ }

external := ssu2path.ExtractExternalAddress(result)
port     := ssu2path.ExtractExternalPort(result)
Path Challenge / Response Blocks
challengeBlock := ssu2path.EncodePathChallenge(challengeID)
responseBlock  := ssu2path.EncodePathResponse(challengeID)

id, err := ssu2path.DecodePathChallenge(block)
id, err  = ssu2path.DecodePathResponse(block)

Features

  • UDP Hole PunchingHolePunchCoordinator manages hole punch attempts with a state machine (Requested → Sent → Waiting → Success/Failed), up to 3 retries, and mandatory Ed25519 signature verification per SSU2 spec
  • Relay ManagementRelayManager allocates cryptographically random relay tags, tracks introducers with 1-hour expiry, and manages pending sessions across concurrent goroutines
  • NAT Type Detection — Classifies NAT as None, Full Cone, Restricted Cone, Port-Restricted Cone, or Symmetric based on probe results
  • Seven-Message Peer TestingPeerTestManager drives the full SSU2 peer test protocol (Alice ↔ Bob ↔ Charlie) to measure external reachability
  • Path ValidationPathValidator safely migrates connections to new UDP paths using cryptographic challenge/response, with optional token cache invalidation and congestion window reset
  • Introducer Registry — Keeps up to 3 fresh introducers sorted by last-seen time for RouterInfo publication
  • Wire Format Codecs — Encode/decode for relay blocks (Types 7, 8, 9, 15, 16), peer test blocks (Type 10, messages 1–7), and path blocks (Types 18, 19)
  • Thread Safety — All public methods on all managers are safe for concurrent use
  • Resource Safety — Background cleanup goroutines require explicit Stop() calls; Stop() is idempotent on all types

Resource Management

Three types start background goroutines that must be stopped explicitly:

Type Cleanup Interval
HolePunchCoordinator 30 seconds
PeerTestManager 60 seconds
RelayManager Timer-based

Always pair construction with a deferred Stop():

mgr := ssu2path.NewRelayManager(listener)
defer mgr.Stop()

License

MIT License — Copyright (c) 2026 I2P For Go

Documentation

Overview

Package ssu2 provides SSU2-specific implementations for the Noise Protocol Framework supporting I2P's SSU2 transport protocol with UDP-based connections and NAT traversal.

Resource Management

This package creates managers with background goroutines for cleanup and maintenance:

  • HolePunchCoordinator (cleanup every 30s)
  • PeerTestManager (cleanup every 60s)
  • RelayManager (cleanup timer-based)

IMPORTANT: Callers MUST call Stop() on these managers when done to prevent goroutine leaks. Failure to call Stop() will cause background goroutines to run until process termination.

Index

Constants

View Source
const (
	// PathValidationTimeout is how long to wait for path response.
	// BUG-L04: 10 seconds chosen per I2P SSU2 spec §Connection Migration.
	// Shorter than hole punch (30s) and peer test (60s) because path
	// validation occurs mid-connection and requires tighter feedback.
	PathValidationTimeout = 10 * time.Second

	// PathValidationCleanupInterval is how often to clean up expired challenges.
	// BUG-L04: 30 seconds = 3× PathValidationTimeout, balancing cleanup cost
	// against memory pressure from abandoned challenges (added in BUG-M07 fix).
	PathValidationCleanupInterval = 30 * time.Second
)

Path validation timeouts

View Source
const (
	// MinMTU is the minimum MTU for SSU2 (spec-defined floor).
	MinMTU = 1280

	// MaxMTU is the upper bound for MTU probing.
	MaxMTU = 1500

	// MTUProbeStep is the size increment between probe steps.
	MTUProbeStep = 20
)

MTU probing constants (G-5).

View Source
const (
	// RelayRequestPrologue is prepended to signed data for relay requests.
	// 16 bytes, not null-terminated.
	RelayRequestPrologue = "RelayRequestData"

	// RelayAgreementPrologue is prepended to signed data for relay responses.
	// 16 bytes, not null-terminated.
	RelayAgreementPrologue = "RelayAgreementOK"
)

Relay signature prologues per SSU2 spec §Relay Request and §Relay Response.

View Source
const (
	BlockTypeRelayRequest    = wire.BlockTypeRelayRequest
	BlockTypeRelayResponse   = wire.BlockTypeRelayResponse
	BlockTypeRelayIntro      = wire.BlockTypeRelayIntro
	BlockTypePeerTest        = wire.BlockTypePeerTest
	BlockTypeAddress         = wire.BlockTypeAddress
	BlockTypePathChallenge   = wire.BlockTypePathChallenge
	BlockTypePathResponse    = wire.BlockTypePathResponse
	BlockTypeRelayTag        = wire.BlockTypeRelayTag
	BlockTypeNewToken        = wire.BlockTypeNewToken
	BlockTypeRelayTagRequest = wire.BlockTypeRelayTagRequest
)

Block type constants needed by path types

View Source
const HolePunchSessionTimeout = 30 * time.Second

HolePunchSessionTimeout is the maximum lifetime of a pending hole-punch session. Shared by HolePunchCoordinator and RelayManager cleanup loops so both sides age out sessions at the same rate. L-04 fix: single definition replaces the two duplicated 30*time.Second literals.

View Source
const MaxClockSkew = 5 * time.Minute

MaxClockSkew is the maximum allowed age (or future offset) of a signed timestamp. Blocks outside this window are rejected to prevent replay attacks.

View Source
const PeerTestPrologue = "PeerTestValidate"

PeerTestPrologue is prepended to signed data for peer test messages. 16 bytes, not null-terminated, per SSU2 spec §Peer Test.

View Source
const RelayResponseTokenTTL = 10 * time.Second

RelayResponseTokenTTL is the maximum lifetime for a relay response token. Per SSU2 spec, "The token must be used immediately by Alice in the Session Request." We enforce a 10-second window to allow for network latency while still requiring near-immediate use.

Variables

View Source
var NewSSU2Block = wire.NewSSU2Block

Constructor alias

Functions

func BuildPeerTestSignedData

func BuildPeerTestSignedData(
	bobHash data.Hash,
	aliceHash *data.Hash,
	version uint8,
	nonce, timestamp uint32,
	alicePort uint16,
	aliceIP net.IP,
) ([]byte, error)

BuildPeerTestSignedData constructs the data to be signed for a peer test message.

Per SSU2 spec §Peer Test, the signed data is:

  • prologue: 16 bytes "PeerTestValidate"
  • bhash: Bob's 32-byte router hash
  • ahash: Alice's 32-byte router hash (only for messages 3 and 4)
  • ver: 1 byte
  • nonce: 4 bytes
  • timestamp: 4 bytes
  • asz: 1 byte (6 or 18)
  • AlicePort: 2 bytes
  • AliceIP: (asz-2) bytes

Set aliceHash to nil for messages 1 and 2.

func BuildRelayRequestSignedData

func BuildRelayRequestSignedData(
	bobHash, charlieHash data.Hash,
	nonce, relayTag, timestamp uint32,
	version uint8,
	alicePort uint16,
	aliceIP net.IP,
) ([]byte, error)

BuildRelayRequestSignedData constructs the data to be signed for a relay request.

Per SSU2 spec §Relay Request, the signed data is:

  • prologue: 16 bytes "RelayRequestData"
  • bhash: Bob's 32-byte router hash
  • chash: Charlie's 32-byte router hash
  • nonce: 4 bytes
  • relay tag: 4 bytes
  • timestamp: 4 bytes
  • ver: 1 byte
  • asz: 1 byte (6 for IPv4, 18 for IPv6)
  • AlicePort: 2 bytes
  • AliceIP: (asz-2) bytes

func BuildRelayResponseSignedData

func BuildRelayResponseSignedData(
	bobHash data.Hash,
	nonce, timestamp uint32,
	version uint8,
	charliePort uint16,
	charlieIP net.IP,
) ([]byte, error)

BuildRelayResponseSignedData constructs the data to be signed for a relay response.

Per SSU2 spec §Relay Response, the signed data is:

  • prologue: 16 bytes "RelayAgreementOK"
  • bhash: Bob's 32-byte router hash
  • nonce: 4 bytes
  • timestamp: 4 bytes
  • ver: 1 byte
  • csz: 1 byte (0, 6, or 18)
  • CharliePort: 2 bytes (not present if csz is 0)
  • CharlieIP: (csz-2) bytes (not present if csz is 0)

func CompareNATTypes

func CompareNATTypes(nat1, nat2 NATType) int

CompareNATTypes determines if one NAT type is more restrictive than another.

Returns:

  • -1 if nat1 is less restrictive than nat2
  • 0 if equal restrictiveness
  • +1 if nat1 is more restrictive than nat2

Restrictiveness order (least to most): NATNone < NATCone < NATRestricted < NATPortRestricted < NATSymmetric NATUnknown is incomparable (returns 0)

func DecodePathChallenge

func DecodePathChallenge(block *SSU2Block) (uint64, error)

DecodePathChallenge decodes a Path Challenge block.

Parameters:

  • block: SSU2Block with Type 18

Returns:

  • uint64: The challenge ID
  • error: If decoding fails

func DecodePathResponse

func DecodePathResponse(block *SSU2Block) (uint64, error)

DecodePathResponse decodes a Path Response block.

Parameters:

  • block: SSU2Block with Type 19

Returns:

  • uint64: The challenge ID
  • error: If decoding fails

func DescribeNATCapabilities

func DescribeNATCapabilities(natType NATType) string

DescribeNATCapabilities returns a human-readable description of what connectivity is possible with the given NAT type.

This helps users understand the implications of their NAT type.

func ExtractExternalAddress

func ExtractExternalAddress(result *TestResult) *net.UDPAddr

ExtractExternalAddress gets the external address from a test result. Returns the ExternalAddr field, or nil if result is nil.

This is a convenience function for safe access to external address.

func ExtractExternalPort

func ExtractExternalPort(result *TestResult) uint16

ExtractExternalPort gets the external port from a test result. Returns the ExternalPort field, or 0 if result is nil.

This is a convenience function for safe access to external port.

func HasPublicIP

func HasPublicIP(natType NATType) bool

HasPublicIP checks if the NAT type indicates a public IP. No NAT or full cone NAT typically indicates public accessibility.

Returns true if NAT type is NATNone or NATCone.

func IsAddressConsistent

func IsAddressConsistent(addr1, addr2 *net.UDPAddr) bool

IsAddressConsistent checks if two addresses are completely equal. This combines IP and port consistency checking.

Returns true if both addresses exist and are equal, false otherwise (including nil addresses).

func IsDirectlyReachable

func IsDirectlyReachable(result *TestResult) bool

IsDirectlyReachable checks if a peer is directly reachable based on test results.

A peer is considered directly reachable if the direct probe succeeded, indicating no restrictive NAT/firewall blocking incoming connections.

Returns true if result exists and direct probe succeeded.

func IsIPConsistent

func IsIPConsistent(addr1, addr2 *net.UDPAddr) bool

IsIPConsistent checks if two addresses use the same IP. This is used to detect multiple NATs or proxies in the path.

Returns true if both addresses exist and have equal IPs, false otherwise (including nil addresses).

func IsPortConsistent

func IsPortConsistent(addr1, addr2 *net.UDPAddr) bool

IsPortConsistent checks if two addresses use the same port. This is used to determine if NAT preserves port mappings.

Returns true if both addresses exist and have the same port, false otherwise (including nil addresses).

func IsReachableViaRelay

func IsReachableViaRelay(result *TestResult) bool

IsReachableViaRelay checks if a peer is reachable via relay based on test results.

A peer is reachable via relay if the relayed probe succeeded, indicating the relay mechanism can establish connectivity.

Returns true if result exists and relayed probe succeeded.

func IsSymmetricNAT

func IsSymmetricNAT(natType NATType) bool

IsSymmetricNAT checks if the NAT type is symmetric. Symmetric NAT is the most restrictive type, requiring sophisticated traversal techniques.

Returns true if NAT type is NATSymmetric.

func IsValidSourcePort

func IsValidSourcePort(addr *net.UDPAddr) bool

IsValidSourcePort checks if a UDP address has a valid (non-reserved) source port.

Port 0 is reserved by IANA and must not appear in peer test messages. Accepting port 0 could allow crafted packets to bypass connectivity checks or cause subtle failures in NAT traversal logic.

Returns true if addr is non-nil and port is in the range [1, 65535].

func NonceConnectionIDs

func NonceConnectionIDs(nonce uint32) (dest, src uint64)

NonceConnectionIDs derives deterministic connection IDs from a 4-byte nonce per spec: dest = (uint64(nonce) << 32) | uint64(nonce), src = ^dest. Used for out-of-session PeerTest (messages 5-7) and HolePunch packets.

func RequiresRelay

func RequiresRelay(natType NATType) bool

RequiresRelay checks if the NAT type requires relay assistance for incoming connections.

Symmetric and port-restricted NATs typically require relay or hole punching for peer-to-peer connectivity.

Returns true if NAT type is symmetric or port-restricted.

func SignPeerTest

func SignPeerTest(
	privateKey ed25519.PrivateKey,
	bobHash data.Hash, aliceHash *data.Hash,
	version uint8,
	nonce, timestamp uint32,
	alicePort uint16,
	aliceIP net.IP,
) ([]byte, error)

SignPeerTest signs peer test data using the signer's Ed25519 private key. For messages 1/2, aliceHash should be nil. For messages 3/4, aliceHash must be provided.

func SignRelayRequest

func SignRelayRequest(
	privateKey ed25519.PrivateKey,
	bobHash, charlieHash data.Hash,
	nonce, relayTag, timestamp uint32,
	version uint8,
	alicePort uint16,
	aliceIP net.IP,
) ([]byte, error)

SignRelayRequest signs a relay request using Alice's Ed25519 private key.

func SignRelayResponse

func SignRelayResponse(
	privateKey ed25519.PrivateKey,
	bobHash data.Hash,
	nonce, timestamp uint32,
	version uint8,
	charliePort uint16,
	charlieIP net.IP,
) ([]byte, error)

SignRelayResponse signs a relay response using the signer's Ed25519 private key. For accepted responses (code 0), Charlie signs. For Bob rejections (code 1-63), Bob signs.

func ValidateTestResult

func ValidateTestResult(result *TestResult) error

ValidateTestResult checks if a TestResult has valid data.

A valid result must have: - At least one probe attempted (direct or relayed) - External address if any probe succeeded - Consistency flags properly set

Returns error if result is invalid, nil otherwise.

func VerifyPeerTestSignature

func VerifyPeerTestSignature(
	publicKey ed25519.PublicKey,
	signature []byte,
	bobHash data.Hash, aliceHash *data.Hash,
	version uint8,
	nonce, timestamp uint32,
	alicePort uint16,
	aliceIP net.IP,
) (bool, error)

VerifyPeerTestSignature verifies a peer test signature. For messages 1/2, aliceHash should be nil. For messages 3/4, aliceHash must be provided.

func VerifyRelayRequestSignature

func VerifyRelayRequestSignature(
	publicKey ed25519.PublicKey,
	signature []byte,
	bobHash, charlieHash data.Hash,
	nonce, relayTag, timestamp uint32,
	version uint8,
	alicePort uint16,
	aliceIP net.IP,
) (bool, error)

VerifyRelayRequestSignature verifies a relay request signature using Alice's Ed25519 public key.

func VerifyRelayResponseSignature

func VerifyRelayResponseSignature(
	publicKey ed25519.PublicKey,
	signature []byte,
	bobHash data.Hash,
	nonce, timestamp uint32,
	version uint8,
	charliePort uint16,
	charlieIP net.IP,
) (bool, error)

VerifyRelayResponseSignature verifies a relay response signature. For accepted responses (code 0), use Charlie's public key. For Bob rejections (code 1-63), use Bob's public key.

Types

type CongestionControllerAccessor

type CongestionControllerAccessor interface {
	Reset()
}

CongestionControllerAccessor provides congestion reset. Implemented by *ssu2/reliability.CongestionController.

type HolePunchAttempt

type HolePunchAttempt struct {
	// SessionID uniquely identifies this attempt
	SessionID uint64

	// RemoteAddr is the target peer's UDP address
	RemoteAddr *net.UDPAddr

	// Introducer is the introducer facilitating the hole punch
	Introducer *net.UDPAddr

	// State is the current state of the attempt
	State HolePunchState

	// StartTime is when the attempt was initiated
	StartTime time.Time

	// Retries is the number of retry attempts made
	Retries int

	// RelayTag is the tag for relay communication
	RelayTag uint32

	// FailureReason stores the error passed to FailHolePunch (H-03 fix).
	// Nil if the attempt succeeded or has not yet failed.
	FailureReason error
}

HolePunchAttempt represents an active hole punch operation.

type HolePunchCoordinator

type HolePunchCoordinator struct {
	// contains filtered or unexported fields
}

HolePunchCoordinator coordinates UDP hole punching for NAT traversal. It manages hole punch attempts with state tracking, retries, and timeout handling.

The HolePunch message (type 11) uses the same wire format as RelayIntro:

[Flag:1][SenderHash:32][Nonce:4][RelayTag:4][Timestamp:4][Ver:1][Asz:1][Port:2][IP:asz-2]

See RelayIntroBlock in relay_blocks.go for the encoder/decoder.

Design rationale: - Session IDs are cryptographically random 64-bit values for security - Maximum 3 retry attempts per I2P convention - 30-second timeout per attempt (I2P spec recommendation) - State machine: Requested → Sent → Waiting → Success/Failed - Signature verification is MANDATORY per SSU2 spec and must be provided at construction

Thread Safety: All public methods are thread-safe.

func NewHolePunchCoordinator

func NewHolePunchCoordinator(manager PendingSessionRegistry, verifyFn func(block *RelayIntroBlock, signerKey ed25519.PublicKey) error) (*HolePunchCoordinator, error)

NewHolePunchCoordinator creates a new HolePunchCoordinator.

L-3 fix: Returns an error instead of panicking on nil verifyFn, following Go constructor conventions. Per SSU2 spec §Hole Punch, all messages must be cryptographically authenticated, so a nil verifier is a programming error.

Parameters:

  • manager: The PendingSessionRegistry to coordinate with (typically *RelayManager)
  • verifyFn: Function to verify HolePunch message signatures (MUST NOT be nil)

Returns a new HolePunchCoordinator, or an error if verifyFn is nil.

func (*HolePunchCoordinator) CleanupExpired

func (hpc *HolePunchCoordinator) CleanupExpired()

CleanupExpired removes expired hole punch attempts. Attempts are considered expired after 30 seconds per I2P spec.

func (*HolePunchCoordinator) CompleteHolePunch

func (hpc *HolePunchCoordinator) CompleteHolePunch(sessionID uint64) error

CompleteHolePunch marks a hole punch attempt as successfully completed.

Parameters:

  • sessionID: Session identifier

Returns error if session not found.

func (*HolePunchCoordinator) FailHolePunch

func (hpc *HolePunchCoordinator) FailHolePunch(sessionID uint64, reason error) error

FailHolePunch marks a hole punch attempt as failed with a reason.

Parameters:

  • sessionID: Session identifier
  • reason: Error explaining failure

Returns error if session not found.

func (*HolePunchCoordinator) GetAttempt

func (hpc *HolePunchCoordinator) GetAttempt(sessionID uint64) *HolePunchAttempt

GetAttempt retrieves hole punch attempt information.

Parameters:

  • sessionID: Session identifier

Returns attempt info, or nil if not found.

func (*HolePunchCoordinator) GetStats

func (hpc *HolePunchCoordinator) GetStats() map[string]int

GetStats returns statistics about active hole punch attempts.

Returns a map with attempt counts by state.

func (*HolePunchCoordinator) HandleHolePunch

func (hpc *HolePunchCoordinator) HandleHolePunch(sessionID uint64, fromAddr *net.UDPAddr, block *RelayIntroBlock, signerKey ed25519.PublicKey) error

HandleHolePunch processes an incoming hole punch packet from a remote peer. Per SSU2 spec §Hole Punch, the message's signature MUST be verified before processing. The block parameter MUST NOT be nil - signature verification is mandatory per the SSU2 specification. If VerifyHolePunchSignature is not set, the message is rejected to prevent unauthenticated state transitions.

Parameters:

  • sessionID: Session identifier from the packet
  • fromAddr: Address the packet came from
  • block: The decoded RelayIntro-format block (MUST NOT be nil)
  • signerKey: Ed25519 public key of the message signer

Returns error if session not found, block is nil, or signature verification fails. BUG-M03 fix: Clarified that block parameter cannot be nil.

func (*HolePunchCoordinator) InitiateHolePunch

func (hpc *HolePunchCoordinator) InitiateHolePunch(remoteAddr, introducerAddr *net.UDPAddr, relayTag uint32) (uint64, error)

InitiateHolePunch starts a new hole punch attempt to reach a remote peer.

Design rationale: - Uses introducer to coordinate hole punch with target peer - Generates cryptographically random session ID - Registers pending session with RelayManager - 30-second timeout per I2P spec

Parameters:

  • remoteAddr: Target peer's UDP address
  • introducerAddr: Introducer's UDP address
  • relayTag: Tag for relay communication

Returns session ID on success, error otherwise.

func (*HolePunchCoordinator) ProcessHolePunchResponse

func (hpc *HolePunchCoordinator) ProcessHolePunchResponse(sessionID uint64, addr *net.UDPAddr, block *RelayIntroBlock, signerKey ed25519.PublicKey) error

ProcessHolePunchResponse processes a response to a hole punch attempt. Per SSU2 spec §Hole Punch, the response's signature MUST be verified. The block parameter MUST NOT be nil - signature verification is mandatory.

Parameters:

  • sessionID: Session identifier
  • addr: Address that responded
  • block: The decoded RelayIntro-format block (MUST NOT be nil)
  • signerKey: Ed25519 public key of the message signer

Returns error if session not found, block is nil, or signature verification fails. BUG-M03 fix: Clarified that block parameter cannot be nil.

func (*HolePunchCoordinator) RemoveAttempt

func (hpc *HolePunchCoordinator) RemoveAttempt(sessionID uint64)

RemoveAttempt removes a hole punch attempt from tracking.

Parameters:

  • sessionID: Session identifier

func (*HolePunchCoordinator) RetryHolePunch

func (hpc *HolePunchCoordinator) RetryHolePunch(sessionID uint64) error

RetryHolePunch retries a failed hole punch attempt.

Parameters:

  • sessionID: Session identifier

Returns error if session not found or max retries exceeded.

func (*HolePunchCoordinator) SendHolePunch

func (hpc *HolePunchCoordinator) SendHolePunch(sessionID uint64, targetAddr *net.UDPAddr) error

SendHolePunch sends a hole punch packet to the target address.

Parameters:

  • sessionID: Session identifier
  • targetAddr: Target peer's UDP address

Returns error if session not found or send fails.

func (*HolePunchCoordinator) SetAttemptStartTime

func (hpc *HolePunchCoordinator) SetAttemptStartTime(sessionID uint64, t time.Time)

SetAttemptStartTime sets the StartTime of an attempt (test helper).

func (*HolePunchCoordinator) SetAttemptState

func (hpc *HolePunchCoordinator) SetAttemptState(sessionID uint64, state HolePunchState)

SetAttemptState sets the State of an attempt (test helper).

func (*HolePunchCoordinator) Stop

func (hpc *HolePunchCoordinator) Stop()

Stop halts the background cleanup goroutine. Call when the coordinator is no longer needed to avoid goroutine leaks. Safe to call multiple times.

type HolePunchState

type HolePunchState int

HolePunchState represents the state of a hole punch attempt.

const (
	// HolePunchRequested indicates hole punch has been requested
	HolePunchRequested HolePunchState = iota

	// HolePunchSent indicates hole punch packet has been sent
	HolePunchSent

	// HolePunchWaiting indicates waiting for response
	HolePunchWaiting

	// HolePunchSuccess indicates hole punch succeeded
	HolePunchSuccess

	// HolePunchFailed indicates hole punch failed
	HolePunchFailed
)

func (HolePunchState) String

func (s HolePunchState) String() string

String returns human-readable state name.

type IntroducerInfo

type IntroducerInfo struct {
	// Addr is the UDP address of the introducer
	Addr *net.UDPAddr

	// RouterHash is the I2P router identity hash
	RouterHash data.Hash

	// RelayTag is the tag assigned by the introducer
	RelayTag uint32

	// ExpiresAt is when this introducer registration expires
	ExpiresAt time.Time
}

IntroducerInfo represents an available introducer for NAT traversal.

type IntroducerRegistry

type IntroducerRegistry struct {
	// contains filtered or unexported fields
}

IntroducerRegistry maintains a list of introducers for publishing in RouterInfo. Introducers help peers behind NAT establish connections through relay mechanisms.

Design rationale: - Maximum 3 introducers per I2P specification - Introducers are sorted by last seen time for freshness - Thread-safe for concurrent access - Defensive copies prevent external mutation

Thread Safety: All public methods are thread-safe.

func NewIntroducerRegistry

func NewIntroducerRegistry(maxCount int) *IntroducerRegistry

NewIntroducerRegistry creates a new IntroducerRegistry with the specified maximum count.

Parameters:

  • maxCount: Maximum number of introducers to maintain (typically 3 per I2P spec)

Returns a new IntroducerRegistry.

func (*IntroducerRegistry) AddIntroducer

func (ir *IntroducerRegistry) AddIntroducer(info *RegisteredIntroducer) error

AddIntroducer registers a new introducer or updates an existing one.

Design rationale: - Updates existing introducer if address matches - Removes oldest introducer when at capacity - Validates all required fields

Parameters:

  • info: Introducer information to register

Returns error if validation fails.

func (*IntroducerRegistry) GetCount

func (ir *IntroducerRegistry) GetCount() int

GetCount returns the current number of registered introducers.

func (*IntroducerRegistry) GetIntroducers

func (ir *IntroducerRegistry) GetIntroducers() []*RegisteredIntroducer

GetIntroducers returns all registered introducers.

Returns a defensive copy of the introducer list.

func (*IntroducerRegistry) GetMaxCount

func (ir *IntroducerRegistry) GetMaxCount() int

GetMaxCount returns the maximum number of introducers allowed.

func (*IntroducerRegistry) RemoveIntroducer

func (ir *IntroducerRegistry) RemoveIntroducer(addr *net.UDPAddr)

RemoveIntroducer removes an introducer by address. Note: This method uses swap-and-truncate for O(1) removal, which does NOT preserve the order of remaining introducers. If the removed introducer is not the last element, the last introducer will be moved to the removed position.

Parameters:

  • addr: UDP address of the introducer to remove

BUG-L03 fix: Documented order instability in removal operation.

func (*IntroducerRegistry) SelectBestIntroducers

func (ir *IntroducerRegistry) SelectBestIntroducers(count int) []*RegisteredIntroducer

SelectBestIntroducers selects the best introducers based on recency.

Design rationale: - Returns introducers sorted by most recently seen - Returns up to count introducers - Ensures fresh introducers are prioritized

Parameters:

  • count: Maximum number of introducers to select

Returns selected introducers (up to count).

func (*IntroducerRegistry) UpdateLastSeen

func (ir *IntroducerRegistry) UpdateLastSeen(addr *net.UDPAddr)

UpdateLastSeen updates the last seen time for an introducer.

Parameters:

  • addr: UDP address of the introducer

type ListenerRef

type ListenerRef interface {
	GetAddr() string
}

ListenerRef is an opaque reference to an SSU2Listener. It is stored by path types but not directly called. L-02 fix: GetAddr provides a minimal method so that wrong-type arguments are caught at compile time rather than silently accepted as interface{}.

type NATType

type NATType int

NATType represents the type of NAT detected.

const (
	// NATUnknown indicates NAT type is not yet determined
	NATUnknown NATType = iota

	// NATNone indicates no NAT (public IP)
	NATNone

	// NATCone indicates full cone NAT
	NATCone

	// NATRestricted indicates restricted cone NAT
	NATRestricted

	// NATPortRestricted indicates port-restricted cone NAT
	NATPortRestricted

	// NATSymmetric indicates symmetric NAT
	NATSymmetric
)

func DetermineNATType

func DetermineNATType(result *TestResult) NATType

DetermineNATType analyzes test results to determine NAT type.

Logic per I2P specification:

  • Both probes succeed + consistent port/IP = No NAT or Full Cone
  • Direct fails + relayed succeeds = Symmetric or Port-Restricted
  • Port inconsistent = Symmetric NAT
  • IP inconsistent = Multiple NATs or proxy

Parameters:

  • result: Test result with probe outcomes

Returns determined NAT type.

func SelectBestNATType

func SelectBestNATType(nat1, nat2 NATType) NATType

SelectBestNATType chooses the less restrictive NAT type from two options.

This is useful when multiple test results suggest different NAT types - we prefer the less restrictive interpretation to enable more connectivity options.

Returns the less restrictive NAT type, or nat1 if equal. BUG-012 fix: If one type is NATUnknown, prefer the known type.

func SelectWorstNATType

func SelectWorstNATType(nat1, nat2 NATType) NATType

SelectWorstNATType chooses the more restrictive NAT type from two options.

This is useful for conservative NAT detection - assuming the worst case ensures relay mechanisms are properly engaged.

Returns the more restrictive NAT type, or nat1 if equal. BUG-012 fix: If one type is NATUnknown, prefer the known type.

func (NATType) String

func (n NATType) String() string

String returns human-readable NAT type name.

type PathChallenge

type PathChallenge struct {
	// ChallengeID uniquely identifies this validation (8 bytes)
	ChallengeID uint64

	// NewAddr is the new UDP address being validated
	NewAddr *net.UDPAddr

	// Timestamp is when the challenge was created
	Timestamp time.Time

	// State tracks the validation progress
	State PathChallengeState

	// ProbeSize is the total packet size this challenge probes (G-5).
	// 0 for non-MTU challenges.
	ProbeSize int
	// contains filtered or unexported fields
}

PathChallenge represents an active path validation attempt.

type PathChallengeState

type PathChallengeState int

PathChallengeState represents the state of a path validation attempt.

const (
	// ChallengeSent indicates we sent a challenge, awaiting response
	ChallengeSent PathChallengeState = iota

	// ChallengeReceived indicates we received a challenge, need to respond
	ChallengeReceived

	// ChallengeValidated indicates successful bidirectional validation
	ChallengeValidated

	// ChallengeFailed indicates validation failed (timeout or error)
	ChallengeFailed
)

func (PathChallengeState) String

func (s PathChallengeState) String() string

String returns a human-readable representation of the challenge state.

type PathValidationConn

type PathValidationConn interface {
	// SendToAddress sends a block to a specific UDP address
	SendToAddress(block *SSU2Block, addr *net.UDPAddr) error

	// GetRemoteAddr returns the current remote address
	GetRemoteAddr() *net.UDPAddr

	// SetRemoteAddr updates the remote address after successful validation
	SetRemoteAddr(addr *net.UDPAddr) error
}

PathValidationConn defines the interface for sending path validation messages. This interface is implemented by SSU2Conn to allow testing with mocks.

type PathValidator

type PathValidator struct {
	// contains filtered or unexported fields
}

PathValidator implements connection migration with path validation.

Path validation allows an SSU2 connection to migrate to a new UDP path (different IP address or port) while maintaining security and preventing amplification attacks. This is useful for:

  • IP address changes (network switch, VPN, mobile roaming)
  • Port changes (NAT rebinding)
  • Failover to backup paths
  • Load balancing across multiple paths

The validation protocol uses Path Challenge (Type 18) and Path Response (Type 19) blocks to verify bidirectional connectivity on the new path before migration.

Design rationale: - Cryptographic challenge IDs prevent spoofing - Timeout-based cleanup prevents resource leaks - Thread-safe for concurrent path validations - Follows ssu2.rst specification for path validation

func NewPathValidator

func NewPathValidator(conn PathValidationConn) *PathValidator

NewPathValidator creates a new path validator for a connection.

BUG-M07 fix: Now starts a background cleanup goroutine. Callers MUST call Stop() when done to prevent goroutine leaks (see package-level documentation).

Parameters:

  • conn: The connection to manage path validation for

Returns an initialized validator.

func (*PathValidator) CleanupExpired

func (pv *PathValidator) CleanupExpired() int

CleanupExpired removes expired path validation challenges.

Challenges are expired if they're older than PathValidationTimeout and not in a terminal state (Validated or Failed).

Returns the number of challenges cleaned up.

func (*PathValidator) CompleteMTUProbe

func (pv *PathValidator) CompleteMTUProbe(challengeID uint64)

CompleteMTUProbe is called when a Path Response is received for an MTU probe challenge. Updates discoveredMTU if this probe was larger than the previously discovered value (G-5).

func (*PathValidator) FailPath

func (pv *PathValidator) FailPath(challengeID uint64, reason error)

FailPath marks a path validation as failed.

Parameters:

  • challengeID: The challenge ID to fail
  • reason: Error describing why validation failed

func (*PathValidator) GetChallenge

func (pv *PathValidator) GetChallenge(challengeID uint64) (*PathChallenge, bool)

GetChallenge returns information about a specific challenge.

Parameters:

  • challengeID: The challenge ID to look up

Returns:

  • *PathChallenge: Challenge information (defensive copy)
  • bool: Whether the challenge exists

func (*PathValidator) GetDiscoveredMTU

func (pv *PathValidator) GetDiscoveredMTU() int

GetDiscoveredMTU returns the largest validated MTU from probing, or 0 if no MTU probe has completed (G-5).

func (*PathValidator) HandlePathChallenge

func (pv *PathValidator) HandlePathChallenge(block *SSU2Block, fromAddr *net.UDPAddr) error

HandlePathChallenge processes a received Path Challenge block.

When we receive a challenge, we:

  1. Record it as ChallengeReceived
  2. Send a Path Response with the same challenge ID

Parameters:

  • block: The received Path Challenge block
  • fromAddr: The UDP address it came from

Returns error if decoding or response fails.

func (*PathValidator) HandlePathResponse

func (pv *PathValidator) HandlePathResponse(block *SSU2Block, fromAddr *net.UDPAddr) error

HandlePathResponse processes a received Path Response block.

When we receive a response:

  1. Verify it matches a pending challenge
  2. Mark the challenge as validated
  3. Complete the path migration if validation succeeds

Parameters:

  • block: The received Path Response block
  • fromAddr: The UDP address it came from

Returns error if validation fails.

func (*PathValidator) InitiateMTUProbe

func (pv *PathValidator) InitiateMTUProbe(addr *net.UDPAddr, size int) (uint64, error)

InitiateMTUProbe starts an MTU probe by sending a Path Challenge padded to the given size. If a Path Response is received for this challenge, the discovered MTU is updated (G-5).

func (*PathValidator) InitiatePathValidation

func (pv *PathValidator) InitiatePathValidation(newAddr *net.UDPAddr) (uint64, error)

InitiatePathValidation starts path validation for a new address.

This sends a Path Challenge (Type 18) block to the new address with a cryptographically random challenge ID. The peer must respond with a Path Response (Type 19) containing the same challenge ID.

Parameters:

  • newAddr: The new UDP address to validate

Returns:

  • uint64: The challenge ID for tracking this validation
  • error: If challenge creation or sending fails

func (*PathValidator) RunPMTUD

func (pv *PathValidator) RunPMTUD(ctx context.Context, addr *net.UDPAddr, low, high int) int

RunPMTUD performs Path MTU Discovery using binary search between low and high. Each step sends a padded Path Challenge and waits for a response. On success the discovered MTU is updated; on timeout (no response within PathValidationTimeout) the probe size is reduced. The final discovered MTU is returned, or MinMTU if no probe succeeded (G-4).

Parameters:

  • addr: the remote address to probe
  • low: minimum probe size (typically MinMTU, 1280)
  • high: maximum probe size (e.g. MaxPacketSizeIPv4 or MaxPacketSizeIPv6)

RunPMTUD blocks until the search completes or ctx is cancelled. BUG-L02 fix: Refactored to reduce cyclomatic complexity by extracting probe logic.

func (*PathValidator) SendPathChallenge

func (pv *PathValidator) SendPathChallenge(challengeID uint64, addr *net.UDPAddr) error

SendPathChallenge sends a Path Challenge block to the specified address.

Parameters:

  • challengeID: The 8-byte challenge identifier
  • addr: The UDP address to send to

Returns error if encoding or sending fails.

func (*PathValidator) SendPathResponse

func (pv *PathValidator) SendPathResponse(challengeID uint64, addr *net.UDPAddr) error

SendPathResponse sends a Path Response block to the specified address.

Parameters:

  • challengeID: The 8-byte challenge identifier from the Path Challenge
  • addr: The UDP address to send to

Returns error if encoding or sending fails.

func (*PathValidator) SetCongestionController

func (pv *PathValidator) SetCongestionController(cc CongestionControllerAccessor)

SetCongestionController sets the congestion controller to reset on path migration (G-7).

func (*PathValidator) SetTokenCache

func (pv *PathValidator) SetTokenCache(tc TokenCacheAccessor)

SetTokenCache sets the token cache used for invalidation when a path migrates. Per spec, tokens are bound to an IP:port and must be invalidated on address change.

func (*PathValidator) Stop

func (pv *PathValidator) Stop()

Stop halts the background cleanup goroutine. Call when the validator is no longer needed to avoid goroutine leaks. Safe to call multiple times. BUG-M07 fix: Added to mirror RelayManager/HolePunchCoordinator pattern.

func (*PathValidator) ValidatePath

func (pv *PathValidator) ValidatePath(challengeID uint64) error

ValidatePath completes the path validation and migrates the connection.

This should be called after receiving a valid Path Response. It updates the connection's remote address to the validated path.

Parameters:

  • challengeID: The challenge ID that was validated

Returns error if migration fails.

type PeerTest

type PeerTest struct {
	// Nonce uniquely identifies this test
	Nonce uint32

	// DestConnectionID is the nonce-derived destination connection ID
	// for out-of-session PeerTest packets (messages 5-7).
	DestConnectionID uint64

	// SrcConnectionID is the nonce-derived source connection ID
	// for out-of-session PeerTest packets (messages 5-7).
	SrcConnectionID uint64

	// Role is this peer's role in the test
	Role PeerTestRole

	// State is the current test state
	State PeerTestState

	// AliceAddr is the initiator's address
	AliceAddr *net.UDPAddr

	// BobAddr is the relay's address
	BobAddr *net.UDPAddr

	// CharlieAddr is the responder's address
	CharlieAddr *net.UDPAddr

	// StartTime is when the test was initiated
	StartTime time.Time

	// Deadline is when the overall test must complete (60 s per I2P spec).
	// M-04 fix: replaced Timeouts []time.Time (7 entries, only [0] ever set)
	// with a single Deadline field to eliminate the misleading dead state.
	Deadline time.Time

	// NATType is the determined NAT type
	NATType NATType

	// Reachable indicates if peer is directly reachable
	Reachable bool

	// ExternalAddr is the detected external address
	ExternalAddr *net.UDPAddr

	// FailureReason stores the error passed to FailTest (M-4 fix).
	FailureReason error
}

PeerTest represents an active peer test operation.

type PeerTestBlock

type PeerTestBlock struct {
	// MessageCode identifies which of the 7 messages this is (1 byte)
	MessageCode PeerTestMessageCode

	// Code is the status/reason code (1 byte)
	Code uint8

	// Flag is reserved for future use (1 byte, must be 0)
	Flag uint8

	// RouterHash is the 32-byte hash (only for messages 2 and 4)
	RouterHash *data.Hash

	// Version is the peer test protocol version (should be 2)
	Version uint8

	// Nonce uniquely identifies the test session (4 bytes)
	Nonce uint32

	// Timestamp is seconds since epoch (4 bytes)
	Timestamp uint32

	// AlicePort is Alice's port number
	AlicePort uint16

	// AliceIP is Alice's IP address (4 bytes IPv4 or 16 bytes IPv6)
	AliceIP []byte

	// Signature is the Ed25519 (or other) signature (variable length)
	// Optional for messages 5-7.
	Signature []byte
}

PeerTestBlock represents a peer test message (Type 10, Block 10). Wire format per SSU2 spec:

Byte 0:       msg (message number 1-7)
Byte 1:       code (status/reason code)
Byte 2:       flag (unused, set to 0)
Bytes 3-34:   router hash (32 bytes, only for messages 2 and 4)
Next 1 byte:  ver (protocol version, should be 2)
Next 4 bytes: nonce (big-endian)
Next 4 bytes: timestamp (seconds since epoch, big-endian)
Next 1 byte:  asz (endpoint size: 6 for IPv4, 18 for IPv6)
Next 2 bytes: AlicePort (big-endian)
Next asz-2:   AliceIP (4 or 16 bytes)
Remaining:    signature (variable length; optional for messages 5-7)

func DecodePeerTestBlock

func DecodePeerTestBlock(ssu2Block *SSU2Block) (*PeerTestBlock, error)

DecodePeerTestBlock decodes a PeerTest block from wire format per the SSU2 spec.

func (*PeerTestBlock) ValidateSourceAddress

func (b *PeerTestBlock) ValidateSourceAddress(sourceAddr *net.UDPAddr) error

ValidateSourceAddress verifies that the IP/port embedded in a PeerTest block matches the actual UDP source address. Per the spec, Charlie must verify that the claimed Alice address matches the packet source to prevent amplification attacks.

type PeerTestManager

type PeerTestManager struct {
	// contains filtered or unexported fields
}

PeerTestManager manages the seven-message NAT traversal testing protocol. It coordinates peer tests to determine NAT type and external reachability.

Design rationale: - Nonce generation uses crypto/rand for security - State machine tracks test progression through 7 messages - Results cached by remote address for efficiency - Thread-safe for concurrent test operations

Protocol flow: 1. Alice → Bob: Request (InitiatePeerTest) 2. Bob → Charlie: Relay request 3. Charlie → Bob: Relay response 4. Bob → Alice: Result 5. Charlie → Alice: Probe 6. Alice → Charlie: Reply 7. Charlie → Alice: Confirmation

Thread Safety: All public methods are thread-safe.

func NewPeerTestManager

func NewPeerTestManager(listener ListenerRef) *PeerTestManager

NewPeerTestManager creates a new PeerTestManager.

Parameters:

  • listener: The SSU2Listener to manage peer tests for

Returns a new PeerTestManager with empty state.

func NewPeerTestManagerWithFields

func NewPeerTestManagerWithFields(listener ListenerRef, tests map[uint32]*PeerTest, results map[string]*TestResult) *PeerTestManager

NewPeerTestManagerWithFields creates a PeerTestManager with pre-populated fields. Intended for integration tests only — does NOT start the background cleanup goroutine. Long-running tests must call CleanupExpired manually or use NewPeerTestManager instead. H-01 fix: pendingResults is always initialised to prevent nil-map panic in CompleteTest.

func (*PeerTestManager) CleanupExpired

func (ptm *PeerTestManager) CleanupExpired()

CleanupExpired removes tests that have exceeded their timeout.

func (*PeerTestManager) CompleteTest

func (ptm *PeerTestManager) CompleteTest(nonce uint32, result *TestResult) error

CompleteTest marks a test as complete and stores the result.

Parameters:

  • nonce: Test nonce
  • result: Test result to store

Returns error if test not found.

func (*PeerTestManager) CreateRelayTest

func (ptm *PeerTestManager) CreateRelayTest(nonce uint32, aliceAddr, charlieAddr *net.UDPAddr) (uint32, error)

CreateRelayTest creates a relay test when Bob receives request from Alice.

Bob acts as relay between Alice (initiator) and Charlie (responder).

Parameters:

  • nonce: Test nonce from Alice
  • aliceAddr: Alice's address
  • charlieAddr: Charlie's address

Returns nonce on success, error otherwise.

func (*PeerTestManager) CreateResponderTest

func (ptm *PeerTestManager) CreateResponderTest(nonce uint32, aliceAddr, bobAddr *net.UDPAddr) error

CreateResponderTest creates a responder test when Charlie receives relay from Bob.

Charlie acts as responder to probe Alice.

Parameters:

  • nonce: Test nonce
  • aliceAddr: Alice's address
  • bobAddr: Bob's address

Returns error on failure.

func (*PeerTestManager) FailTest

func (ptm *PeerTestManager) FailTest(nonce uint32, reason error) error

FailTest marks a test as failed.

Parameters:

  • nonce: Test nonce
  • reason: Error explaining failure

Returns error if test not found.

func (*PeerTestManager) GetListener

func (ptm *PeerTestManager) GetListener() ListenerRef

GetListener returns the listener reference (for testing).

func (*PeerTestManager) GetResult

func (ptm *PeerTestManager) GetResult(addr *net.UDPAddr) *TestResult

GetResult retrieves cached test result for an address.

Parameters:

  • addr: Remote address

Returns result copy, or nil if not found.

func (*PeerTestManager) GetResultsMap

func (ptm *PeerTestManager) GetResultsMap() map[string]*TestResult

GetResultsMap returns a shallow copy of the results map under lock (for testing only).

WARNING (M-3): The returned map values are pointers into the manager's internal state. Callers MUST NOT modify the returned *TestResult values concurrently with any other manager operation. This method is intended for read-only inspection in tests. For production use, prefer GetResult(addr).

func (*PeerTestManager) GetStats

func (ptm *PeerTestManager) GetStats() map[string]int

GetStats returns statistics about active tests.

func (*PeerTestManager) GetTest

func (ptm *PeerTestManager) GetTest(nonce uint32) *PeerTest

GetTest retrieves peer test information by nonce.

Parameters:

  • nonce: Test nonce

Returns test copy, or nil if not found.

Performance Note (BUG-L05): This method returns a defensive copy to prevent external mutation of internal state. This involves 4-5 allocations per call (UDPAddr copies + struct copy). The current approach prioritizes safety over performance and is acceptable for typical usage patterns. If profiling reveals this is a hot path causing GC pressure, consider alternatives like read-only views or sync.Pool, but only after demonstrating actual performance impact.

func (*PeerTestManager) GetTestsMap

func (ptm *PeerTestManager) GetTestsMap() map[uint32]*PeerTest

GetTestsMap returns a shallow copy of the tests map under lock (for testing only).

WARNING (M-3): The returned map values are pointers into the manager's internal state. Callers MUST NOT modify the returned *PeerTest values concurrently with any other manager operation. This method is intended for read-only inspection in tests. For production use, prefer GetTest(nonce).

func (*PeerTestManager) InitiatePeerTest

func (ptm *PeerTestManager) InitiatePeerTest(bobAddr *net.UDPAddr) (uint32, error)

InitiatePeerTest starts a new peer test as Alice (initiator).

Design rationale: - Generates cryptographically random nonce for test identification - Creates test record with 60-second timeout per I2P spec - Returns nonce for tracking test progress

Parameters:

  • bobAddr: Address of Bob (relay peer)

Returns nonce on success, error otherwise.

func (*PeerTestManager) RemoveTest

func (ptm *PeerTestManager) RemoveTest(nonce uint32)

RemoveTest removes a test from tracking.

Parameters:

  • nonce: Test nonce

func (*PeerTestManager) SetAliceAddr

func (ptm *PeerTestManager) SetAliceAddr(nonce uint32, addr *net.UDPAddr) error

SetAliceAddr sets Alice's address in an existing test.

Used when Alice's external address is determined during the test.

Parameters:

  • nonce: Test nonce
  • addr: Alice's address

Returns error if test not found.

func (*PeerTestManager) SetRawResult

func (ptm *PeerTestManager) SetRawResult(key string, result *TestResult)

SetRawResult stores a result directly in the results map (for testing).

func (*PeerTestManager) SetTestAliceAddr

func (ptm *PeerTestManager) SetTestAliceAddr(nonce uint32, addr *net.UDPAddr)

SetTestAliceAddr sets AliceAddr on the test with the given nonce (for testing).

func (*PeerTestManager) SetTestStartTime

func (ptm *PeerTestManager) SetTestStartTime(nonce uint32, t time.Time)

SetTestStartTime sets StartTime on the test with the given nonce (for testing).

func (*PeerTestManager) Stop

func (ptm *PeerTestManager) Stop()

Stop halts the background cleanup goroutine. Call when the manager is no longer needed to avoid goroutine leaks. Safe to call multiple times.

func (*PeerTestManager) UpdateState

func (ptm *PeerTestManager) UpdateState(nonce uint32, state PeerTestState) error

UpdateState updates the state of a peer test.

Parameters:

  • nonce: Test nonce
  • state: New state

Returns error if test not found.

type PeerTestMessageCode

type PeerTestMessageCode uint8

PeerTestMessageCode identifies the message type in the seven-message protocol.

const (
	// PeerTestRequest (1): Alice -> Bob, request test via relay
	PeerTestRequest PeerTestMessageCode = 1

	// PeerTestRelay (2): Bob -> Charlie, relay request to responder
	PeerTestRelay PeerTestMessageCode = 2

	// PeerTestResponse (3): Charlie -> Bob, responder acknowledges
	PeerTestResponse PeerTestMessageCode = 3

	// PeerTestResult (4): Bob -> Alice, relay sends result
	PeerTestResult PeerTestMessageCode = 4

	// PeerTestProbe (5): Charlie -> Alice, responder probes directly
	PeerTestProbe PeerTestMessageCode = 5

	// PeerTestReply (6): Alice -> Charlie, initiator confirms probe
	PeerTestReply PeerTestMessageCode = 6

	// PeerTestConfirmation (7): Charlie -> Alice, responder confirms success
	PeerTestConfirmation PeerTestMessageCode = 7
)

func (PeerTestMessageCode) String

func (c PeerTestMessageCode) String() string

String returns string representation of the message code.

type PeerTestRole

type PeerTestRole int

PeerTestRole represents the role of a peer in the test.

const (
	// RoleInitiator is Alice who initiates the test
	RoleInitiator PeerTestRole = iota

	// RoleRelay is Bob who relays messages
	RoleRelay

	// RoleResponder is Charlie who responds to test
	RoleResponder
)

func (PeerTestRole) String

func (r PeerTestRole) String() string

String returns human-readable role name.

type PeerTestState

type PeerTestState int

PeerTestState represents the current state of a peer test.

const (
	// TestRequested indicates test has been requested
	TestRequested PeerTestState = iota

	// TestRelayed indicates test has been relayed to responder
	TestRelayed

	// TestProbed indicates probe has been sent
	TestProbed

	// TestComplete indicates test completed successfully
	TestComplete

	// TestFailed indicates test failed
	TestFailed
)

func (PeerTestState) String

func (s PeerTestState) String() string

String returns human-readable state name.

type PendingSession

type PendingSession struct {
	// SessionID uniquely identifies this pending session
	SessionID uint64

	// RemoteAddr is the target peer's UDP address
	RemoteAddr *net.UDPAddr

	// IntroducerAddr is the introducer being used
	IntroducerAddr *net.UDPAddr

	// RelayTag is the tag provided by the introducer
	RelayTag uint32

	// CreatedAt is when this session was initiated
	CreatedAt time.Time

	// Retries tracks the number of retry attempts
	Retries int
}

PendingSession represents a connection awaiting hole punch completion.

type PendingSessionRegistry

type PendingSessionRegistry interface {
	AddPendingSession(sessionID uint64, remoteAddr, introducerAddr *net.UDPAddr, relayTag uint32) error
	IncrementRetries(sessionID uint64) int
	RemovePendingSession(sessionID uint64)
}

PendingSessionRegistry is the relay dependency of HolePunchCoordinator. *RelayManager satisfies this interface.

type RegisteredIntroducer

type RegisteredIntroducer struct {
	// Addr is the UDP address of the introducer
	Addr *net.UDPAddr

	// RouterHash is the 32-byte I2P router identity
	RouterHash []byte

	// StaticKey is the introducer's static public key for RouterInfo publication.
	// This is the 44-byte base64-encoded form (encoding 32 raw bytes) as required
	// by the RouterInfo address format. Other key fields in this package (e.g.,
	// IntroKeySize in key_rotation.go) use raw 32-byte slices (M-5).
	StaticKey []byte

	// IntroKey is the introducer's introduction key for RouterInfo publication.
	// This is the 44-byte base64-encoded form (encoding 32 raw bytes) as required
	// by the RouterInfo address format (M-5).
	IntroKey []byte

	// RelayTag is the tag assigned by this introducer
	RelayTag uint32

	// AddedAt is when this introducer was registered
	AddedAt time.Time

	// LastSeen is when we last communicated with this introducer
	LastSeen time.Time
}

RegisteredIntroducer represents an introducer registered for RouterInfo publication.

type RelayIntroBlock

type RelayIntroBlock struct {
	// Flag is a 1-byte flag field (unused, set to 0)
	Flag uint8

	// AliceRouterHash is Alice's 32-byte router identity hash
	AliceRouterHash []byte

	// Nonce uniquely identifies this relay request (4 bytes, forwarded from Alice)
	Nonce uint32

	// AliceRelayTag is the itag from Charlie's RI (4 bytes)
	AliceRelayTag uint32

	// Timestamp is when the intro was created (4 bytes, seconds since epoch)
	Timestamp uint32

	// Version is the SSU version for the introduction (1=SSU1, 2=SSU2)
	Version uint8

	// AlicePort is Alice's port number (2 bytes, big endian)
	AlicePort uint16

	// AliceIP is Alice's IP address (4 bytes IPv4 or 16 bytes IPv6)
	AliceIP net.IP

	// Signature is the variable-length signature (64 bytes for Ed25519)
	Signature []byte
}

RelayIntroBlock represents a relay introduction (Type 9). Bob sends this to Charlie to introduce Alice.

Wire format per SSU2 spec:

[flag:1][AliceRouterHash:32][nonce:4][relay_tag:4][timestamp:4]
[ver:1][asz:1][AlicePort:2][AliceIP:asz-2][signature:varies]

func DecodeRelayIntro

func DecodeRelayIntro(block *SSU2Block) (*RelayIntroBlock, error)

DecodeRelayIntro decodes a RelayIntro block from wire format.

Wire format per SSU2 spec:

[flag:1][AliceRouterHash:32][nonce:4][relay_tag:4][timestamp:4]
[ver:1][asz:1][AlicePort:2][AliceIP:asz-2][signature:varies]

type RelayManager

type RelayManager struct {
	// contains filtered or unexported fields
}

RelayManager manages relay connections and introducer services for NAT traversal. It handles relay tag allocation, introducer registration, and relay request processing.

Design rationale: - Relay tags are cryptographically random 4-byte values for security - Introducers expire after 1 hour (I2P spec recommendation) - Pending sessions track connections awaiting hole punch completion - Thread-safe for concurrent relay operations

Thread Safety: All public methods are thread-safe.

func NewRelayManager

func NewRelayManager(listener ListenerRef) *RelayManager

NewRelayManager creates a new RelayManager for the specified listener.

Parameters:

  • listener: The SSU2Listener to manage relays for

Returns a new RelayManager with empty state.

func (*RelayManager) AddPendingSession

func (rm *RelayManager) AddPendingSession(sessionID uint64, remoteAddr, introducerAddr *net.UDPAddr, relayTag uint32) error

AddPendingSession adds a session awaiting hole punch completion.

Parameters:

  • sessionID: Unique session identifier
  • remoteAddr: Target peer's UDP address
  • introducerAddr: Introducer's UDP address
  • relayTag: Tag for relay requests

Returns error if parameters are invalid.

func (*RelayManager) AllocateRelayTag

func (rm *RelayManager) AllocateRelayTag(addr *net.UDPAddr) (uint32, error)

AllocateRelayTag allocates a new relay tag for the specified address. Relay tags are used to identify connections being relayed through this peer.

Design rationale: - Tags are cryptographically random 4-byte values - Tags expire after 1 hour per I2P spec - Zero tag is reserved and never allocated

Parameters:

  • addr: UDP address to allocate tag for

Returns the allocated tag, or error if allocation fails.

func (*RelayManager) CleanupExpired

func (rm *RelayManager) CleanupExpired()

CleanupExpired removes expired relay tags, introducers, and pending sessions. This is called periodically by the cleanup timer.

func (*RelayManager) ExpireAllIntroducers

func (rm *RelayManager) ExpireAllIntroducers()

ExpireAllIntroducers immediately expires all registered introducers (test helper).

func (*RelayManager) GetAllIntroducers

func (rm *RelayManager) GetAllIntroducers() []*IntroducerInfo

GetAllIntroducers returns all introducers including expired (for testing).

func (*RelayManager) GetIntroducers

func (rm *RelayManager) GetIntroducers() []*IntroducerInfo

GetIntroducers returns a copy of all active introducers.

Returns a slice of IntroducerInfo pointers (defensive copies).

func (*RelayManager) GetListener

func (rm *RelayManager) GetListener() ListenerRef

GetListener returns the listener reference (for testing).

func (*RelayManager) GetPendingSession

func (rm *RelayManager) GetPendingSession(sessionID uint64) *PendingSession

GetPendingSession retrieves a pending session by ID.

Parameters:

  • sessionID: Session identifier

Returns PendingSession info, or nil if not found.

func (*RelayManager) GetPendingSessionsMap

func (rm *RelayManager) GetPendingSessionsMap() map[uint64]*PendingSession

GetPendingSessionsMap returns a copy of the pending sessions map (for testing).

func (*RelayManager) GetRelayTag

func (rm *RelayManager) GetRelayTag(tag uint32) *RelayTag

GetRelayTag retrieves relay tag information.

Parameters:

  • tag: Relay tag to look up

Returns RelayTag info, or nil if not found.

Performance Note (BUG-L05): This method returns a defensive copy including deep copy of IP address slice to prevent external mutation. This involves multiple allocations per call but ensures thread-safety and prevents aliasing bugs. The current approach prioritizes correctness over performance. Optimize only if profiling shows this is a bottleneck in production.

func (*RelayManager) GetRelayTagsMap

func (rm *RelayManager) GetRelayTagsMap() map[uint32]*RelayTag

GetRelayTagsMap returns a copy of the relay tags map (for testing).

func (*RelayManager) GetStats

func (rm *RelayManager) GetStats() map[string]int

GetStats returns statistics about the relay manager state.

func (*RelayManager) IncrementRetries

func (rm *RelayManager) IncrementRetries(sessionID uint64) int

IncrementRetries increments the retry counter for a pending session.

Parameters:

  • sessionID: Session identifier

Returns the new retry count, or -1 if session not found.

func (*RelayManager) RegisterIntroducer

func (rm *RelayManager) RegisterIntroducer(addr *net.UDPAddr, routerHash data.Hash, relayTag uint32) error

RegisterIntroducer registers a new introducer for this peer. The introducer can be used to relay connection requests to this peer when behind NAT.

Design rationale: - Maximum 3 introducers per I2P spec - Introducers expire after 1 hour - Oldest introducer is replaced if at capacity

Parameters:

  • addr: UDP address of the introducer
  • routerHash: router identity hash of the introducer
  • relayTag: Tag assigned by the introducer for relay requests

Returns error if parameters are invalid.

func (*RelayManager) RemoveIntroducer

func (rm *RelayManager) RemoveIntroducer(addr *net.UDPAddr)

RemoveIntroducer removes an introducer by address.

Parameters:

  • addr: UDP address of the introducer to remove

func (*RelayManager) RemovePendingSession

func (rm *RelayManager) RemovePendingSession(sessionID uint64)

RemovePendingSession removes a pending session.

Parameters:

  • sessionID: Session identifier to remove

func (*RelayManager) SetIntroducerExpiry

func (rm *RelayManager) SetIntroducerExpiry(idx int, t time.Time)

SetIntroducerExpiry sets the expiry of the introducer at given index (for testing).

func (*RelayManager) SetRelayTagExpiry

func (rm *RelayManager) SetRelayTagExpiry(tag uint32, t time.Time)

SetRelayTagExpiry sets the expiry of a relay tag (for testing).

func (*RelayManager) Start

func (rm *RelayManager) Start()

Start starts the cleanup timer for periodic maintenance. Must be called after NewRelayManager() to enable automatic cleanup. Safe to call multiple times (idempotent).

func (*RelayManager) Stop

func (rm *RelayManager) Stop()

Stop stops the relay manager and cleans up resources. Safe to call multiple times (idempotent).

func (*RelayManager) ValidateRelayTag

func (rm *RelayManager) ValidateRelayTag(tag uint32, addr *net.UDPAddr) bool

ValidateRelayTag validates that a relay tag is active and matches the specified address.

Parameters:

  • tag: Relay tag to validate
  • addr: Expected address for the tag

Returns true if tag is valid and matches address.

type RelayRequestBlock

type RelayRequestBlock struct {
	// Flag is a 1-byte flag field (unused, set to 0)
	Flag uint8

	// Nonce uniquely identifies this relay request (4 bytes)
	Nonce uint32

	// RelayTag is the itag from Charlie's RI (4 bytes)
	RelayTag uint32

	// Timestamp is Unix timestamp in seconds (4 bytes)
	Timestamp uint32

	// Version is the SSU version for the introduction (1=SSU1, 2=SSU2)
	Version uint8

	// AlicePort is Alice's port number (2 bytes, big endian)
	AlicePort uint16

	// AliceIP is Alice's IP address (4 bytes IPv4 or 16 bytes IPv6)
	AliceIP net.IP

	// Signature is the variable-length signature (64 bytes for Ed25519)
	Signature []byte
}

RelayRequestBlock represents a relay request (Type 7). Alice sends this to Bob to request relay through to Charlie.

Wire format per SSU2 spec:

[Flag:1][Nonce:4][RelayTag:4][Timestamp:4][Ver:1][Asz:1][AlicePort:2][AliceIP:asz-2][Signature:varies]

func DecodeRelayRequest

func DecodeRelayRequest(block *SSU2Block) (*RelayRequestBlock, error)

DecodeRelayRequest decodes a RelayRequest block from wire format.

Wire format: [Flag:1][Nonce:4][RelayTag:4][Timestamp:4][Ver:1][Asz:1][AlicePort:2][AliceIP:asz-2][Signature:varies]

type RelayResponseBlock

type RelayResponseBlock struct {
	// Flag is a 1-byte flag field (unused, set to 0)
	Flag uint8

	// Code indicates success or failure reason (1 byte)
	// 0 = accepted, 1-63 = rejected by Bob, 64+ = rejected by Charlie
	Code uint8

	// Nonce matches the nonce from RelayRequest (4 bytes)
	Nonce uint32

	// Timestamp is Unix timestamp in seconds (4 bytes).
	// Present when Code == 0 or Code >= 64.
	Timestamp uint32

	// Version is the SSU version (1 byte).
	// Present when Code == 0 or Code >= 64.
	Version uint8

	// CharliePort is Charlie's port number (2 bytes).
	// Present when Code == 0 or Code >= 64 with csz > 0.
	CharliePort uint16

	// CharlieIP is Charlie's IP address.
	// Present when Code == 0 or Code >= 64 with csz > 0.
	CharlieIP net.IP

	// Signature is the variable-length signature (typically 64 bytes for Ed25519).
	// Present when Code == 0 or Code >= 64.
	Signature []byte

	// Token is the 8-byte session request token from Charlie.
	// Only present when Code == 0 (accepted).
	Token []byte
}

RelayResponseBlock represents a relay response (Type 8). Bob sends this to Alice after processing relay request.

Wire format per SSU2 spec:

[Flag:1][Code:1][Nonce:4]

When Code == 0 (accepted by Charlie), additional fields follow:

[Timestamp:4][Ver:1][Csz:1][CharliePort:2][CharlieIP:csz-2][Signature:varies][Token:8]

When Code >= 64 (rejected by Charlie), additional fields follow:

[Timestamp:4][Ver:1][Csz:1][CharliePort:2][CharlieIP:csz-2][Signature:varies]

When Code 1-63 (rejected by Bob), no additional fields.

func DecodeRelayResponse

func DecodeRelayResponse(block *SSU2Block) (*RelayResponseBlock, error)

DecodeRelayResponse decodes a RelayResponse block from wire format.

Wire format: [Flag:1][Code:1][Nonce:4][...]

type RelayTag

type RelayTag struct {
	// Tag is the 4-byte relay tag value
	Tag uint32

	// ForAddr is the address this tag was allocated for
	ForAddr *net.UDPAddr

	// CreatedAt is when this tag was created
	CreatedAt time.Time

	// ExpiresAt is when this tag expires
	ExpiresAt time.Time
}

RelayTag represents an active relay tag allocation.

type RelayTagBlock

type RelayTagBlock struct {
	// RelayTag is the assigned tag value (4 bytes)
	RelayTag uint32
}

RelayTagBlock represents a relay tag assignment (Type 16). Per spec: relay tag is 4 bytes, big-endian, nonzero.

func DecodeRelayTag

func DecodeRelayTag(block *SSU2Block) (*RelayTagBlock, error)

DecodeRelayTag decodes a RelayTag block from wire format. Per spec: 4 bytes minimum (relay tag only).

Parameters:

  • block: SSU2Block with Type 16

Returns:

  • *RelayTagBlock: Decoded relay tag
  • error: If decoding fails or validation fails

type RelayTagRequestBlock

type RelayTagRequestBlock struct{}

RelayTagRequestBlock represents a relay tag request (Type 15). Per spec §Relay Tag Request Block, the data portion is empty (size=0).

func DecodeRelayTagRequest

func DecodeRelayTagRequest(block *SSU2Block) (*RelayTagRequestBlock, error)

DecodeRelayTagRequest decodes a RelayTagRequest block from wire format. Per spec the data portion is empty (size=0). Non-empty data is accepted for backward compatibility with older implementations but is ignored.

type SSU2Block

type SSU2Block = wire.SSU2Block

Type aliases from ssu2/wire so path code can reference these types without qualifying them with the wire package name.

func EncodePathChallenge

func EncodePathChallenge(challengeID uint64) *SSU2Block

EncodePathChallenge encodes a Path Challenge block (Type 18).

Wire format: [ChallengeID:8]

Parameters:

  • challengeID: 8-byte challenge identifier

Returns encoded block.

func EncodePathChallengeWithPadding

func EncodePathChallengeWithPadding(challengeID uint64, probeSize int) *SSU2Block

EncodePathChallengeWithPadding creates a Path Challenge block padded to probeSize bytes (total block data length). The first 8 bytes are the challenge ID; remaining bytes are random padding for MTU probing (G-5).

func EncodePathResponse

func EncodePathResponse(challengeID uint64) *SSU2Block

EncodePathResponse encodes a Path Response block (Type 19).

Wire format: [ChallengeID:8]

Parameters:

  • challengeID: 8-byte challenge identifier from the Path Challenge

Returns encoded block.

func EncodePeerTestBlock

func EncodePeerTestBlock(block *PeerTestBlock) (*SSU2Block, error)

EncodePeerTestBlock encodes a PeerTest block to wire format per the SSU2 spec.

func EncodeRelayIntro

func EncodeRelayIntro(intro *RelayIntroBlock) (*SSU2Block, error)

EncodeRelayIntro encodes a RelayIntro block to wire format.

Wire format per SSU2 spec:

[flag:1][AliceRouterHash:32][nonce:4][relay_tag:4][timestamp:4]
[ver:1][asz:1][AlicePort:2][AliceIP:asz-2][signature:varies]

func EncodeRelayRequest

func EncodeRelayRequest(req *RelayRequestBlock) (*SSU2Block, error)

EncodeRelayRequest encodes a RelayRequest block to wire format.

Wire format per SSU2 spec: [Flag:1][Nonce:4][RelayTag:4][Timestamp:4][Ver:1][Asz:1][AlicePort:2][AliceIP:asz-2][Signature:varies]

func EncodeRelayResponse

func EncodeRelayResponse(resp *RelayResponseBlock) (*SSU2Block, error)

EncodeRelayResponse encodes a RelayResponse block to wire format.

Wire format per SSU2 spec:

[Flag:1][Code:1][Nonce:4][...]

For Code 0 (accepted): includes Timestamp, Ver, Csz, CharliePort, CharlieIP, Signature, Token. For Code >= 64 (Charlie rejection): includes Timestamp, Ver, Csz, CharliePort, CharlieIP, Signature. For Code 1-63 (Bob rejection): only Flag, Code, Nonce.

func EncodeRelayTag

func EncodeRelayTag(tag *RelayTagBlock) (*SSU2Block, error)

EncodeRelayTag encodes a RelayTag block to wire format. Per spec: [RelayTag:4] (4 bytes, big-endian, nonzero)

Parameters:

  • tag: RelayTag data to encode

Returns:

  • *SSU2Block: Encoded block ready for transmission
  • error: If validation fails

func EncodeRelayTagRequest

func EncodeRelayTagRequest(req *RelayTagRequestBlock) (*SSU2Block, error)

EncodeRelayTagRequest encodes a RelayTagRequest block to wire format. Per spec §Relay Tag Request Block: size=0 (empty data portion).

type SSU2Packet

type SSU2Packet = wire.SSU2Packet

Type aliases from ssu2/wire so path code can reference these types without qualifying them with the wire package name.

type TestResult

type TestResult struct {
	// NATType is the determined NAT type
	NATType NATType

	// ExternalAddr is the detected external address
	ExternalAddr *net.UDPAddr

	// ExternalPort is the detected external port
	ExternalPort uint16

	// Reachable indicates if peer is directly reachable
	Reachable bool

	// TestTime is when the test completed
	TestTime time.Time

	// DirectProbeSuccess indicates if Charlie → Alice direct probe succeeded
	DirectProbeSuccess bool

	// RelayedProbeSuccess indicates if Charlie → Alice via Bob succeeded
	RelayedProbeSuccess bool

	// PortConsistent indicates if external port is consistent
	PortConsistent bool

	// IPConsistent indicates if external IP is consistent
	IPConsistent bool
}

TestResult stores the results of a completed peer test.

func AnalyzeProbeResults

func AnalyzeProbeResults(directSuccess, relayedSuccess bool, addr1, addr2 *net.UDPAddr) *TestResult

AnalyzeProbeResults analyzes probe outcomes and address consistency to build a TestResult summary.

This helper consolidates probe data into a structured result for NAT type determination.

Parameters:

  • directSuccess: Whether direct probe (Charlie → Alice) succeeded
  • relayedSuccess: Whether relayed probe succeeded
  • addr1: First observed external address
  • addr2: Second observed external address

Returns a TestResult with consistency flags set.

type TokenCacheAccessor

type TokenCacheAccessor interface {
	InvalidateAddress(addr *net.UDPAddr)
}

TokenCacheAccessor provides address-based token invalidation. Implemented by *ssu2.TokenCache.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL