OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / swarm-firewall.test.js

130 lines · javascript ref:
import test from 'brittle'
import b4a from 'b4a'
import { SwarmFirewall } from 'p2p-hiverelay/core/relay-node/swarm-firewall.js'

const PK_A = b4a.alloc(32, 0xaa)
const PK_B = b4a.alloc(32, 0xbb)
const PK_C = b4a.alloc(32, 0xcc)
const HEX_A = b4a.toString(PK_A, 'hex')
const HEX_B = b4a.toString(PK_B, 'hex')
const HEX_C = b4a.toString(PK_C, 'hex')

test('SwarmFirewall — accepts by default with no rules', (t) => {
  const fw = new SwarmFirewall()
  t.is(fw.check(PK_A, { remoteAddress: '1.2.3.4' }), false, 'accepts unknown peer')
  fw.destroy()
})

test('SwarmFirewall — blocklist rejects', (t) => {
  const fw = new SwarmFirewall({ blocklist: [HEX_A] })
  t.is(fw.check(PK_A, { remoteAddress: '1.2.3.4' }), true, 'rejects blocklisted')
  t.is(fw.check(PK_B, { remoteAddress: '1.2.3.4' }), false, 'accepts non-listed')
  const s = fw.stats()
  t.is(s.rejected, 1)
  t.is(s.byReason.blocklist, 1)
  fw.destroy()
})

test('SwarmFirewall — allowlist beats blocklist on same key', (t) => {
  const fw = new SwarmFirewall()
  fw.block(HEX_A)
  fw.allow(HEX_A) // should remove from blocklist
  t.is(fw.isBlocked(HEX_A), false, 'no longer blocked')
  t.is(fw.isAllowed(HEX_A), true)
  t.is(fw.check(PK_A, { remoteAddress: '1.2.3.4' }), false, 'allowed peer accepted')
  fw.destroy()
})

test('SwarmFirewall — allowlist short-circuits IP rate limit', (t) => {
  const fw = new SwarmFirewall({ allowlist: [HEX_A], ipMaxConnects: 1 })
  // First connect from IP — should accept and not consume the bucket for allowlisted
  fw.check(PK_A, { remoteAddress: '5.5.5.5' })
  fw.check(PK_A, { remoteAddress: '5.5.5.5' })
  fw.check(PK_A, { remoteAddress: '5.5.5.5' })
  // Now a non-allowlisted peer from same IP — should still get the IP's first slot
  t.is(fw.check(PK_B, { remoteAddress: '5.5.5.5' }), false, 'non-allowlisted gets first slot since allowlist did not consume')
  fw.destroy()
})

test('SwarmFirewall — IP rate limit rejects after threshold', (t) => {
  let now = 1_000_000
  const fw = new SwarmFirewall({ ipMaxConnects: 3, ipWindowMs: 10_000, now: () => now })
  t.is(fw.check(PK_A, { remoteAddress: '7.7.7.7' }), false)
  t.is(fw.check(PK_B, { remoteAddress: '7.7.7.7' }), false)
  t.is(fw.check(PK_C, { remoteAddress: '7.7.7.7' }), false)
  // 4th connect from same IP within window → reject
  t.is(fw.check(b4a.alloc(32, 0xdd), { remoteAddress: '7.7.7.7' }), true, 'rejects after 3')
  // Different IP: still allowed
  t.is(fw.check(PK_A, { remoteAddress: '8.8.8.8' }), false, 'different IP accepted')
  // After the window expires, rate clears
  now += 11_000
  t.is(fw.check(PK_A, { remoteAddress: '7.7.7.7' }), false, 'window expiry resets bucket')
  fw.destroy()
})

test('SwarmFirewall — reputation threshold rejects low scores', (t) => {
  const scores = { [HEX_A]: -2000, [HEX_B]: 50, [HEX_C]: null }
  const fw = new SwarmFirewall({
    minReputation: -1000,
    getReputationScore: (pk) => scores[pk]
  })
  t.is(fw.check(PK_A, { remoteAddress: '1.1.1.1' }), true, 'rejects -2000 (below -1000)')
  t.is(fw.check(PK_B, { remoteAddress: '1.1.1.1' }), false, 'accepts +50')
  t.is(fw.check(PK_C, { remoteAddress: '1.1.1.1' }), false, 'unknown reputation accepted')
  const s = fw.stats()
  t.is(s.byReason['low-reputation'], 1)
  fw.destroy()
})

test('SwarmFirewall — onReject fires with details', (t) => {
  const events = []
  const fw = new SwarmFirewall({
    blocklist: [HEX_A],
    onReject: (info) => events.push(info)
  })
  fw.check(PK_A, { remoteAddress: '9.9.9.9' })
  t.is(events.length, 1)
  t.is(events[0].reason, 'blocklist')
  t.is(events[0].pubkey, HEX_A)
  t.is(events[0].ip, '9.9.9.9')
  fw.destroy()
})

test('SwarmFirewall — stats track accept/reject totals', (t) => {
  const fw = new SwarmFirewall({ blocklist: [HEX_A] })
  fw.check(PK_A) // reject
  fw.check(PK_B) // accept
  fw.check(PK_C) // accept
  const s = fw.stats()
  t.is(s.accepted, 2)
  t.is(s.rejected, 1)
  fw.destroy()
})

test('SwarmFirewall — block/unblock/allow/unallow are mutually exclusive', (t) => {
  const fw = new SwarmFirewall()
  fw.block(HEX_A)
  t.is(fw.isBlocked(HEX_A), true)
  fw.unblock(HEX_A)
  t.is(fw.isBlocked(HEX_A), false)
  fw.allow(HEX_A)
  t.is(fw.isAllowed(HEX_A), true)
  fw.unallow(HEX_A)
  t.is(fw.isAllowed(HEX_A), false)
  fw.destroy()
})

test('SwarmFirewall — destroy clears interval', (t) => {
  const fw = new SwarmFirewall()
  t.ok(fw._cleanup, 'has cleanup interval')
  fw.destroy()
  t.is(fw._cleanup, null, 'interval cleared')
})

test('SwarmFirewall — null pubkey handled gracefully', (t) => {
  const fw = new SwarmFirewall({ blocklist: [HEX_A] })
  // No pubkey → cannot match block/allow lists, falls through to IP check
  t.is(fw.check(null, { remoteAddress: '3.3.3.3' }), false, 'accepts when no pubkey + no other rules trip')
  fw.destroy()
})