From 8ce036b4138493744dbd8a9ecf47d65db9e1d104 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 28 Oct 2026 15:51:08 -0400 Subject: [PATCH] Clean up config for dns servers --- Dockerfile.dns | 2 ++ server/lib/readConfigFile.ts | 9 +++++++++ server/lib/runMode.ts | 14 ++++++++++++++ server/private/dnsRunMode.ts | 18 ++++++++++++++++++ server/private/dnsServer.ts | 20 ++++++++++++++++++++ 5 files changed, 63 insertions(+) create mode 100644 server/lib/runMode.ts create mode 100644 server/private/dnsRunMode.ts diff --git a/Dockerfile.dns b/Dockerfile.dns index ad91ef7af..8ac34b78b 100644 --- a/Dockerfile.dns +++ b/Dockerfile.dns @@ -52,6 +52,8 @@ ENV ENVIRONMENT=prod ENV NODE_ENV=production EXPOSE 53/udp +# HTTP health check (GET /healthz) +EXPOSE 8053/tcp ARG VERSION="dev" ARG REVISION="" diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index d53045a9f..f7c31e455 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -4,6 +4,7 @@ import { configFilePath1, configFilePath2 } from "./consts"; import { z } from "zod"; import stoi from "./stoi"; import { getEnvOrYaml, readEnvOrFile } from "./getEnvOrYaml"; +import { getRunMode } from "./runMode"; // type-only: offlineThresholds imports config, which imports this file import type { OfflineThresholds } from "@server/lib/offlineThresholds"; @@ -325,6 +326,10 @@ export const configSchema = z base_endpoint: z .string() .optional() + // The standalone DNS server never creates exit nodes + .transform((url) => + url === undefined && getRunMode() === "dns" ? "" : url + ) .pipe(z.string()) .transform((url) => url.toLowerCase()), subnet_group: z.string().optional().default("100.89.137.0/18"), @@ -516,6 +521,10 @@ export const configSchema = z ) .refine( (data) => { + // The standalone DNS server never uses the secret + if (getRunMode() === "dns") { + return true; + } // If hybrid is not defined, server secret must be defined return ( data.server?.secret !== undefined && diff --git a/server/lib/runMode.ts b/server/lib/runMode.ts new file mode 100644 index 000000000..be1921ab2 --- /dev/null +++ b/server/lib/runMode.ts @@ -0,0 +1,14 @@ +// Which entry point is running. Standalone entry points set this before +// anything loads the config, so the config schema can skip requirements +// that only apply to the full Pangolin server. +export type RunMode = "server" | "dns"; + +let runMode: RunMode = "server"; + +export function setRunMode(mode: RunMode) { + runMode = mode; +} + +export function getRunMode(): RunMode { + return runMode; +} diff --git a/server/private/dnsRunMode.ts b/server/private/dnsRunMode.ts new file mode 100644 index 000000000..af8b3ea84 --- /dev/null +++ b/server/private/dnsRunMode.ts @@ -0,0 +1,18 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +// Imported first by dnsServer.ts: modules evaluate in import order, so this +// runs before any later import loads (and validates) the config. +import { setRunMode } from "@server/lib/runMode"; + +setRunMode("dns"); diff --git a/server/private/dnsServer.ts b/server/private/dnsServer.ts index d8280b591..9a1fd911e 100644 --- a/server/private/dnsServer.ts +++ b/server/private/dnsServer.ts @@ -14,13 +14,19 @@ // Standalone entry point that runs only the authoritative DNS server against // an existing Pangolin database, without the API, dashboard or schedulers. // Migrations are left to the main Pangolin server that owns the database. +// Must stay the first import - see dnsRunMode.ts +import "./dnsRunMode"; import "@server/extendZod"; +import http from "http"; import logger from "@server/logger"; import { privateConfig } from "#private/lib/config"; import { rateLimitService } from "#private/lib/rateLimit"; import { startDnsServer, stopDnsServer } from "./dns"; +// The DNS server only speaks UDP, which orchestrators can't probe directly +const HEALTHCHECK_PORT = 8053; + async function start() { if (!privateConfig.getRawPrivateConfig().dns?.enabled) { throw new Error( @@ -30,11 +36,25 @@ async function start() { await startDnsServer({ requireLicense: false }); + const healthcheckServer = http.createServer((req, res) => { + if (req.url === "/healthz") { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("OK"); + } else { + res.writeHead(404); + res.end(); + } + }); + healthcheckServer.listen(HEALTHCHECK_PORT, () => { + logger.info(`Healthcheck server listening on port ${HEALTHCHECK_PORT}`); + }); + let stopping = false; const shutdown = async () => { if (stopping) return; stopping = true; try { + healthcheckServer.close(); await stopDnsServer(); await rateLimitService.cleanup(); } catch (error) {