OverviewFilesCommitsIssuesDocsReleasesPeers

hyperident-gateway / test / router.test.js

146 lines · javascript ref:
/**
 * HyperswarmRouter on a private testnet DHT (no public DHT, no TUN, no root).
 *
 * Run with: npx brittle-bare test/router.test.js
 */

import test from 'brittle'
import createTestnet from 'hyperdht/testnet'
import { HyperswarmRouter } from '../lib/hyperswarm-router.js'
import { GatewayIdentity } from '../lib/gateway-identity.js'
import { UTUNManager } from '../lib/utun-manager.js'

test('routers on the same topic learn each other\'s IPs', async (t) => {
  const { bootstrap } = await createTestnet(3, t.teardown)

  const a = await createRouter(t, bootstrap, 'net-a', '10.22.0.1')
  const b = await createRouter(t, bootstrap, 'net-a', '10.22.0.2')
  const c = await createRouter(t, bootstrap, 'net-a', '10.22.0.3')

  t.ok(await waitFor(() => routes(a) === '10.22.0.2,10.22.0.3' && routes(b) === '10.22.0.1,10.22.0.3' && routes(c) === '10.22.0.1,10.22.0.2'), 'routing tables converged')

  t.is(routes(a), '10.22.0.2,10.22.0.3')
  t.is(routes(b), '10.22.0.1,10.22.0.3')
  t.is(routes(c), '10.22.0.1,10.22.0.2')
  t.alike([...a.conflicts, ...b.conflicts, ...c.conflicts], [], 'distinct IPs report no conflicts')
})

test('a duplicate claim or a claim on our own IP is reported', async (t) => {
  const { bootstrap } = await createTestnet(3, t.teardown)

  const a = await createRouter(t, bootstrap, 'net-a', '10.22.0.1')
  await createRouter(t, bootstrap, 'net-a', '10.22.0.2')
  await createRouter(t, bootstrap, 'net-a', '10.22.0.2') // misconfigured duplicate
  await createRouter(t, bootstrap, 'net-a', '10.22.0.1') // claims a's own IP

  const seen = () => a.conflicts.map((c) => c.ip + ' ' + c.reason).sort().join(' | ')

  t.ok(await waitFor(() => seen() === '10.22.0.1 is our own IP | 10.22.0.2 another peer already announced'), 'a reported both conflicts')
  t.is(seen(), '10.22.0.1 is our own IP | 10.22.0.2 another peer already announced')
  t.is(a.getStats().ipConflicts, 2)
})

test('a router on a different topic is not seen', async (t) => {
  const { bootstrap } = await createTestnet(3, t.teardown)

  const a = await createRouter(t, bootstrap, 'net-a', '10.22.0.1')
  const b = await createRouter(t, bootstrap, 'net-a', '10.22.0.2')
  const other = await createRouter(t, bootstrap, 'net-b', '10.22.0.9')

  t.ok(await waitFor(() => routes(a) === '10.22.0.2' && routes(b) === '10.22.0.1'), 'same-topic routers converged')
  await sleep(2000) // give a wrong connection time to show up

  t.is(routes(a), '10.22.0.2')
  t.is(routes(b), '10.22.0.1')
  t.is(routes(other), '')
  t.is(other.connections.size, 0)
})

test('forwardToP2P delivers the packet to the owner of the destination IP', async (t) => {
  const { bootstrap } = await createTestnet(3, t.teardown)

  const a = await createRouter(t, bootstrap, 'net-a', '10.22.0.1')
  const b = await createRouter(t, bootstrap, 'net-a', '10.22.0.2')

  t.ok(await waitFor(() => routes(a) === '10.22.0.2' && routes(b) === '10.22.0.1'), 'routers converged')

  const packet = UTUNManager.buildIPv4Packet({
    srcIP: '10.22.0.1',
    dstIP: '10.22.0.2',
    protocol: 17,
    payload: Buffer.from('hello over the gateway')
  })

  t.is(await a.forwardToP2P(packet, '10.22.0.2'), true)
  t.is(await a.forwardToP2P(packet, '10.22.0.77'), false, 'no route to an unannounced IP')

  t.ok(await waitFor(() => b.utunManager.written.length > 0), 'packet arrived at b')

  t.is(b.utunManager.written.length, 1)
  t.alike(b.utunManager.written[0], packet)
  t.is(a.utunManager.written.length, 0)
})

test('joining never logs the topic string (it is the network secret)', async (t) => {
  const { bootstrap } = await createTestnet(3, t.teardown)
  const secret = 'secret-topic-' + Math.random().toString(16).slice(2)

  const logged = []
  const original = console.log
  console.log = (...args) => { logged.push(args.join(' ')); original(...args) }
  try {
    await createRouter(t, bootstrap, secret, '10.22.0.1')
  } finally {
    console.log = original
  }

  t.ok(logged.some((l) => l.includes('Topic hash:')), 'the join is still logged, by hash')
  t.absent(logged.some((l) => l.includes(secret)), 'the topic string is not')
})

async function createRouter (t, bootstrap, networkTopic, ip) {
  const conflicts = []

  const router = new HyperswarmRouter({
    gatewayIdentity: new GatewayIdentity({ gatewayIP: ip }),
    networkTopic,
    swarmOptions: { bootstrap },
    onError: (err) => t.fail(err.message),
    onConflict: (conflict) => conflicts.push(conflict)
  })

  router.conflicts = conflicts

  // Stands in for UTUNManager so no TUN device is needed
  router.utunManager = {
    written: [],
    writePacket (packet) { this.written.push(packet) }
  }

  t.teardown(() => router.stop())

  await router.start()
  await router.joinNetwork()

  return router
}

function routes (router) {
  return [...router.routingTable.keys()].sort().join(',')
}

// Resolves true once condition() holds, false on timeout (never throws, so a
// failure is reported as a failed assertion instead of aborting the run)
async function waitFor (condition, timeout = 15000) {
  const start = Date.now()
  while (!condition()) {
    if (Date.now() - start > timeout) return false
    await sleep(50)
  }
  return true
}

function sleep (ms) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}