/** * Holesail-style HyperDHT TCP tunnel for the local REST API. * * Enable: PEARDATA_REST_TUNNEL=1 * Optional: PEARDATA_REST_TUNNEL_SEED=<64 hex> for a stable public key * * Peers connect with HyperDHT to the tunnel public key; each connection is * proxied to PEARDATA_REST_HOST:PEARDATA_REST_PORT (default 127.0.0.1:18888). * * Uses existing `hyperdht` — no holesail package dependency (Bare-friendly). */ import net from 'net' import DHT from 'hyperdht' import b4a from 'b4a' import logger from '../utils/logger.js' const log = logger.child('rest-tunnel') /** @type {import('hyperdht')|null} */ let dht = null /** @type {any} */ let server = null /** @type {string|null} */ let publicKeyHex = null export function isRestTunnelEnabled() { const v = process.env.PEARDATA_REST_TUNNEL return v === '1' || v === 'on' || v === 'true' } function tunnelKeyPair() { const seedHex = process.env.PEARDATA_REST_TUNNEL_SEED if (seedHex && /^[0-9a-fA-F]{64}$/.test(seedHex)) { return DHT.keyPair(b4a.from(seedHex, 'hex')) } return DHT.keyPair() } /** * @returns {Promise<{ publicKeyHex: string, target: string }|null>} */ export async function startRestTunnel() { if (!isRestTunnelEnabled()) { log.info('REST tunnel disabled (set PEARDATA_REST_TUNNEL=1)') return null } if (server) { return { publicKeyHex, target: `${process.env.PEARDATA_REST_HOST || '127.0.0.1'}:${Number(process.env.PEARDATA_REST_PORT) || 18888}`, } } const host = process.env.PEARDATA_REST_HOST || '127.0.0.1' const port = Number(process.env.PEARDATA_REST_PORT) || 18888 const keyPair = tunnelKeyPair() publicKeyHex = b4a.toString(keyPair.publicKey, 'hex') dht = new DHT() server = dht.createServer() server.on('connection', (socket) => { const remote = socket.remotePublicKey ? b4a.toString(socket.remotePublicKey, 'hex').slice(0, 12) : '?' log.info('Tunnel peer connected', { remote }) const local = net.connect({ host, port }) const cleanup = () => { try { socket.destroy() } catch { // ignore } try { local.destroy() } catch { // ignore } } socket.on('error', cleanup) local.on('error', cleanup) socket.on('close', cleanup) local.on('close', cleanup) socket.pipe(local).pipe(socket) }) await server.listen(keyPair) log.info('REST tunnel listening on HyperDHT', { publicKeyHex, target: `${host}:${port}`, tip: `Dial HyperDHT ${publicKeyHex} → localhost REST`, }) return { publicKeyHex, target: `${host}:${port}` } } export function getRestTunnelInfo() { if (!publicKeyHex) return { enabled: isRestTunnelEnabled(), active: false } return { enabled: true, active: Boolean(server), publicKeyHex, target: `${process.env.PEARDATA_REST_HOST || '127.0.0.1'}:${Number(process.env.PEARDATA_REST_PORT) || 18888}`, seedConfigured: Boolean(process.env.PEARDATA_REST_TUNNEL_SEED), } } export async function stopRestTunnel() { if (server) { try { await server.close() } catch { // ignore } server = null } if (dht) { try { await dht.destroy() } catch { // ignore } dht = null } publicKeyHex = null log.info('REST tunnel stopped') } /** Exported for tests — stable key from seed */ export function publicKeyFromTunnelSeed(seedHex) { const kp = DHT.keyPair(b4a.from(seedHex, 'hex')) return b4a.toString(kp.publicKey, 'hex') }