mirror of
https://github.com/fosrl/pangolin.git
synced 2026-10-30 15:39:06 +01:00
Standalone dns server
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user