Files
peardata/server/services/rest-tunnel.js
T
Raven Scott 3fc0245b32
CI / test (push) Successful in 1m3s
Release rolling / release (push) Has been cancelled
Updates
2026-07-18 23:47:54 -04:00

133 lines
3.4 KiB
JavaScript

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