// circuit-relay-bridge: v0.8.19 regression tests for the proper bridge
// data plane.
//
// Background: prior to v0.8.19, _bridgeCircuit called Relay.createCircuit
// with `channel.stream` arguments — but `channel.stream` is undefined in
// modern protomux (the Channel class only exposes `_mux.stream`). The
// reservation handshake completed successfully on the wire, but no
// actual bridged data ever flowed. PearPaste was the first customer to
// drive a flow that reached the bridge code and discovered the gap.
//
// Same release fixes a silent auth bypass — the reserve/connect identity
// checks were reading channel.stream?.remotePublicKey (always undefined),
// causing the check to short-circuit to "pass" and allowing any peer to
// reserve under any pubkey. Now reads channel._mux.stream.remotePublicKey.
//
// These tests do not stand up a real swarm. They mock protomux channels
// (with _hiverelay message senders that record calls) and a mock Relay
// (which tracks registerCircuit/recordCircuitBytes/closeCircuit calls).
// That's enough to exercise the routing/auth/accounting logic.
import test from 'brittle'
import b4a from 'b4a'
import { CircuitRelay, REASON_CODES } from 'p2p-hiverelay/core/protocol/relay-circuit.js'
import { ERR } from 'p2p-hiverelay/core/protocol/messages.js'
// ─── Test helpers ─────────────────────────────────────────────────
function pub (byteVal) {
// 32-byte fake pubkey filled with a known byte
return b4a.alloc(32, byteVal)
}
function makeMockChannel ({ remotePubkey = null } = {}) {
const sends = {
statusMsg: [],
dataMsg: [],
readyMsg: [],
closeMsg: []
}
const channel = {
opened: true,
closed: false,
_hiverelay: null,
// The underlying _mux.stream is what the v0.8.19 auth check reads.
// Set remotePublicKey here to simulate the noise-authenticated peer.
_mux: {
stream: remotePubkey ? { remotePublicKey: remotePubkey } : { remotePublicKey: null }
}
}
// _hiverelay senders capture the outgoing payload so tests can assert
// on what got sent to whom.
channel._hiverelay = {
reserveMsg: { send: (msg) => sends.reserveMsg && sends.reserveMsg.push(msg) },
connectMsg: { send: (msg) => sends.connectMsg && sends.connectMsg.push(msg) },
statusMsg: { send: (msg) => sends.statusMsg.push(msg) },
dataMsg: { send: (msg) => sends.dataMsg.push(msg) },
readyMsg: { send: (msg) => sends.readyMsg.push(msg) },
closeMsg: { send: (msg) => sends.closeMsg.push(msg) }
}
channel.sends = sends
return channel
}
function makeMockRelay ({ atCapacity = false, perPeerCap = 5 } = {}) {
const calls = { register: [], record: [], close: [] }
return {
circuits: new Map(),
maxConnections: atCapacity ? 0 : 256,
maxCircuitsPerPeer: perPeerCap,
registerCircuit (circuitId, sourcePeerKey, maxBytes) {
calls.register.push({ circuitId, sourcePeerKey, maxBytes })
if (atCapacity) return false
return true
},
recordCircuitBytes (circuitId, bytes) {
calls.record.push({ circuitId, bytes })
return true
},
closeCircuit (circuitId, reason) {
calls.close.push({ circuitId, reason })
},
_calls: calls
}
}
// ─── Tests ────────────────────────────────────────────────────────
test('reserve+connect bridges a circuit and sends ready to BOTH peers', (t) => {
const cr = new CircuitRelay(null, makeMockRelay())
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const bob = pub(0xBB)
const aliceChannel = makeMockChannel({ remotePubkey: alice })
const bobChannel = makeMockChannel({ remotePubkey: bob })
// Alice reserves.
cr._onReserve(aliceChannel, { peerPubkey: alice })
t.is(aliceChannel.sends.statusMsg.length, 1, 'reserve responded with status')
t.is(aliceChannel.sends.statusMsg[0].code, ERR.NONE, 'reserve status code is NONE (accepted)')
// Bob connects targeting Alice.
cr._onConnect(bobChannel, { targetPubkey: alice, sourcePubkey: bob })
// Both sides should now have received a ready message with the
// OTHER peer's pubkey + the same circuitId.
t.is(aliceChannel.sends.readyMsg.length, 1, 'alice got ready')
t.is(bobChannel.sends.readyMsg.length, 1, 'bob got ready')
t.ok(b4a.equals(aliceChannel.sends.readyMsg[0].remotePubkey, bob), 'alice ready carries bob pubkey')
t.ok(b4a.equals(bobChannel.sends.readyMsg[0].remotePubkey, alice), 'bob ready carries alice pubkey')
t.ok(b4a.equals(aliceChannel.sends.readyMsg[0].circuitId, bobChannel.sends.readyMsg[0].circuitId),
'both peers see the same circuitId')
t.is(aliceChannel.sends.readyMsg[0].circuitId.byteLength, 16, 'circuitId is 16 bytes')
t.is(cr.activeCircuits.size, 1, 'one active circuit tracked')
})
test('data sent on one channel is forwarded to the other', (t) => {
const cr = new CircuitRelay(null, makeMockRelay())
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const bob = pub(0xBB)
const aliceChannel = makeMockChannel({ remotePubkey: alice })
const bobChannel = makeMockChannel({ remotePubkey: bob })
cr._onReserve(aliceChannel, { peerPubkey: alice })
cr._onConnect(bobChannel, { targetPubkey: alice, sourcePubkey: bob })
const circuitId = aliceChannel.sends.readyMsg[0].circuitId
// Alice sends data through the circuit.
const payload = b4a.from('hello bob, this is alice')
cr._onCircuitData(aliceChannel, { circuitId, data: payload })
t.is(bobChannel.sends.dataMsg.length, 1, 'bob received forwarded data')
t.ok(b4a.equals(bobChannel.sends.dataMsg[0].data, payload), 'payload arrives intact')
t.is(aliceChannel.sends.dataMsg.length, 0, 'alice does not receive her own data back')
// Reverse direction.
const reply = b4a.from('hi alice')
cr._onCircuitData(bobChannel, { circuitId, data: reply })
t.is(aliceChannel.sends.dataMsg.length, 1, 'alice received reply')
t.ok(b4a.equals(aliceChannel.sends.dataMsg[0].data, reply), 'reply arrives intact')
})
test('data from a non-endpoint channel is dropped (no impersonation)', (t) => {
const cr = new CircuitRelay(null, makeMockRelay())
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const bob = pub(0xBB)
const eve = pub(0xEE)
const aliceChannel = makeMockChannel({ remotePubkey: alice })
const bobChannel = makeMockChannel({ remotePubkey: bob })
const eveChannel = makeMockChannel({ remotePubkey: eve })
cr._onReserve(aliceChannel, { peerPubkey: alice })
cr._onConnect(bobChannel, { targetPubkey: alice, sourcePubkey: bob })
const circuitId = aliceChannel.sends.readyMsg[0].circuitId
// Eve learned the circuitId somehow and tries to inject data.
cr._onCircuitData(eveChannel, { circuitId, data: b4a.from('mwahaha') })
t.is(aliceChannel.sends.dataMsg.length, 0, 'alice did not receive eve traffic')
t.is(bobChannel.sends.dataMsg.length, 0, 'bob did not receive eve traffic')
})
test('data for unknown circuitId is dropped silently (no feedback to attacker)', (t) => {
const cr = new CircuitRelay(null, makeMockRelay())
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const aliceChannel = makeMockChannel({ remotePubkey: alice })
// Random 16-byte circuitId, no matching bridge.
const fakeId = b4a.alloc(16, 0xFF)
cr._onCircuitData(aliceChannel, { circuitId: fakeId, data: b4a.from('ping') })
t.is(aliceChannel.sends.dataMsg.length, 0, 'no echo')
t.is(aliceChannel.sends.closeMsg.length, 0, 'no close to dox the lookup')
})
test('per-frame size cap closes the circuit (DoS guard)', (t) => {
const relayMock = makeMockRelay()
const cr = new CircuitRelay(null, relayMock, { maxDataMsgBytes: 100 })
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const bob = pub(0xBB)
const aliceChannel = makeMockChannel({ remotePubkey: alice })
const bobChannel = makeMockChannel({ remotePubkey: bob })
cr._onReserve(aliceChannel, { peerPubkey: alice })
cr._onConnect(bobChannel, { targetPubkey: alice, sourcePubkey: bob })
const circuitId = aliceChannel.sends.readyMsg[0].circuitId
// Send a frame larger than the cap.
const big = b4a.alloc(200, 0x42)
cr._onCircuitData(aliceChannel, { circuitId, data: big })
t.is(bobChannel.sends.dataMsg.length, 0, 'oversized frame not forwarded')
t.is(cr.activeCircuits.size, 0, 'circuit was torn down')
t.is(relayMock._calls.close.length, 1, 'relay was notified of close')
t.is(relayMock._calls.close[0].reason, 'FRAME_TOO_LARGE')
})
test('per-circuit byte cap closes the circuit when reached', (t) => {
const relayMock = makeMockRelay()
const cr = new CircuitRelay(null, relayMock, { maxCircuitBytes: 50 })
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const bob = pub(0xBB)
const aliceChannel = makeMockChannel({ remotePubkey: alice })
const bobChannel = makeMockChannel({ remotePubkey: bob })
cr._onReserve(aliceChannel, { peerPubkey: alice })
cr._onConnect(bobChannel, { targetPubkey: alice, sourcePubkey: bob })
const circuitId = aliceChannel.sends.readyMsg[0].circuitId
// First frame: 30 bytes, under cap.
cr._onCircuitData(aliceChannel, { circuitId, data: b4a.alloc(30, 0x01) })
t.is(bobChannel.sends.dataMsg.length, 1, 'first frame forwarded')
// Second frame: 30 bytes, would push total to 60 > 50.
cr._onCircuitData(aliceChannel, { circuitId, data: b4a.alloc(30, 0x02) })
t.is(bobChannel.sends.dataMsg.length, 1, 'second frame NOT forwarded')
t.is(cr.activeCircuits.size, 0, 'circuit torn down')
t.is(relayMock._calls.close[0].reason, 'BYTES_EXCEEDED')
})
test('per-circuit rate cap closes the circuit when exceeded', (t) => {
const relayMock = makeMockRelay()
const cr = new CircuitRelay(null, relayMock, { maxCircuitRateBytesPerSecond: 50 })
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const bob = pub(0xBB)
const aliceChannel = makeMockChannel({ remotePubkey: alice })
const bobChannel = makeMockChannel({ remotePubkey: bob })
cr._onReserve(aliceChannel, { peerPubkey: alice })
cr._onConnect(bobChannel, { targetPubkey: alice, sourcePubkey: bob })
const circuitId = aliceChannel.sends.readyMsg[0].circuitId
cr._onCircuitData(aliceChannel, { circuitId, data: b4a.alloc(30, 0x01) })
t.is(bobChannel.sends.dataMsg.length, 1, 'first frame forwarded')
cr._onCircuitData(aliceChannel, { circuitId, data: b4a.alloc(30, 0x02) })
t.is(bobChannel.sends.dataMsg.length, 1, 'second frame not forwarded')
t.is(cr.activeCircuits.size, 0, 'circuit torn down')
t.is(relayMock._calls.close[0].reason, 'RATE_EXCEEDED')
})
test('circuit duration cap closes stale active circuits in the bridge layer', (t) => {
const relayMock = makeMockRelay()
const cr = new CircuitRelay(null, relayMock, { maxCircuitDuration: 50 })
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const bob = pub(0xBB)
const aliceChannel = makeMockChannel({ remotePubkey: alice })
const bobChannel = makeMockChannel({ remotePubkey: bob })
cr._onReserve(aliceChannel, { peerPubkey: alice })
cr._onConnect(bobChannel, { targetPubkey: alice, sourcePubkey: bob })
const circuitId = aliceChannel.sends.readyMsg[0].circuitId
const circuit = cr.activeCircuits.get(b4a.toString(circuitId, 'hex'))
circuit.startedAt = Date.now() - 51
cr._onCircuitData(aliceChannel, { circuitId, data: b4a.alloc(1, 0x01) })
t.is(bobChannel.sends.dataMsg.length, 0, 'stale circuit data not forwarded')
t.is(cr.activeCircuits.size, 0, 'circuit torn down')
t.is(relayMock._calls.close[0].reason, 'DURATION_EXCEEDED')
})
test('single-argument CircuitRelay constructor keeps Bare relay compatibility', (t) => {
const relayMock = makeMockRelay()
const cr = new CircuitRelay(relayMock)
t.teardown(() => cr.destroy())
t.is(cr.relay, relayMock)
t.is(cr.swarm, null)
})
test('reserve with mismatched pubkey is REJECTED (auth bypass closed)', (t) => {
const cr = new CircuitRelay(null, makeMockRelay())
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const mallory = pub(0xCC)
// Mallory's channel is authenticated as mallory, but the reserve
// payload claims to be alice. Pre-v0.8.19 this silently passed
// because channel.stream was undefined. v0.8.19 reads _mux.stream
// correctly and catches it.
const malloryChannel = makeMockChannel({ remotePubkey: mallory })
cr._onReserve(malloryChannel, { peerPubkey: alice })
t.is(malloryChannel.sends.statusMsg.length, 1, 'got a status response')
t.is(malloryChannel.sends.statusMsg[0].code, ERR.NOT_FOUND, 'reserve rejected')
t.is(malloryChannel.sends.statusMsg[0].message, 'Peer identity mismatch')
t.is(cr.reservations.size, 0, 'no reservation granted')
})
test('connect with mismatched sourcePubkey is REJECTED (auth bypass closed)', (t) => {
const cr = new CircuitRelay(null, makeMockRelay())
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const bob = pub(0xBB)
const mallory = pub(0xCC)
// Alice has a real reservation.
const aliceChannel = makeMockChannel({ remotePubkey: alice })
cr._onReserve(aliceChannel, { peerPubkey: alice })
// Mallory tries to connect to Alice but claims to be Bob.
const malloryChannel = makeMockChannel({ remotePubkey: mallory })
cr._onConnect(malloryChannel, { targetPubkey: alice, sourcePubkey: bob })
t.is(malloryChannel.sends.statusMsg.length, 1, 'got a status response')
t.is(malloryChannel.sends.statusMsg[0].code, ERR.NOT_FOUND, 'connect rejected')
t.is(malloryChannel.sends.statusMsg[0].message, 'Source identity mismatch')
t.is(cr.activeCircuits.size, 0, 'no bridge established')
})
test('connect before reserve queues the pending connect (existing semantics preserved)', (t) => {
const cr = new CircuitRelay(null, makeMockRelay())
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const bob = pub(0xBB)
const bobChannel = makeMockChannel({ remotePubkey: bob })
// Bob connects before Alice reserves.
cr._onConnect(bobChannel, { targetPubkey: alice, sourcePubkey: bob })
t.is(bobChannel.sends.statusMsg.length, 1, 'got a status response')
t.is(bobChannel.sends.statusMsg[0].code, ERR.NOT_FOUND, 'queued, not bridged yet')
t.is(cr.pendingConnects.size, 1, 'pending connect queued')
// Now Alice reserves.
const aliceChannel = makeMockChannel({ remotePubkey: alice })
cr._onReserve(aliceChannel, { peerPubkey: alice })
// The pending connect should have fired.
t.is(cr.activeCircuits.size, 1, 'pending connect bridged on reserve')
t.is(aliceChannel.sends.readyMsg.length, 1, 'alice got ready')
t.is(bobChannel.sends.readyMsg.length, 1, 'bob got ready')
})
test('channel close tears down circuits the channel was part of', (t) => {
const relayMock = makeMockRelay()
const cr = new CircuitRelay(null, relayMock)
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const bob = pub(0xBB)
const aliceChannel = makeMockChannel({ remotePubkey: alice })
const bobChannel = makeMockChannel({ remotePubkey: bob })
cr._onReserve(aliceChannel, { peerPubkey: alice })
cr._onConnect(bobChannel, { targetPubkey: alice, sourcePubkey: bob })
t.is(cr.activeCircuits.size, 1, 'circuit established')
// Bob's channel drops.
cr._onChannelClose(bobChannel)
t.is(cr.activeCircuits.size, 0, 'circuit torn down')
// Alice should have been notified of close.
t.is(aliceChannel.sends.closeMsg.length, 1, 'alice notified of close')
t.is(aliceChannel.sends.closeMsg[0].reason, REASON_CODES.PEER_CLOSED, 'reason is PEER_CLOSED')
})
test('relay at-capacity refuses to register new circuit', (t) => {
const relayMock = makeMockRelay({ atCapacity: true })
const cr = new CircuitRelay(null, relayMock)
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const bob = pub(0xBB)
// With atCapacity, `relay.circuits.size >= relay.maxConnections` is
// true (0 >= 0), so the reserve itself gets rejected before we even
// attempt to bridge. Tweak the mock to allow reservation but reject
// registration — that's the real "tons of reservations queued, one
// tries to bridge" scenario.
relayMock.maxConnections = 256 // allow reservation
const aliceChannel = makeMockChannel({ remotePubkey: alice })
const bobChannel = makeMockChannel({ remotePubkey: bob })
cr._onReserve(aliceChannel, { peerPubkey: alice })
cr._onConnect(bobChannel, { targetPubkey: alice, sourcePubkey: bob })
// Bridge attempt would have called registerCircuit, which returns
// false in atCapacity mode.
t.is(relayMock._calls.register.length, 1, 'attempted to register')
t.is(cr.activeCircuits.size, 0, 'no active circuit')
t.is(bobChannel.sends.statusMsg.length, 1, 'bob got status (capacity full)')
t.is(bobChannel.sends.statusMsg[0].code, ERR.CAPACITY_FULL, 'rejected as capacity full')
})
test('getStats reports activeCircuits in addition to reservations', (t) => {
const cr = new CircuitRelay(null, makeMockRelay())
t.teardown(() => cr.destroy())
const alice = pub(0xAA)
const bob = pub(0xBB)
const aliceChannel = makeMockChannel({ remotePubkey: alice })
const bobChannel = makeMockChannel({ remotePubkey: bob })
cr._onReserve(aliceChannel, { peerPubkey: alice })
cr._onConnect(bobChannel, { targetPubkey: alice, sourcePubkey: bob })
const stats = cr.getStats()
t.is(stats.activeReservations, 1)
t.is(stats.pendingConnects, 0)
t.is(stats.activeCircuits, 1, 'new field exposed in v0.8.19')
})