2024-06-02 15:34:19 +02:00
|
|
|
import * as plugins from './plugins.js';
|
2026-02-20 15:18:30 +00:00
|
|
|
import { RustDnsBridge, type IDnsQueryEvent, type IIpcDnsAnswer, type IIpcDnsQuestion, type IRustDnsConfig } from './classes.rustdnsbridge.js';
|
2024-06-02 15:34:19 +02:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
export interface IDnsServerOptions {
|
2024-06-02 15:34:19 +02:00
|
|
|
httpsKey: string;
|
|
|
|
|
httpsCert: string;
|
|
|
|
|
httpsPort: number;
|
|
|
|
|
udpPort: number;
|
2024-09-19 18:51:34 +02:00
|
|
|
dnssecZone: string;
|
2025-05-28 19:03:45 +00:00
|
|
|
udpBindInterface?: string;
|
|
|
|
|
httpsBindInterface?: string;
|
2025-05-28 19:16:54 +00:00
|
|
|
// New options for independent manual socket control
|
|
|
|
|
manualUdpMode?: boolean;
|
|
|
|
|
manualHttpsMode?: boolean;
|
2025-05-30 18:20:55 +00:00
|
|
|
// Primary nameserver for SOA records (defaults to ns1.{dnssecZone})
|
|
|
|
|
primaryNameserver?: string;
|
2025-09-12 17:32:03 +00:00
|
|
|
// Local handling for RFC 6761 localhost (default: true)
|
|
|
|
|
enableLocalhostHandling?: boolean;
|
2024-09-19 18:51:34 +02:00
|
|
|
}
|
|
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
export interface DnsAnswer {
|
2024-09-19 18:51:34 +02:00
|
|
|
name: string;
|
|
|
|
|
type: string;
|
|
|
|
|
class: string | number;
|
|
|
|
|
ttl: number;
|
|
|
|
|
data: any;
|
2024-06-02 15:34:19 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-12 23:52:46 +00:00
|
|
|
export interface IDnsQuestion {
|
|
|
|
|
name: string;
|
|
|
|
|
type: string;
|
|
|
|
|
class?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
export interface IDnsHandler {
|
2024-09-18 19:28:28 +02:00
|
|
|
domainPattern: string;
|
|
|
|
|
recordTypes: string[];
|
2026-02-12 23:52:46 +00:00
|
|
|
handler: (question: IDnsQuestion) => DnsAnswer | null;
|
2024-09-19 18:51:34 +02:00
|
|
|
}
|
|
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
// Let's Encrypt related interfaces
|
|
|
|
|
interface LetsEncryptOptions {
|
|
|
|
|
email?: string;
|
|
|
|
|
staging?: boolean;
|
|
|
|
|
certDir?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 15:18:30 +00:00
|
|
|
export interface IDnsQueryCompletedEvent {
|
|
|
|
|
/** The original questions from the query */
|
|
|
|
|
questions: IIpcDnsQuestion[];
|
|
|
|
|
/** Whether any handler answered the query */
|
|
|
|
|
answered: boolean;
|
|
|
|
|
/** How long handler resolution took (ms) */
|
|
|
|
|
responseTimeMs: number;
|
|
|
|
|
/** Timestamp of the query */
|
|
|
|
|
timestamp: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class DnsServer extends plugins.events.EventEmitter {
|
2026-02-11 11:24:10 +00:00
|
|
|
private bridge: RustDnsBridge;
|
2024-09-18 19:28:28 +02:00
|
|
|
private handlers: IDnsHandler[] = [];
|
2024-06-02 15:34:19 +02:00
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
// Track initialization state
|
|
|
|
|
private bridgeSpawned: boolean = false;
|
2024-09-19 18:51:34 +02:00
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
// Legacy server references (kept for backward-compatible test access)
|
|
|
|
|
private httpsServer: any = null;
|
|
|
|
|
private udpServer: any = null;
|
2025-05-28 19:16:54 +00:00
|
|
|
|
2024-09-19 18:51:34 +02:00
|
|
|
constructor(private options: IDnsServerOptions) {
|
2026-02-20 15:18:30 +00:00
|
|
|
super();
|
2026-02-11 11:24:10 +00:00
|
|
|
this.bridge = new RustDnsBridge();
|
2024-09-19 18:51:34 +02:00
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
// Wire up the dnsQuery event to run TypeScript handlers
|
|
|
|
|
this.bridge.on('dnsQuery', async (event: IDnsQueryEvent) => {
|
|
|
|
|
try {
|
2026-02-20 15:18:30 +00:00
|
|
|
const startTime = Date.now();
|
2026-02-11 11:24:10 +00:00
|
|
|
const answers = this.resolveQuery(event);
|
2026-02-20 15:18:30 +00:00
|
|
|
const responseTimeMs = Date.now() - startTime;
|
|
|
|
|
this.emit('query', {
|
|
|
|
|
questions: event.questions,
|
|
|
|
|
answered: answers.answered,
|
|
|
|
|
responseTimeMs,
|
|
|
|
|
timestamp: startTime,
|
|
|
|
|
} satisfies IDnsQueryCompletedEvent);
|
2026-02-11 11:24:10 +00:00
|
|
|
await this.bridge.sendQueryResult(
|
|
|
|
|
event.correlationId,
|
|
|
|
|
answers.answers,
|
|
|
|
|
answers.answered
|
|
|
|
|
);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Error handling DNS query:', err);
|
|
|
|
|
try {
|
|
|
|
|
await this.bridge.sendQueryResult(event.correlationId, [], false);
|
|
|
|
|
} catch (sendErr) {
|
|
|
|
|
console.error('Error sending empty query result:', sendErr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-09-19 18:51:34 +02:00
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
/**
|
|
|
|
|
* Register a DNS handler for a domain pattern and record types.
|
|
|
|
|
*/
|
|
|
|
|
public registerHandler(
|
|
|
|
|
domainPattern: string,
|
|
|
|
|
recordTypes: string[],
|
2026-02-12 23:52:46 +00:00
|
|
|
handler: (question: IDnsQuestion) => DnsAnswer | null
|
2026-02-11 11:24:10 +00:00
|
|
|
): void {
|
|
|
|
|
this.handlers.push({ domainPattern, recordTypes, handler });
|
2024-09-19 18:51:34 +02:00
|
|
|
}
|
2024-06-02 15:34:19 +02:00
|
|
|
|
2025-05-28 19:16:54 +00:00
|
|
|
/**
|
2026-02-11 11:24:10 +00:00
|
|
|
* Unregister a specific handler.
|
2025-05-28 19:16:54 +00:00
|
|
|
*/
|
2026-02-11 11:24:10 +00:00
|
|
|
public unregisterHandler(domainPattern: string, recordTypes: string[]): boolean {
|
|
|
|
|
const initialLength = this.handlers.length;
|
|
|
|
|
this.handlers = this.handlers.filter(handler =>
|
|
|
|
|
!(handler.domainPattern === domainPattern &&
|
|
|
|
|
recordTypes.every(type => handler.recordTypes.includes(type)))
|
|
|
|
|
);
|
|
|
|
|
return this.handlers.length < initialLength;
|
2025-05-28 19:16:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-11 11:24:10 +00:00
|
|
|
* Start the DNS server.
|
2025-05-28 19:16:54 +00:00
|
|
|
*/
|
2026-02-11 11:24:10 +00:00
|
|
|
public async start(): Promise<void> {
|
|
|
|
|
// Validate interface addresses if provided
|
|
|
|
|
const udpInterface = this.options.udpBindInterface || '0.0.0.0';
|
|
|
|
|
const httpsInterface = this.options.httpsBindInterface || '0.0.0.0';
|
|
|
|
|
|
|
|
|
|
if (this.options.udpBindInterface && !this.isValidIpAddress(this.options.udpBindInterface)) {
|
|
|
|
|
throw new Error(`Invalid UDP bind interface: ${this.options.udpBindInterface}`);
|
2025-05-28 19:16:54 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
if (this.options.httpsBindInterface && !this.isValidIpAddress(this.options.httpsBindInterface)) {
|
|
|
|
|
throw new Error(`Invalid HTTPS bind interface: ${this.options.httpsBindInterface}`);
|
|
|
|
|
}
|
2025-05-28 19:16:54 +00:00
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
// Spawn the Rust binary
|
|
|
|
|
if (!this.bridgeSpawned) {
|
|
|
|
|
const spawned = await this.bridge.spawn();
|
|
|
|
|
if (!spawned) {
|
|
|
|
|
throw new Error('Failed to spawn rustdns binary');
|
|
|
|
|
}
|
|
|
|
|
this.bridgeSpawned = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build config for Rust
|
|
|
|
|
const config: IRustDnsConfig = {
|
|
|
|
|
udpPort: this.options.udpPort,
|
|
|
|
|
httpsPort: this.options.httpsPort,
|
|
|
|
|
udpBindInterface: udpInterface,
|
|
|
|
|
httpsBindInterface: httpsInterface,
|
|
|
|
|
httpsKey: this.options.httpsKey || '',
|
|
|
|
|
httpsCert: this.options.httpsCert || '',
|
|
|
|
|
dnssecZone: this.options.dnssecZone,
|
|
|
|
|
dnssecAlgorithm: 'ECDSA',
|
|
|
|
|
primaryNameserver: this.options.primaryNameserver || `ns1.${this.options.dnssecZone}`,
|
|
|
|
|
enableLocalhostHandling: this.options.enableLocalhostHandling !== false,
|
|
|
|
|
manualUdpMode: this.options.manualUdpMode || false,
|
|
|
|
|
manualHttpsMode: this.options.manualHttpsMode || false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await this.bridge.startServer(config);
|
2025-05-28 19:16:54 +00:00
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
// Set legacy markers for backward-compatible test checks
|
|
|
|
|
this.httpsServer = { _rustBridgeManaged: true };
|
|
|
|
|
this.udpServer = { _rustBridgeManaged: true };
|
|
|
|
|
|
|
|
|
|
const udpManual = this.options.manualUdpMode || false;
|
|
|
|
|
const httpsManual = this.options.manualHttpsMode || false;
|
|
|
|
|
|
|
|
|
|
if (udpManual && httpsManual) {
|
|
|
|
|
console.log('DNS server started in full manual mode - ready to accept connections');
|
|
|
|
|
} else if (udpManual && !httpsManual) {
|
|
|
|
|
console.log('DNS server started with manual UDP mode and automatic HTTPS binding');
|
|
|
|
|
} else if (!udpManual && httpsManual) {
|
|
|
|
|
console.log('DNS server started with automatic UDP binding and manual HTTPS mode');
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`DNS server started (UDP: ${udpInterface}:${this.options.udpPort}, HTTPS: ${httpsInterface}:${this.options.httpsPort})`);
|
|
|
|
|
}
|
2025-05-28 19:16:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-11 11:24:10 +00:00
|
|
|
* Stop the DNS server.
|
2025-05-28 19:16:54 +00:00
|
|
|
*/
|
2026-02-11 11:24:10 +00:00
|
|
|
public async stop(): Promise<void> {
|
|
|
|
|
if (this.bridgeSpawned) {
|
|
|
|
|
try {
|
|
|
|
|
await this.bridge.stopServer();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// Ignore errors during stop (process may have already exited)
|
|
|
|
|
}
|
|
|
|
|
this.bridge.kill();
|
|
|
|
|
this.bridgeSpawned = false;
|
2025-05-28 19:16:54 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
this.httpsServer = null;
|
|
|
|
|
this.udpServer = null;
|
|
|
|
|
}
|
2025-05-28 19:16:54 +00:00
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
/**
|
|
|
|
|
* Initialize servers (no-op with Rust bridge, kept for API compatibility).
|
|
|
|
|
*/
|
|
|
|
|
public initializeServers(): void {
|
|
|
|
|
// No-op — Rust bridge handles server initialization via start()
|
2025-05-28 19:16:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-11 11:24:10 +00:00
|
|
|
* Initialize UDP server (no-op with Rust bridge).
|
|
|
|
|
*/
|
|
|
|
|
public initializeUdpServer(): void {
|
|
|
|
|
// No-op
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize HTTPS server (no-op with Rust bridge).
|
|
|
|
|
*/
|
|
|
|
|
public initializeHttpsServer(): void {
|
|
|
|
|
// No-op
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle a raw TCP socket for HTTPS/DoH.
|
|
|
|
|
* In Rust mode, this is not directly supported — use processRawDnsPacket instead.
|
2025-05-28 19:16:54 +00:00
|
|
|
*/
|
|
|
|
|
public handleHttpsSocket(socket: plugins.net.Socket): void {
|
2026-02-11 11:24:10 +00:00
|
|
|
console.warn('handleHttpsSocket: direct socket handling not available with Rust bridge. Use processRawDnsPacket instead.');
|
2025-05-28 19:16:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-11 11:24:10 +00:00
|
|
|
* Handle a UDP message manually.
|
2025-05-28 19:16:54 +00:00
|
|
|
*/
|
|
|
|
|
public handleUdpMessage(
|
2026-02-11 11:24:10 +00:00
|
|
|
msg: Buffer,
|
2025-05-28 19:16:54 +00:00
|
|
|
rinfo: plugins.dgram.RemoteInfo,
|
|
|
|
|
responseCallback?: (response: Buffer, rinfo: plugins.dgram.RemoteInfo) => void
|
|
|
|
|
): void {
|
2026-02-11 11:24:10 +00:00
|
|
|
// Async processing via Rust bridge
|
|
|
|
|
this.processRawDnsPacketAsync(msg)
|
|
|
|
|
.then((responseData) => {
|
|
|
|
|
if (responseCallback) {
|
|
|
|
|
responseCallback(responseData, rinfo);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.error('Error processing UDP DNS request:', err);
|
|
|
|
|
});
|
2025-05-28 19:16:54 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
/**
|
|
|
|
|
* Process a raw DNS packet asynchronously via Rust bridge.
|
|
|
|
|
*/
|
|
|
|
|
public async processRawDnsPacketAsync(packet: Buffer): Promise<Buffer> {
|
2026-02-12 23:52:46 +00:00
|
|
|
if (!this.bridgeSpawned) {
|
|
|
|
|
throw new Error('DNS server not started — call start() first');
|
2026-02-11 11:24:10 +00:00
|
|
|
}
|
2026-02-12 23:52:46 +00:00
|
|
|
return this.bridge.processPacket(packet);
|
2025-03-21 18:21:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieve SSL certificate for specified domains using Let's Encrypt
|
|
|
|
|
*/
|
|
|
|
|
public async retrieveSslCertificate(
|
|
|
|
|
domainNames: string[],
|
|
|
|
|
options: LetsEncryptOptions = {}
|
|
|
|
|
): Promise<{ cert: string; key: string; success: boolean }> {
|
|
|
|
|
const opts = {
|
|
|
|
|
email: options.email || 'admin@example.com',
|
|
|
|
|
staging: options.staging !== undefined ? options.staging : false,
|
|
|
|
|
certDir: options.certDir || './certs'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!plugins.fs.existsSync(opts.certDir)) {
|
|
|
|
|
plugins.fs.mkdirSync(opts.certDir, { recursive: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const authorizedDomains = this.filterAuthorizedDomains(domainNames);
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
if (authorizedDomains.length === 0) {
|
|
|
|
|
console.error('None of the provided domains are authorized for this DNS server');
|
|
|
|
|
return { cert: '', key: '', success: false };
|
|
|
|
|
}
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
console.log(`Retrieving SSL certificate for domains: ${authorizedDomains.join(', ')}`);
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
try {
|
|
|
|
|
// @ts-ignore - acmeClientOverride is added for testing purposes
|
|
|
|
|
const acmeClient = this.acmeClientOverride || await import('acme-client');
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
const accountKeyPath = plugins.path.join(opts.certDir, 'account.key');
|
|
|
|
|
let accountKey: Buffer;
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
if (plugins.fs.existsSync(accountKeyPath)) {
|
|
|
|
|
accountKey = plugins.fs.readFileSync(accountKeyPath);
|
|
|
|
|
} else {
|
|
|
|
|
const { privateKey } = plugins.crypto.generateKeyPairSync('rsa', {
|
|
|
|
|
modulusLength: 2048,
|
|
|
|
|
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
|
|
|
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
|
|
|
});
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
accountKey = Buffer.from(privateKey);
|
|
|
|
|
plugins.fs.writeFileSync(accountKeyPath, accountKey);
|
|
|
|
|
}
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
const client = new acmeClient.Client({
|
|
|
|
|
directoryUrl: opts.staging
|
|
|
|
|
? acmeClient.directory.letsencrypt.staging
|
|
|
|
|
: acmeClient.directory.letsencrypt.production,
|
|
|
|
|
accountKey: accountKey
|
|
|
|
|
});
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
await client.createAccount({
|
|
|
|
|
termsOfServiceAgreed: true,
|
|
|
|
|
contact: [`mailto:${opts.email}`]
|
|
|
|
|
});
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
const order = await client.createOrder({
|
|
|
|
|
identifiers: authorizedDomains.map(domain => ({
|
|
|
|
|
type: 'dns',
|
|
|
|
|
value: domain
|
|
|
|
|
}))
|
|
|
|
|
});
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
const authorizations = await client.getAuthorizations(order);
|
|
|
|
|
const challengeHandlers: { domain: string; pattern: string }[] = [];
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
for (const auth of authorizations) {
|
|
|
|
|
const domain = auth.identifier.value;
|
2025-05-28 19:03:45 +00:00
|
|
|
const challenge = auth.challenges.find((c: any) => c.type === 'dns-01');
|
2025-03-21 18:21:47 +00:00
|
|
|
if (!challenge) {
|
|
|
|
|
throw new Error(`No DNS-01 challenge found for ${domain}`);
|
|
|
|
|
}
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
|
|
|
|
const recordValue = this.getDnsRecordValueForChallenge(keyAuthorization);
|
|
|
|
|
const challengeDomain = `_acme-challenge.${domain}`;
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
console.log(`Setting up TXT record for ${challengeDomain}: ${recordValue}`);
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
this.registerHandler(
|
|
|
|
|
challengeDomain,
|
|
|
|
|
['TXT'],
|
2026-02-12 23:52:46 +00:00
|
|
|
(question: IDnsQuestion): DnsAnswer | null => {
|
2025-03-21 18:21:47 +00:00
|
|
|
if (question.name === challengeDomain && question.type === 'TXT') {
|
|
|
|
|
return {
|
|
|
|
|
name: question.name,
|
|
|
|
|
type: 'TXT',
|
|
|
|
|
class: 'IN',
|
|
|
|
|
ttl: 300,
|
|
|
|
|
data: [recordValue]
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
);
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
challengeHandlers.push({ domain, pattern: challengeDomain });
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
|
await client.completeChallenge(challenge);
|
|
|
|
|
await client.waitForValidStatus(challenge);
|
|
|
|
|
console.log(`Challenge for ${domain} validated successfully!`);
|
|
|
|
|
}
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
const domainKeyPath = plugins.path.join(opts.certDir, `${authorizedDomains[0]}.key`);
|
|
|
|
|
const { privateKey } = plugins.crypto.generateKeyPairSync('rsa', {
|
|
|
|
|
modulusLength: 2048,
|
|
|
|
|
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
|
|
|
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
|
|
|
});
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
plugins.fs.writeFileSync(domainKeyPath, privateKey);
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
interface CSRResult {
|
|
|
|
|
csr: Buffer;
|
|
|
|
|
}
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
const csrResult = await acmeClient.forge.createCsr({
|
|
|
|
|
commonName: authorizedDomains[0],
|
|
|
|
|
altNames: authorizedDomains
|
|
|
|
|
}) as unknown as CSRResult;
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
await client.finalizeOrder(order, csrResult.csr);
|
|
|
|
|
const certificate = await client.getCertificate(order);
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
const certPath = plugins.path.join(opts.certDir, `${authorizedDomains[0]}.cert`);
|
|
|
|
|
plugins.fs.writeFileSync(certPath, certificate);
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
this.options.httpsCert = certificate;
|
|
|
|
|
this.options.httpsKey = privateKey;
|
2026-02-11 11:24:10 +00:00
|
|
|
|
|
|
|
|
// Update certs on Rust bridge if running
|
|
|
|
|
if (this.bridgeSpawned) {
|
|
|
|
|
try {
|
|
|
|
|
await this.bridge.updateCerts(privateKey, certificate);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Error updating certs on Rust bridge:', err);
|
|
|
|
|
}
|
2025-05-28 19:16:54 +00:00
|
|
|
}
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
for (const handler of challengeHandlers) {
|
|
|
|
|
this.unregisterHandler(handler.pattern, ['TXT']);
|
|
|
|
|
console.log(`Cleaned up challenge handler for ${handler.domain}`);
|
|
|
|
|
}
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
return {
|
|
|
|
|
cert: certificate,
|
|
|
|
|
key: privateKey,
|
|
|
|
|
success: true
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error retrieving SSL certificate:', error);
|
|
|
|
|
return { cert: '', key: '', success: false };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-11 11:24:10 +00:00
|
|
|
* Filter domains to include only those the server is authoritative for.
|
2025-03-21 18:21:47 +00:00
|
|
|
*/
|
|
|
|
|
public filterAuthorizedDomains(domainNames: string[]): string[] {
|
|
|
|
|
const authorizedDomains: string[] = [];
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
for (const domain of domainNames) {
|
|
|
|
|
if (domain.startsWith('*.')) {
|
|
|
|
|
const baseDomain = domain.substring(2);
|
|
|
|
|
if (this.isAuthorizedForDomain(baseDomain)) {
|
|
|
|
|
authorizedDomains.push(domain);
|
|
|
|
|
}
|
2026-02-11 11:24:10 +00:00
|
|
|
} else if (this.isAuthorizedForDomain(domain)) {
|
2025-03-21 18:21:47 +00:00
|
|
|
authorizedDomains.push(domain);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-11 11:24:10 +00:00
|
|
|
|
2025-03-21 18:21:47 +00:00
|
|
|
return authorizedDomains;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
// ── Private helpers ───────────────────────────────────────────────
|
2025-05-28 19:03:45 +00:00
|
|
|
|
|
|
|
|
/**
|
2026-02-11 11:24:10 +00:00
|
|
|
* Resolve a DNS query event from Rust using TypeScript handlers.
|
2025-05-28 19:03:45 +00:00
|
|
|
*/
|
2026-02-11 11:24:10 +00:00
|
|
|
private resolveQuery(event: IDnsQueryEvent): { answers: IIpcDnsAnswer[]; answered: boolean } {
|
|
|
|
|
const answers: IIpcDnsAnswer[] = [];
|
|
|
|
|
let answered = false;
|
|
|
|
|
|
|
|
|
|
for (const q of event.questions) {
|
2026-02-12 23:52:46 +00:00
|
|
|
const question: IDnsQuestion = {
|
2026-02-11 11:24:10 +00:00
|
|
|
name: q.name,
|
2026-02-12 23:52:46 +00:00
|
|
|
type: q.type,
|
|
|
|
|
class: q.class,
|
2026-02-11 11:24:10 +00:00
|
|
|
};
|
2025-09-12 17:32:03 +00:00
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
for (const handlerEntry of this.handlers) {
|
|
|
|
|
if (
|
|
|
|
|
plugins.minimatch.minimatch(q.name, handlerEntry.domainPattern) &&
|
|
|
|
|
handlerEntry.recordTypes.includes(q.type)
|
|
|
|
|
) {
|
|
|
|
|
const answer = handlerEntry.handler(question);
|
|
|
|
|
if (answer) {
|
|
|
|
|
answers.push({
|
|
|
|
|
name: answer.name,
|
|
|
|
|
type: answer.type,
|
|
|
|
|
class: typeof answer.class === 'number' ? 'IN' : (answer.class || 'IN'),
|
|
|
|
|
ttl: answer.ttl || 300,
|
|
|
|
|
data: answer.data,
|
|
|
|
|
});
|
|
|
|
|
answered = true;
|
2025-09-12 17:32:03 +00:00
|
|
|
}
|
2024-09-18 19:28:28 +02:00
|
|
|
}
|
2024-06-02 15:34:19 +02:00
|
|
|
}
|
2025-05-30 18:20:55 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
return { answers, answered };
|
2024-09-19 18:51:34 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
private getDnsRecordValueForChallenge(keyAuthorization: string): string {
|
|
|
|
|
const digest = plugins.crypto
|
|
|
|
|
.createHash('sha256')
|
|
|
|
|
.update(keyAuthorization)
|
|
|
|
|
.digest('base64')
|
|
|
|
|
.replace(/\+/g, '-')
|
|
|
|
|
.replace(/\//g, '_')
|
|
|
|
|
.replace(/=/g, '');
|
2024-09-19 18:51:34 +02:00
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
return digest;
|
2024-09-19 18:51:34 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
private isValidIpAddress(ip: string): boolean {
|
|
|
|
|
const ipv4Pattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
|
|
|
|
const ipv6Pattern = /^(::1|::)$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
|
|
|
|
return ipv4Pattern.test(ip) || ipv6Pattern.test(ip);
|
2024-06-02 15:34:19 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
private isAuthorizedForDomain(domain: string): boolean {
|
|
|
|
|
for (const handler of this.handlers) {
|
|
|
|
|
if (plugins.minimatch.minimatch(domain, handler.domainPattern)) {
|
|
|
|
|
return true;
|
2025-05-28 19:16:54 +00:00
|
|
|
}
|
|
|
|
|
}
|
2024-09-18 19:28:28 +02:00
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
if (domain === this.options.dnssecZone || domain.endsWith(`.${this.options.dnssecZone}`)) {
|
|
|
|
|
return true;
|
2024-09-19 18:51:34 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-11 11:24:10 +00:00
|
|
|
return false;
|
2024-09-19 18:51:34 +02:00
|
|
|
}
|
2025-09-12 17:32:03 +00:00
|
|
|
}
|