313 lines
9.4 KiB
TypeScript
313 lines
9.4 KiB
TypeScript
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<plugins.tsclass.network.IDnsRecord[]> {
|
|
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<plugins.tsclass.network.IDnsRecord[]> {
|
|
return await this.getRecords(recordNameArg, 'TXT');
|
|
}
|
|
|
|
public async getRecords(
|
|
recordNameArg: string,
|
|
recordTypeArg: plugins.tsclass.network.TDnsRecordType,
|
|
retriesCounterArg = 20
|
|
): Promise<plugins.tsclass.network.IDnsRecord[]> {
|
|
const trySystem = async (): Promise<plugins.tsclass.network.IDnsRecord[]> => {
|
|
// 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<string[][]>((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<plugins.tsclass.network.IDnsRecord[]> => {
|
|
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<plugins.tsclass.network.IDnsRecord[]> {
|
|
const done = plugins.smartpromise.defer<plugins.tsclass.network.IDnsRecord[]>();
|
|
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<string[]> {
|
|
const done = plugins.smartpromise.defer<string[]>();
|
|
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;
|
|
}
|
|
}
|
|
}
|