import * as plugins from './plugins.js'; import { RustDnsClientBridge } from './classes.rustdnsclientbridge.js'; export type TDnsProvider = 'google' | 'cloudflare'; export const makeNodeProcessUseDnsProvider = (providerArg: TDnsProvider) => { switch (providerArg) { case 'cloudflare': plugins.dns.setServers([ '1.1.1.1', '1.0.0.1', '[2606:4700:4700::1111]', '[2606:4700:4700::1001]', ]); break; case 'google': plugins.dns.setServers([ '8.8.8.8', '8.8.4.4', '[2001:4860:4860::8888]', '[2606:4700:4700::1001]', ]); } }; export type TResolutionStrategy = 'doh' | 'udp' | 'system' | 'prefer-system' | 'prefer-udp'; export interface ISmartDnsConstructorOptions { strategy?: TResolutionStrategy; // default: 'prefer-system' allowDohFallback?: boolean; // allow fallback to DoH if system fails (default: true) timeoutMs?: number; // optional per-query timeout } /** * Smartdns offers methods for working with DNS resolution. * Supports system resolver, UDP wire-format, and DoH (DNS-over-HTTPS) via a Rust binary. */ export class Smartdns { private strategy: TResolutionStrategy = 'prefer-system'; private allowDohFallback = true; private timeoutMs: number | undefined; private rustBridge: RustDnsClientBridge | null = null; public dnsTypeMap: { [key: string]: number } = { A: 1, NS: 2, CNAME: 5, SOA: 6, PTR: 12, MX: 15, TXT: 16, AAAA: 28, SRV: 33, }; constructor(optionsArg: ISmartDnsConstructorOptions) { this.strategy = optionsArg?.strategy || 'prefer-system'; this.allowDohFallback = optionsArg?.allowDohFallback === undefined ? true : optionsArg.allowDohFallback; this.timeoutMs = optionsArg?.timeoutMs; } private getRustBridge(): RustDnsClientBridge { if (!this.rustBridge) { this.rustBridge = new RustDnsClientBridge(); } return this.rustBridge; } /** * check a dns record until it has propagated */ public async checkUntilAvailable( recordNameArg: string, recordTypeArg: plugins.tsclass.network.TDnsRecordType, expectedValue: string, cyclesArg: number = 50, intervalArg: number = 500 ) { let runCycles = 0; const doCheck = async () => { if (runCycles < cyclesArg) { runCycles++; try { let myRecordArray: plugins.tsclass.network.IDnsRecord[]; if (runCycles % 2 === 0 || !plugins.dns) { myRecordArray = await this.getRecords(recordNameArg, recordTypeArg, 0); } else { myRecordArray = await this.getRecordWithNodeDNS(recordNameArg, recordTypeArg); } const myRecord = myRecordArray[0].value; if (myRecord === expectedValue) { console.log( `smartdns: .checkUntilAvailable() verified that wanted >>>${recordTypeArg}<<< record exists for >>>${recordNameArg}<<< with value >>>${expectedValue}<<<` ); return true; } else { await plugins.smartdelay.delayFor(intervalArg); return await doCheck(); } } catch (err) { await plugins.smartdelay.delayFor(intervalArg); return await doCheck(); } } else { console.log( `smartdns: .checkUntilAvailable() failed permanently for ${recordNameArg} with value ${recordTypeArg} - ${expectedValue}...` ); return false; } }; return await doCheck(); } /** * get A Dns Record */ public async getRecordsA(recordNameArg: string): Promise { return await this.getRecords(recordNameArg, 'A'); } /** * get AAAA Record */ public async getRecordsAAAA(recordNameArg: string) { return await this.getRecords(recordNameArg, 'AAAA'); } /** * gets a txt record */ public async getRecordsTxt(recordNameArg: string): Promise { return await this.getRecords(recordNameArg, 'TXT'); } public async getRecords( recordNameArg: string, recordTypeArg: plugins.tsclass.network.TDnsRecordType, retriesCounterArg = 20 ): Promise { const trySystem = async (): Promise => { // Prefer dns.lookup for A/AAAA so hosts file and OS resolver are honored if (recordTypeArg === 'A' || recordTypeArg === 'AAAA') { const family = recordTypeArg === 'A' ? 4 : 6; const addresses = await new Promise<{ address: string }[]>((resolve, reject) => { const timer = this.timeoutMs ? setTimeout(() => reject(new Error('system lookup timeout')), this.timeoutMs) : null; plugins.dns.lookup( recordNameArg, { family, all: true }, (err, result) => { if (timer) clearTimeout(timer as any); if (err) return reject(err); resolve(result || []); } ); }); return addresses.map((a) => ({ name: recordNameArg, type: recordTypeArg, dnsSecEnabled: false, value: a.address, })); } if (recordTypeArg === 'TXT') { const records = await new Promise((resolve, reject) => { const timer = this.timeoutMs ? setTimeout(() => reject(new Error('system resolveTxt timeout')), this.timeoutMs) : null; plugins.dns.resolveTxt(recordNameArg, (err, res) => { if (timer) clearTimeout(timer as any); if (err) return reject(err); resolve(res || []); }); }); return records.map((chunks) => ({ name: recordNameArg, type: 'TXT' as plugins.tsclass.network.TDnsRecordType, dnsSecEnabled: false, value: chunks.join(''), })); } return []; }; const tryRust = async (protocol: 'udp' | 'doh'): Promise => { const bridge = this.getRustBridge(); const result = await bridge.resolve( recordNameArg, recordTypeArg, protocol, undefined, undefined, this.timeoutMs ); return result.answers.map((answer) => ({ name: answer.name, type: this.convertDnsTypeNameToCanonical(answer.type) || recordTypeArg, dnsSecEnabled: result.adFlag, value: answer.value, })); }; try { if (this.strategy === 'system') { return await trySystem(); } if (this.strategy === 'doh') { return await tryRust('doh'); } if (this.strategy === 'udp') { return await tryRust('udp'); } if (this.strategy === 'prefer-udp') { try { const udpRes = await tryRust('udp'); if (udpRes.length > 0) return udpRes; return await tryRust('doh'); } catch (err) { return await tryRust('doh'); } } // prefer-system (default) try { const sysRes = await trySystem(); if (sysRes.length > 0) return sysRes; return this.allowDohFallback ? await tryRust('doh') : []; } catch (err) { return this.allowDohFallback ? await tryRust('doh') : []; } } catch (finalErr) { return []; } } /** * gets a record using nodejs dns resolver */ public async getRecordWithNodeDNS( recordNameArg: string, recordTypeArg: plugins.tsclass.network.TDnsRecordType ): Promise { const done = plugins.smartpromise.defer(); plugins.dns.resolve(recordNameArg, recordTypeArg, (err, recordsArg) => { if (err) { done.reject(err); return; } const returnArray: plugins.tsclass.network.IDnsRecord[] = []; for (const recordKey in recordsArg) { returnArray.push({ name: recordNameArg, value: recordsArg[recordKey][0], type: recordTypeArg, dnsSecEnabled: false, }); } done.resolve(returnArray); }); return done.promise; } public async getNameServers(domainNameArg: string): Promise { const done = plugins.smartpromise.defer(); plugins.dns.resolveNs(domainNameArg, (err, result) => { if (!err) { done.resolve(result); } else { console.log(err); done.reject(err); } }); return await done.promise; } public convertDnsTypeNameToTypeNumber(dnsTypeNameArg: string): number { return this.dnsTypeMap[dnsTypeNameArg]; } public convertDnsTypeNumberToTypeName( dnsTypeNumberArg: number ): plugins.tsclass.network.TDnsRecordType { for (const key in this.dnsTypeMap) { if (this.dnsTypeMap[key] === dnsTypeNumberArg) { return key as plugins.tsclass.network.TDnsRecordType; } } return null; } /** * Convert a DNS type string from Rust (e.g. "A", "AAAA") to the canonical TDnsRecordType. */ private convertDnsTypeNameToCanonical(typeName: string): plugins.tsclass.network.TDnsRecordType | null { const upper = typeName.toUpperCase(); if (upper in this.dnsTypeMap) { return upper as plugins.tsclass.network.TDnsRecordType; } return null; } /** * Destroy the Rust client bridge and free resources. */ public destroy(): void { if (this.rustBridge) { this.rustBridge.kill(); this.rustBridge = null; } } }