/* * remchild server — proof of concept. * * Listens for remchild clients on a dedicated port, accepts only known IPs, * records heartbeats, and keeps a history of first and last seen. The proveout * runs standalone; the plan moves it into the daemon with settings and the * launch-arg unlock. * * Run: node remchild/server/server.mjs --port 8799 --ips 127.0.0.1,192.168.1.5 * node remchild/server/server.mjs --allow-non-lan --ips 203.0.113.7 * node remchild/server/server.mjs --allow-all # testing: any address */ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; import net from "node:net"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const HERE = dirname(fileURLToPath(import.meta.url)); const STALE_MS = 12 * 60 * 1000; // no jobs after this const GONE_MS = 30 * 60 * 1000; // considered gone after this function arg(name, fallback = undefined) { const index = process.argv.indexOf(`--${name}`); if (index < 0) return fallback; const value = process.argv[index + 1]; return value && !value.startsWith("--") ? value : true; } const port = Number(arg("port", process.env.REMCHILD_PORT ?? 8799)); const allowNonLan = arg("allow-non-lan", false) !== false; const allowAll = arg("allow-all", false) !== false; const historyFile = arg("history", join(HERE, "clients.jsonl")); const configFile = arg("config", join(HERE, "config.json")); let configured = []; if (typeof arg("ips") === "string") { configured = String(arg("ips")).split(",").map(value => value.trim()).filter(Boolean); } else if (existsSync(configFile)) { try { const config = JSON.parse(readFileSync(configFile, "utf8")); configured = Array.isArray(config.ips) ? config.ips.map(String) : []; } catch (error) { console.error(`[remchild] bad config ${configFile}: ${error.message}`); } } function isLan(address) { const parts = address.split(".").map(Number); if (parts.length !== 4 || parts.some(value => !Number.isInteger(value))) return false; if (parts[0] === 10 || parts[0] === 127) return true; if (parts[0] === 192 && parts[1] === 168) return true; if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) return true; // Tailscale return false; } /** The allowlist rule: known IPs only; non-LAN entries need the launch flag. */ function allowed(address) { if (allowAll) return true; if (configured.length === 0) return isLan(address); return configured.includes(address); } const clients = new Map(); mkdirSync(dirname(historyFile), { recursive: true }); function history(entry) { try { appendFileSync(historyFile, JSON.stringify({ at: new Date().toISOString(), ...entry }) + "\n"); } catch (error) { console.error(`[remchild] history write failed: ${error.message}`); } } function stateOf(client, now) { if (now - client.lastSeen > GONE_MS) return "gone"; if (now - client.lastSeen > STALE_MS) return "stale"; return "active"; } function touch(client, reason) { client.lastSeen = Date.now(); const state = stateOf(client, client.lastSeen); if (state !== client.state) { console.log(`[remchild] ${client.name} is ${state}`); history({ event: state, id: client.id, name: client.name }); client.state = state; } else if (reason !== "heartbeat") { console.log(`[remchild] ${client.name} ${reason}`); } } setInterval(() => { const now = Date.now(); for (const client of clients.values()) { const state = stateOf(client, now); if (state !== client.state) { if (state === "stale") console.log(`[remchild] ${client.name} is stale (no heartbeat for 12 min)`); if (state === "gone") console.log(`[remchild] ${client.name} is gone (no heartbeat for 30 min)`); history({ event: state, id: client.id, name: client.name }); client.state = state; } } }, 30_000).unref(); const server = net.createServer(socket => { const address = socket.remoteAddress?.replace(/^::ffff:/, "") ?? "unknown"; if (!allowed(address)) { console.log(`[remchild] rejected ${address}${configured.length ? " (not in the allowlist)" : " (not LAN)"}`); socket.end(JSON.stringify({ type: "reject", reason: "not allowed" }) + "\n"); return; } if (!allowAll && configured.length && !isLan(address) && allowNonLan !== true) { console.log(`[remchild] rejected ${address} (non-LAN address needs --allow-non-lan)`); socket.end(JSON.stringify({ type: "reject", reason: "non-lan locked" }) + "\n"); return; } console.log(`[remchild] connection from ${address}`); let buffer = ""; socket.on("data", chunk => { buffer += chunk.toString("utf8"); let newline; while ((newline = buffer.indexOf("\n")) >= 0) { const line = buffer.slice(0, newline).trim(); buffer = buffer.slice(newline + 1); if (!line) continue; let message; try { message = JSON.parse(line); } catch { console.log(`[remchild] bad line from ${address}: ${line.slice(0, 80)}`); continue; } if (message.type === "hello") { const id = String(message.id ?? address); const existing = clients.get(id); const client = { id, name: String(message.name ?? "Unnamed"), platform: String(message.platform ?? "unknown"), address, socket, firstSeen: existing?.firstSeen ?? Date.now(), lastSeen: Date.now(), heartbeats: 0, state: "active" }; clients.set(id, client); history({ event: "hello", id, name: client.name, address, platform: client.platform }); console.log(`[remchild] ${client.name} (${id}) connected from ${address}`); socket.write( JSON.stringify({ type: "welcome", name: client.name, heartbeatMs: 300000, server: "veskslop-remchild-poc" }) + "\n" ); continue; } if (message.type === "heartbeat") { const client = [...clients.values()].find(item => item.socket === socket); if (client) { client.heartbeats++; touch(client, "heartbeat"); } continue; } if (message.type === "ping") { socket.write(JSON.stringify({ type: "pong", seq: message.seq ?? 0 }) + "\n"); continue; } if (message.type === "pong") { const client = [...clients.values()].find(item => item.socket === socket); if (client) console.log(`[remchild] ${client.name} pong seq=${message.seq ?? 0}`); continue; } console.log(`[remchild] ${address}: ${message.type}`); } }); socket.on("close", () => { const client = [...clients.values()].find(item => item.socket === socket); if (client) { client.socket = undefined; history({ event: "disconnect", id: client.id, name: client.name, address: client.address, lastSeen: new Date(client.lastSeen).toISOString() }); console.log(`[remchild] ${client.name} disconnected`); } }); socket.on("error", () => {}); }); server.listen(port, () => { console.log(`[remchild] listening on 0.0.0.0:${port}`); console.log( `[remchild] allowlist: ${ allowAll ? "ALL (testing mode)" : configured.length ? configured.join(", ") : "LAN only (no list configured)" }` ); if (allowAll) console.log("[remchild] WARNING: every address is accepted; there is no auth"); else if (allowNonLan === true) console.log("[remchild] non-LAN addresses are unlocked"); }); process.on("SIGINT", () => { console.log("\n[remchild] client history:"); for (const client of clients.values()) { console.log( ` ${client.name} first ${new Date(client.firstSeen).toISOString()} last ${new Date(client.lastSeen).toISOString()}` ); } process.exit(0); });