/** * Core agent logic — registers with TIP routing server, * starts SOCKS5 server, manages heartbeat and graceful shutdown. */ import * as https from "https"; import * as http from "http"; import * as os from "os"; import { createSocks5Server } from "./socks5"; import { BandwidthTracker } from "./bandwidth"; import { startHeartbeatLoop } from "./heartbeat"; import { saveConfig, savePid, clearPid, AgentConfig } from "./config"; const AGENT_VERSION = "1.0.0"; interface RegisterResponse { token: string; nodeId: string; message?: string; } function postJson(url: string, body: unknown): Promise<{ status: number; data: unknown }> { return new Promise((resolve, reject) => { const json = JSON.stringify(body); const parsed = new URL(url); const mod = parsed.protocol === "https:" ? https : http; const req = mod.request( { hostname: parsed.hostname, port: parsed.port || (parsed.protocol === "https:" ? 443 : 80), path: parsed.pathname + parsed.search, method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(json), "User-Agent": `TIP-Agent/${AGENT_VERSION}`, }, timeout: 15_000, }, (res) => { const chunks: Buffer[] = []; res.on("data", (c: Buffer) => chunks.push(c)); res.on("end", () => { try { const data = JSON.parse(Buffer.concat(chunks).toString()); resolve({ status: res.statusCode ?? 0, data }); } catch { resolve({ status: res.statusCode ?? 0, data: {} }); } }); } ); req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); }); req.on("error", reject); req.write(json); req.end(); }); } async function registerNode(cfg: AgentConfig): Promise { const url = `${cfg.serverUrl}/api/proxy/register`; console.log(`[agent] Registering with ${url}...`); const { status, data } = await postJson(url, { token: cfg.token, name: cfg.name, port: cfg.port, version: AGENT_VERSION, }); if (status >= 200 && status < 300) { const resp = data as RegisterResponse; console.log(`[agent] Registered successfully. Node: ${resp.nodeId ?? "unknown"}`); return resp; } throw new Error(`Registration failed: HTTP ${status}`); } export async function startAgent(cfg: AgentConfig): Promise { const bandwidth = new BandwidthTracker(cfg.maxGb); // Register with server await registerNode(cfg).catch((err) => { console.warn(`[agent] Registration warning: ${err.message} (continuing anyway)`); }); // Start SOCKS5 server const server = createSocks5Server({ port: cfg.port, bandwidth, onNewConnection: () => { if (bandwidth.isLimitReached()) { console.warn("[agent] Bandwidth limit reached — rejecting new connections"); return false; } return true; }, }); await new Promise((resolve, reject) => { server.listen(cfg.port, () => { console.log(`[agent] SOCKS5 proxy listening on port ${cfg.port}`); resolve(); }); server.on("error", reject); }); // Save PID savePid(process.pid); saveConfig({ ...cfg, pid: process.pid, startedAt: new Date().toISOString() }); // Start heartbeat const heartbeatTimer = startHeartbeatLoop( cfg.serverUrl, () => { const stats = bandwidth.getStats(); return { token: cfg.token, port: cfg.port, bytesProxied: stats.bytesProxied, requestsProxied: stats.requestsProxied, version: AGENT_VERSION, }; }, (result) => { if (!result.ok) { console.warn(`[agent] Heartbeat failed: ${result.error ?? "unknown error"}`); } } ); console.log(`[agent] TIP Agent running. Max bandwidth: ${cfg.maxGb} GB`); console.log(`[agent] Press Ctrl+C to stop.\n`); // Print stats every minute const statsTimer = setInterval(() => { const stats = bandwidth.getStats(); const usedGb = (stats.bytesProxied / (1024 ** 3)).toFixed(3); console.log( `[agent] Stats: ${stats.requestsProxied} requests, ${usedGb} GB used / ${cfg.maxGb} GB limit` ); }, 60_000); // Graceful shutdown const shutdown = (signal: string) => { console.log(`\n[agent] Received ${signal}, shutting down...`); clearInterval(heartbeatTimer); clearInterval(statsTimer); clearPid(); server.close(() => { console.log("[agent] SOCKS5 server closed. Goodbye."); process.exit(0); }); setTimeout(() => process.exit(1), 5000); }; process.on("SIGINT", () => shutdown("SIGINT")); process.on("SIGTERM", () => shutdown("SIGTERM")); } export function getLocalIp(): string { const interfaces = os.networkInterfaces(); for (const iface of Object.values(interfaces)) { if (!iface) continue; for (const addr of iface) { if (addr.family === "IPv4" && !addr.internal) { return addr.address; } } } return "127.0.0.1"; }