import logger from "@server/logger"; import config from "@server/lib/config"; import { createWebSocketClient } from "./routers/ws/client"; import { addPeer, deletePeer } from "./routers/gerbil/peers"; import { db, exitNodes } from "./db"; import { TraefikConfigManager } from "./lib/traefikConfig"; import { tokenManager } from "./lib/tokenManager"; import { APP_VERSION } from "./lib/consts"; import axios from "axios"; export async function createHybridClientServer() { logger.info("Starting hybrid client server..."); // Start the token manager await tokenManager.start(); const token = await tokenManager.getToken(); const monitor = new TraefikConfigManager(); await monitor.start(); // Create client const client = createWebSocketClient( token, config.getRawConfig().hybrid!.endpoint!, { reconnectInterval: 5000, pingInterval: 30000, pingTimeout: 10000 } ); // Register message handlers client.registerHandler("remoteExitNode/peers/add", async (message) => { const { publicKey, allowedIps } = message.data; // TODO: we are getting the exit node twice here // NOTE: there should only be one gerbil registered so... const [exitNode] = await db.select().from(exitNodes).limit(1); await addPeer(exitNode.exitNodeId, { publicKey: publicKey, allowedIps: allowedIps || [] }); }); client.registerHandler("remoteExitNode/peers/remove", async (message) => { const { publicKey } = message.data; // TODO: we are getting the exit node twice here // NOTE: there should only be one gerbil registered so... const [exitNode] = await db.select().from(exitNodes).limit(1); await deletePeer(exitNode.exitNodeId, publicKey); }); // /update-proxy-mapping client.registerHandler("remoteExitNode/update-proxy-mapping", async (message) => { try { const [exitNode] = await db.select().from(exitNodes).limit(1); if (!exitNode) { logger.error("No exit node found for proxy mapping update"); return; } const response = await axios.post(`${exitNode.endpoint}/update-proxy-mapping`, message.data); logger.info(`Successfully updated proxy mapping: ${response.status}`); } catch (error) { // pull data out of the axios error to log if (axios.isAxiosError(error)) { logger.error("Error updating proxy mapping:", { message: error.message, code: error.code, status: error.response?.status, statusText: error.response?.statusText, url: error.config?.url, method: error.config?.method }); } else { logger.error("Error updating proxy mapping:", error); } } }); // /update-destinations client.registerHandler("remoteExitNode/update-destinations", async (message) => { try { const [exitNode] = await db.select().from(exitNodes).limit(1); if (!exitNode) { logger.error("No exit node found for destinations update"); return; } const response = await axios.post(`${exitNode.endpoint}/update-destinations`, message.data); logger.info(`Successfully updated destinations: ${response.status}`); } catch (error) { // pull data out of the axios error to log if (axios.isAxiosError(error)) { logger.error("Error updating destinations:", { message: error.message, code: error.code, status: error.response?.status, statusText: error.response?.statusText, url: error.config?.url, method: error.config?.method }); } else { logger.error("Error updating destinations:", error); } } }); client.registerHandler("remoteExitNode/traefik/reload", async (message) => { await monitor.HandleTraefikConfig(); }); // Listen to connection events client.on("connect", () => { logger.info("Connected to WebSocket server"); client.sendMessage("remoteExitNode/register", { remoteExitNodeVersion: APP_VERSION }); }); client.on("disconnect", () => { logger.info("Disconnected from WebSocket server"); }); client.on("message", (message) => { logger.info( `Received message: ${message.type} ${JSON.stringify(message.data)}` ); }); // Connect to the server try { await client.connect(); logger.info("Connection initiated"); } catch (error) { logger.error("Failed to connect:", error); } client.sendMessageInterval( "remoteExitNode/ping", { timestamp: Date.now() / 1000 }, 60000 ); // send every minute }