feat(rustdns-client): add Rust DNS client binary and TypeScript IPC bridge to enable UDP and DoH resolution, RDATA decoding, and DNSSEC AD/rcode support

This commit is contained in:
2026-02-11 13:02:37 +00:00
parent 9d4db39741
commit 368430d199
24 changed files with 2805 additions and 863 deletions

View File

@@ -1,4 +1,5 @@
import * as plugins from './plugins.js';
import { RustDnsClientBridge } from './classes.rustdnsclientbridge.js';
export type TDnsProvider = 'google' | 'cloudflare';
@@ -22,7 +23,7 @@ export const makeNodeProcessUseDnsProvider = (providerArg: TDnsProvider) => {
}
};
export type TResolutionStrategy = 'doh' | 'system' | 'prefer-system';
export type TResolutionStrategy = 'doh' | 'udp' | 'system' | 'prefer-system' | 'prefer-udp';
export interface ISmartDnsConstructorOptions {
strategy?: TResolutionStrategy; // default: 'prefer-system'
@@ -30,40 +31,28 @@ export interface ISmartDnsConstructorOptions {
timeoutMs?: number; // optional per-query timeout
}
export interface IDnsJsonResponse {
Status: number;
TC: boolean;
RD: boolean;
RA: boolean;
AD: boolean;
CD: boolean;
Question: Array<{ name: string; type: number }>;
Answer: Array<{ name: string; type: number; TTL: number; data: string }>;
Additional: [];
Comment: string;
}
/**
* class dnsly offers methods for working with dns from a dns provider like Google DNS
* 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 {
public dnsServerIp: string;
public dnsServerPort: number;
private strategy: TResolutionStrategy = 'prefer-system';
private allowDohFallback = true;
private timeoutMs: number | undefined;
private rustBridge: RustDnsClientBridge | null = null;
public dnsTypeMap: { [key: string]: number } = {
A: 1,
AAAA: 28,
NS: 2,
CNAME: 5,
SOA: 6,
PTR: 12,
MX: 15,
TXT: 16,
AAAA: 28,
SRV: 33,
};
/**
* constructor for class dnsly
*/
constructor(optionsArg: ISmartDnsConstructorOptions) {
this.strategy = optionsArg?.strategy || 'prefer-system';
this.allowDohFallback =
@@ -71,12 +60,15 @@ export class Smartdns {
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 to Google DNS
* should be considerably fast
* @param recordNameArg
* @param recordTypeArg
* @param expectedValue
* check a dns record until it has propagated
*/
public async checkUntilAvailable(
recordNameArg: string,
@@ -107,7 +99,6 @@ export class Smartdns {
return await doCheck();
}
} catch (err) {
// console.log(err);
await plugins.smartdelay.delayFor(intervalArg);
return await doCheck();
}
@@ -185,7 +176,7 @@ export class Smartdns {
});
return records.map((chunks) => ({
name: recordNameArg,
type: 'TXT',
type: 'TXT' as plugins.tsclass.network.TDnsRecordType,
dnsSecEnabled: false,
value: chunks.join(''),
}));
@@ -193,44 +184,22 @@ export class Smartdns {
return [];
};
const tryDoh = async (): Promise<plugins.tsclass.network.IDnsRecord[]> => {
const requestUrl = `https://cloudflare-dns.com/dns-query?name=${recordNameArg}&type=${recordTypeArg}&do=1`;
const returnArray: plugins.tsclass.network.IDnsRecord[] = [];
const getResponseBody = async (counterArg = 0): Promise<IDnsJsonResponse> => {
const response = await plugins.smartrequest.request(requestUrl, {
method: 'GET',
headers: {
accept: 'application/dns-json',
},
timeout: this.timeoutMs,
});
const responseBody: IDnsJsonResponse = response.body;
if (responseBody?.Status !== 0 && counterArg < retriesCounterArg) {
await plugins.smartdelay.delayFor(500);
return getResponseBody(counterArg + 1);
} else {
return responseBody;
}
};
const responseBody = await getResponseBody();
if (!responseBody || !responseBody.Answer || !typeof (responseBody.Answer as any)[Symbol.iterator]) {
return returnArray;
}
for (const dnsEntry of responseBody.Answer) {
if (typeof dnsEntry.data === 'string' && dnsEntry.data.startsWith('"') && dnsEntry.data.endsWith('"')) {
dnsEntry.data = dnsEntry.data.replace(/^\"(.*)\"$/, '$1');
}
if (dnsEntry.name.endsWith('.')) {
dnsEntry.name = dnsEntry.name.substring(0, dnsEntry.name.length - 1);
}
returnArray.push({
name: dnsEntry.name,
type: this.convertDnsTypeNumberToTypeName(dnsEntry.type),
dnsSecEnabled: !!responseBody.AD,
value: dnsEntry.data,
});
}
return returnArray;
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 {
@@ -238,21 +207,32 @@ export class Smartdns {
return await trySystem();
}
if (this.strategy === 'doh') {
return await tryDoh();
return await tryRust('doh');
}
// prefer-system
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 tryDoh() : [];
return this.allowDohFallback ? await tryRust('doh') : [];
} catch (err) {
return this.allowDohFallback ? await tryDoh() : [];
return this.allowDohFallback ? await tryRust('doh') : [];
}
} catch (finalErr) {
return [];
}
}
/**
* gets a record using nodejs dns resolver
@@ -308,4 +288,25 @@ export class Smartdns {
}
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;
}
}
}