Standalone dns server

This commit is contained in:
Owen
2026-10-28 15:41:31 -04:00
parent 0be847ce9e
commit 3dd46e48ec
5 changed files with 193 additions and 10 deletions
+19 -8
View File
@@ -36,7 +36,7 @@ import {
import { privateConfig as config } from "#private/lib/config";
import * as dnsResolver from "dns";
import logger from "@server/logger";
import { listExitNodes } from "../exitNodes";
import { listExitNodesAcrossRegions } from "../exitNodes/exitNodes";
import { parseCustomDnsRecordValues } from "@server/routers/customDnsRecord/types";
import {
matchCustomRecords,
@@ -44,7 +44,6 @@ import {
type CustomZoneRecord
} from "./customRecordMatch";
import { rateLimitService } from "../rateLimit";
import license from "#private/license/license";
type DNSRecord = {
id: number;
@@ -89,6 +88,7 @@ export class AuthoritativeDNSServer {
// set refresh and read from memory here.
private isLicensed: boolean = false;
private licenseRefreshInterval: NodeJS.Timeout | null = null;
private requireLicense: boolean;
// Cache for per-queryName zone resolution and SOA records
private authoritativeDomainCache: LocalCache = createLocalCache(
@@ -100,8 +100,9 @@ export class AuthoritativeDNSServer {
DNS_CACHE_TTL_SECONDS
);
constructor(port: number) {
constructor(port: number, options: { requireLicense?: boolean } = {}) {
this.port = port;
this.requireLicense = options.requireLicense ?? true;
this.server = dgram.createSocket("udp4");
this.setupServer();
@@ -965,7 +966,7 @@ export class AuthoritativeDNSServer {
queryType: dns.RecordType
): Promise<DNSRecord[] | null> {
// TODO: EVENTUALLY WE SHOULD PICK THE CLOSEST EXIT NODE TO THE USER BASED ON LATENCY
const exitNodesList = await listExitNodes(orgId, true);
const exitNodesList = await listExitNodesAcrossRegions(orgId, true);
if (!exitNodesList || exitNodesList.length === 0) {
logger.warn(`No exit nodes found for orgId: ${orgId}`);
@@ -1549,6 +1550,12 @@ export class AuthoritativeDNSServer {
private async refreshLicenseStatus(): Promise<void> {
try {
// Imported lazily so the standalone DNS server, which runs
// without a license, never loads the license module (it touches
// the database and starts phoning home as soon as it is imported)
const { default: license } = await import(
"#private/license/license"
);
this.isLicensed = await license.hasTier([
"personal",
"tier2",
@@ -1568,10 +1575,14 @@ export class AuthoritativeDNSServer {
);
}, 60_000);
await this.refreshLicenseStatus();
this.licenseRefreshInterval = setInterval(() => {
this.refreshLicenseStatus();
}, 60_000);
if (this.requireLicense) {
await this.refreshLicenseStatus();
this.licenseRefreshInterval = setInterval(() => {
this.refreshLicenseStatus();
}, 60_000);
} else {
this.isLicensed = true;
}
return new Promise((resolve, reject) => {
this.server.bind(this.port, (err?: Error) => {
+46
View File
@@ -333,6 +333,52 @@ export async function listExitNodes(
return exitNodesList;
}
// Like listExitNodes, but gerbil nodes are not restricted to this server's
// region, so a caller that prefers same-region nodes (e.g. the DNS server)
// can still fall back to another region when this one has none online.
export async function listExitNodesAcrossRegions(
orgId: string,
filterOnline = false
) {
const allExitNodes = await db
.select({
exitNodeId: exitNodes.exitNodeId,
endpoint: exitNodes.endpoint,
online: exitNodes.online,
type: exitNodes.type,
region: exitNodes.region
})
.from(exitNodes)
.leftJoin(
exitNodeOrgs,
eq(exitNodes.exitNodeId, exitNodeOrgs.exitNodeId)
)
.where(
or(
eq(exitNodes.type, "gerbil"),
and(
eq(exitNodes.type, "remoteExitNode"),
eq(exitNodeOrgs.orgId, orgId)
)
)
);
const onlineNodes = allExitNodes.filter(
(node) => !filterOnline || node.online
);
const remoteExitNodesList = onlineNodes.filter(
(node) => node.type === "remoteExitNode"
);
const gerbilExitNodes = onlineNodes.filter(
(node) => node.type === "gerbil"
);
// An org's own remote exit nodes take precedence over the cloud ones
return remoteExitNodesList.length > 0
? remoteExitNodesList
: gerbilExitNodes;
}
export type ExitNodePingResult = {
exitNodeId: number;
latencyMs: number;