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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user