/** * anisail-tunnel: Bare-compatible Holesail client that binds a local port and * prints it to stdout for the Ansible connection plugin. Keeps running until * stdin closes or SIGTERM/SIGINT. * * Usage: anisail-tunnel --key [--remote-port 22] [--local-port 0] [--timeout 30000] * Output: exactly one JSON line to stdout: {"local_port": , "ready": true} */ import 'bare-process/global'; const DEFAULT_REMOTE_PORT = 22; const DEFAULT_LOCAL_PORT = 0; const DEFAULT_TIMEOUT_MS = 30000; const LOCAL_HOST = '127.0.0.1'; function parseArgs() { // slice(1): when run as a bundled binary, argv is [binaryPath, ...cliArgs]; slice(2) would drop --key const args = process.argv.slice(1); let key = null; let remotePort = DEFAULT_REMOTE_PORT; let localPort = DEFAULT_LOCAL_PORT; let timeoutMs = DEFAULT_TIMEOUT_MS; for (let i = 0; i < args.length; i++) { if (args[i] === '--key' && args[i + 1]) { key = args[++i].trim(); } else if (args[i] === '--remote-port' && args[i + 1]) { remotePort = parseInt(args[++i], 10) || DEFAULT_REMOTE_PORT; } else if (args[i] === '--local-port' && args[i + 1]) { localPort = parseInt(args[++i], 10); if (isNaN(localPort) || localPort < 0) localPort = DEFAULT_LOCAL_PORT; } else if (args[i] === '--timeout' && args[i + 1]) { timeoutMs = parseInt(args[++i], 10) || DEFAULT_TIMEOUT_MS; } } return { key, remotePort, localPort, timeoutMs }; } function emit(line) { try { process.stdout.write(line + '\n'); } catch (_) {} } /** * Verify that the port is accepting TCP connections before we report it. * Tries to connect to host:port; resolves when connect succeeds (then we close the socket). * Rejects on error or after timeoutMs. */ function waitUntilPortAccepting(host, port, timeoutMs = 10000, intervalMs = 200) { const deadline = Date.now() + timeoutMs; return new Promise((resolve, reject) => { function tryConnect() { if (Date.now() >= deadline) { reject(new Error('Port did not accept connections within ' + timeoutMs + 'ms')); return; } import('net').then((net) => { const socket = net.createConnection( { port, host, allowHalfOpen: false }, () => { socket.setTimeout(0); socket.destroy(); resolve(); } ); socket.on('error', () => { socket.destroy(); setTimeout(tryConnect, intervalMs); }); socket.setTimeout(intervalMs, () => { socket.destroy(); setTimeout(tryConnect, intervalMs); }); }).catch(reject); } tryConnect(); }); } async function run() { const mod = await import('holesail'); const Holesail = mod.default || mod; const { key, remotePort, localPort, timeoutMs } = parseArgs(); if (!key || key.length === 0) { const msg = 'anisail-tunnel: --key is required'; try { process.stderr.write(msg + '\n'); } catch (_) {} process.exitCode = 1; return; } const portToUse = (localPort > 0 ? localPort : 19200 + Math.floor(Math.random() * 1000)) || 19200; const readyPromise = new Promise((resolve, reject) => { const t = timeoutMs > 0 ? setTimeout(() => reject(new Error('Tunnel ready timeout after ' + timeoutMs + 'ms')), timeoutMs) : null; const done = (err, port) => { if (t) clearTimeout(t); if (err) reject(err); else resolve(port); }; const opts = { client: true, key, host: LOCAL_HOST, port: portToUse }; // If the Holesail API supports remotePort, tell it which port on the peer to use (e.g. 22 for SSH). if (remotePort > 0 && remotePort <= 65535) { opts.remotePort = remotePort; } const hs = new Holesail(opts); if (typeof hs.on === 'function') { hs.on('error', (err) => { done(err || new Error('Tunnel error')); }); } hs.ready() .then(() => { const info = typeof hs.info === 'function' ? hs.info() : (hs.info || {}); const boundPort = (info && typeof info.port === 'number') ? info.port : portToUse; if (boundPort == null || boundPort < 1 || boundPort > 65535) { done(new Error('Could not determine local tunnel port')); return; } done(null, boundPort); return hs; }) .catch(done); run._hs = hs; }); let hs; try { const port = await readyPromise; hs = run._hs; // Verify the port is actually accepting connections before reporting it await waitUntilPortAccepting(LOCAL_HOST, port, 10000, 200); emit(JSON.stringify({ local_port: port, ready: true })); } catch (err) { const msg = (err && err.message) ? err.message : String(err); try { process.stderr.write('anisail-tunnel: ' + msg + '\n'); } catch (_) {} process.exitCode = 1; return; } const close = () => { if (hs && typeof hs.close === 'function') { try { hs.close(); } catch (_) {} hs = null; } process.exitCode = 0; process.exit(0); }; process.on('SIGTERM', close); process.on('SIGINT', close); process.stdin.on('close', () => { close(); }); process.stdin.on('end', () => { close(); }); process.stdin.resume(); } run().catch((err) => { try { process.stderr.write('anisail-tunnel: ' + (err && err.message ? err.message : String(err)) + '\n'); } catch (_) {} process.exitCode = 1; });