/**
* 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))
}