Files
smartdns/ts_client/classes.dnsclient.ts

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;
}
}
}