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:
168
ts_client/classes.rustdnsclientbridge.ts
Normal file
168
ts_client/classes.rustdnsclientbridge.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
// IPC command map for type-safe bridge communication
|
||||
export type TClientDnsCommands = {
|
||||
resolve: {
|
||||
params: IResolveParams;
|
||||
result: IResolveResult;
|
||||
};
|
||||
ping: {
|
||||
params: Record<string, never>;
|
||||
result: { pong: boolean };
|
||||
};
|
||||
};
|
||||
|
||||
export interface IResolveParams {
|
||||
name: string;
|
||||
recordType: string;
|
||||
protocol: 'udp' | 'doh';
|
||||
serverAddr?: string;
|
||||
dohUrl?: string;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface IClientDnsAnswer {
|
||||
name: string;
|
||||
type: string;
|
||||
ttl: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IResolveResult {
|
||||
answers: IClientDnsAnswer[];
|
||||
adFlag: boolean;
|
||||
rcode: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridge to the Rust DNS client binary via smartrust IPC.
|
||||
*/
|
||||
export class RustDnsClientBridge {
|
||||
private bridge: InstanceType<typeof plugins.smartrust.RustBridge<TClientDnsCommands>>;
|
||||
private spawnPromise: Promise<boolean> | null = null;
|
||||
|
||||
constructor() {
|
||||
const packageDir = plugins.path.resolve(
|
||||
plugins.path.dirname(new URL(import.meta.url).pathname),
|
||||
'..'
|
||||
);
|
||||
|
||||
// Determine platform suffix for dist_rust binaries (matches tsrust naming)
|
||||
const platformSuffix = getPlatformSuffix();
|
||||
const localPaths: string[] = [];
|
||||
|
||||
// dist_rust/ candidates (tsrust cross-compiled output, platform-specific)
|
||||
if (platformSuffix) {
|
||||
localPaths.push(plugins.path.join(packageDir, 'dist_rust', `rustdns-client_${platformSuffix}`));
|
||||
}
|
||||
// dist_rust/ without suffix (native build)
|
||||
localPaths.push(plugins.path.join(packageDir, 'dist_rust', 'rustdns-client'));
|
||||
// Local dev build paths
|
||||
localPaths.push(plugins.path.join(packageDir, 'rust', 'target', 'release', 'rustdns-client'));
|
||||
localPaths.push(plugins.path.join(packageDir, 'rust', 'target', 'debug', 'rustdns-client'));
|
||||
|
||||
this.bridge = new plugins.smartrust.RustBridge<TClientDnsCommands>({
|
||||
binaryName: 'rustdns-client',
|
||||
cliArgs: ['--management'],
|
||||
requestTimeoutMs: 30_000,
|
||||
readyTimeoutMs: 10_000,
|
||||
localPaths,
|
||||
searchSystemPath: false,
|
||||
});
|
||||
|
||||
this.bridge.on('stderr', (line: string) => {
|
||||
if (line.trim()) {
|
||||
console.log(`[rustdns-client] ${line}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily spawn the Rust binary. Only spawns once, caches the promise.
|
||||
*/
|
||||
public async ensureSpawned(): Promise<void> {
|
||||
if (!this.spawnPromise) {
|
||||
this.spawnPromise = this.bridge.spawn();
|
||||
}
|
||||
const ok = await this.spawnPromise;
|
||||
if (!ok) {
|
||||
this.spawnPromise = null;
|
||||
throw new Error('Failed to spawn rustdns-client binary');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a DNS query through the Rust binary.
|
||||
*/
|
||||
public async resolve(
|
||||
name: string,
|
||||
recordType: string,
|
||||
protocol: 'udp' | 'doh',
|
||||
serverAddr?: string,
|
||||
dohUrl?: string,
|
||||
timeoutMs?: number
|
||||
): Promise<IResolveResult> {
|
||||
await this.ensureSpawned();
|
||||
const params: IResolveParams = {
|
||||
name,
|
||||
recordType,
|
||||
protocol,
|
||||
};
|
||||
if (serverAddr) params.serverAddr = serverAddr;
|
||||
if (dohUrl) params.dohUrl = dohUrl;
|
||||
if (timeoutMs) params.timeoutMs = timeoutMs;
|
||||
return this.bridge.sendCommand('resolve', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping the Rust binary for health check.
|
||||
*/
|
||||
public async ping(): Promise<boolean> {
|
||||
await this.ensureSpawned();
|
||||
const result = await this.bridge.sendCommand('ping', {} as Record<string, never>);
|
||||
return result.pong;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill the Rust process.
|
||||
*/
|
||||
public kill(): void {
|
||||
this.bridge.kill();
|
||||
this.spawnPromise = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the bridge is running.
|
||||
*/
|
||||
public get running(): boolean {
|
||||
return this.bridge.running;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tsrust platform suffix for the current platform.
|
||||
*/
|
||||
function getPlatformSuffix(): string | null {
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
|
||||
const platformMap: Record<string, string> = {
|
||||
'linux': 'linux',
|
||||
'darwin': 'macos',
|
||||
'win32': 'windows',
|
||||
};
|
||||
|
||||
const archMap: Record<string, string> = {
|
||||
'x64': 'amd64',
|
||||
'arm64': 'arm64',
|
||||
};
|
||||
|
||||
const p = platformMap[platform];
|
||||
const a = archMap[arch];
|
||||
|
||||
if (p && a) {
|
||||
return `${p}_${a}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user