feat(rust): add Rust-based DNS server backend with IPC management and TypeScript bridge

This commit is contained in:
2026-02-11 11:24:10 +00:00
parent abbb971d6a
commit 60371e1ad5
37 changed files with 4509 additions and 1272 deletions
-189
View File
@@ -1,189 +0,0 @@
// Import necessary plugins from plugins.ts
import * as plugins from './plugins.js';
interface DnssecZone {
zone: string;
algorithm: 'ECDSA' | 'ED25519' | 'RSA';
keySize: number;
days: number;
}
interface DnssecKeyPair {
privateKey: string;
publicKey: string;
}
export class DnsSec {
private zone: DnssecZone;
private keyPair: DnssecKeyPair;
private ec?: plugins.elliptic.ec; // For ECDSA algorithms
private eddsa?: plugins.elliptic.eddsa; // For EdDSA algorithms
constructor(zone: DnssecZone) {
this.zone = zone;
// Initialize the appropriate cryptographic instance based on the algorithm
switch (this.zone.algorithm) {
case 'ECDSA':
this.ec = new plugins.elliptic.ec('p256'); // Use P-256 curve for ECDSA
break;
case 'ED25519':
this.eddsa = new plugins.elliptic.eddsa('ed25519');
break;
case 'RSA':
// RSA implementation would go here
throw new Error('RSA algorithm is not yet implemented.');
default:
throw new Error(`Unsupported algorithm: ${this.zone.algorithm}`);
}
// Generate the key pair
this.keyPair = this.generateKeyPair();
}
private generateKeyPair(): DnssecKeyPair {
let privateKey: string;
let publicKey: string;
switch (this.zone.algorithm) {
case 'ECDSA':
if (!this.ec) throw new Error('EC instance is not initialized.');
const ecKeyPair = this.ec.genKeyPair();
privateKey = ecKeyPair.getPrivate('hex');
publicKey = ecKeyPair.getPublic(false, 'hex'); // Uncompressed format
break;
case 'ED25519':
if (!this.eddsa) throw new Error('EdDSA instance is not initialized.');
const secret = plugins.crypto.randomBytes(32);
const edKeyPair = this.eddsa.keyFromSecret(secret);
privateKey = edKeyPair.getSecret('hex');
publicKey = edKeyPair.getPublic('hex');
break;
case 'RSA':
// RSA key generation would be implemented here
throw new Error('RSA key generation is not yet implemented.');
default:
throw new Error(`Unsupported algorithm: ${this.zone.algorithm}`);
}
return { privateKey, publicKey };
}
public getAlgorithmNumber(): number {
switch (this.zone.algorithm) {
case 'ECDSA':
return 13; // ECDSAP256SHA256
case 'ED25519':
return 15;
case 'RSA':
return 8; // RSASHA256
default:
throw new Error(`Unsupported algorithm: ${this.zone.algorithm}`);
}
}
public signData(data: Buffer): Buffer {
switch (this.zone.algorithm) {
case 'ECDSA':
if (!this.ec) throw new Error('EC instance is not initialized.');
const ecKeyPair = this.ec.keyFromPrivate(this.keyPair.privateKey, 'hex');
const ecSignature = ecKeyPair.sign(plugins.crypto.createHash('sha256').update(data).digest());
return Buffer.from(ecSignature.toDER());
case 'ED25519':
if (!this.eddsa) throw new Error('EdDSA instance is not initialized.');
const edKeyPair = this.eddsa.keyFromSecret(Buffer.from(this.keyPair.privateKey, 'hex'));
// ED25519 doesn't need a separate hash function as it includes the hashing internally
const edSignature = edKeyPair.sign(data);
// Convert the signature to the correct format for Buffer.from
return Buffer.from(edSignature.toBytes());
case 'RSA':
throw new Error('RSA signing is not yet implemented.');
default:
throw new Error(`Unsupported algorithm: ${this.zone.algorithm}`);
}
}
private generateDNSKEY(): Buffer {
const flags = 256; // 256 indicates a Zone Signing Key (ZSK)
const protocol = 3; // Must be 3 according to RFC
const algorithm = this.getAlgorithmNumber();
let publicKeyData: Buffer;
switch (this.zone.algorithm) {
case 'ECDSA':
if (!this.ec) throw new Error('EC instance is not initialized.');
const ecPublicKey = this.ec.keyFromPublic(this.keyPair.publicKey, 'hex').getPublic();
const x = ecPublicKey.getX().toArrayLike(Buffer, 'be', 32);
const y = ecPublicKey.getY().toArrayLike(Buffer, 'be', 32);
publicKeyData = Buffer.concat([x, y]);
break;
case 'ED25519':
publicKeyData = Buffer.from(this.keyPair.publicKey, 'hex');
break;
case 'RSA':
// RSA public key extraction would go here
throw new Error('RSA public key extraction is not yet implemented.');
default:
throw new Error(`Unsupported algorithm: ${this.zone.algorithm}`);
}
// Construct the DNSKEY RDATA
const dnskeyRdata = Buffer.concat([
Buffer.from([flags >> 8, flags & 0xff]), // Flags (2 bytes)
Buffer.from([protocol]), // Protocol (1 byte)
Buffer.from([algorithm]), // Algorithm (1 byte)
publicKeyData, // Public Key
]);
return dnskeyRdata;
}
private computeKeyTag(dnskeyRdata: Buffer): number {
// Key Tag calculation as per RFC 4034, Appendix B
let acc = 0;
for (let i = 0; i < dnskeyRdata.length; i++) {
acc += i & 1 ? dnskeyRdata[i] : dnskeyRdata[i] << 8;
}
acc += (acc >> 16) & 0xffff;
return acc & 0xffff;
}
private getDNSKEYRecord(): string {
const dnskeyRdata = this.generateDNSKEY();
const flags = 256;
const protocol = 3;
const algorithm = this.getAlgorithmNumber();
const publicKeyData = dnskeyRdata.slice(4); // Skip flags, protocol, algorithm bytes
const publicKeyBase64 = publicKeyData.toString('base64');
return `${this.zone.zone}. IN DNSKEY ${flags} ${protocol} ${algorithm} ${publicKeyBase64}`;
}
public getDSRecord(): string {
const dnskeyRdata = this.generateDNSKEY();
const keyTag = this.computeKeyTag(dnskeyRdata);
const algorithm = this.getAlgorithmNumber();
const digestType = 2; // SHA-256
const digest = plugins.crypto
.createHash('sha256')
.update(dnskeyRdata)
.digest('hex')
.toUpperCase();
return `${this.zone.zone}. IN DS ${keyTag} ${algorithm} ${digestType} ${digest}`;
}
public getKeyPair(): DnssecKeyPair {
return this.keyPair;
}
public getDsAndKeyPair(): { keyPair: DnssecKeyPair; dsRecord: string; dnskeyRecord: string } {
const dsRecord = this.getDSRecord();
const dnskeyRecord = this.getDNSKEYRecord();
return { keyPair: this.keyPair, dsRecord, dnskeyRecord };
}
}
File diff suppressed because it is too large Load Diff
+236
View File
@@ -0,0 +1,236 @@
import * as plugins from './plugins.js';
// IPC command map for type-safe bridge communication
export type TDnsCommands = {
start: {
params: { config: IRustDnsConfig };
result: Record<string, never>;
};
stop: {
params: Record<string, never>;
result: Record<string, never>;
};
dnsQueryResult: {
params: {
correlationId: string;
answers: IIpcDnsAnswer[];
answered: boolean;
};
result: { resolved: boolean };
};
updateCerts: {
params: { httpsKey: string; httpsCert: string };
result: Record<string, never>;
};
processPacket: {
params: { packet: string }; // base64-encoded DNS packet
result: { packet: string }; // base64-encoded DNS response
};
ping: {
params: Record<string, never>;
result: { pong: boolean };
};
};
export interface IRustDnsConfig {
udpPort: number;
httpsPort: number;
udpBindInterface: string;
httpsBindInterface: string;
httpsKey: string;
httpsCert: string;
dnssecZone: string;
dnssecAlgorithm: string;
primaryNameserver: string;
enableLocalhostHandling: boolean;
manualUdpMode: boolean;
manualHttpsMode: boolean;
}
export interface IIpcDnsQuestion {
name: string;
type: string;
class: string;
}
export interface IIpcDnsAnswer {
name: string;
type: string;
class: string;
ttl: number;
data: any;
}
export interface IDnsQueryEvent {
correlationId: string;
questions: IIpcDnsQuestion[];
dnssecRequested: boolean;
}
/**
* Bridge to the Rust DNS binary via smartrust IPC.
*/
export class RustDnsBridge extends plugins.events.EventEmitter {
private bridge: InstanceType<typeof plugins.smartrust.RustBridge<TDnsCommands>>;
constructor() {
super();
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_${platformSuffix}`));
}
// dist_rust/ without suffix (native build)
localPaths.push(plugins.path.join(packageDir, 'dist_rust', 'rustdns'));
// Local dev build paths
localPaths.push(plugins.path.join(packageDir, 'rust', 'target', 'release', 'rustdns'));
localPaths.push(plugins.path.join(packageDir, 'rust', 'target', 'debug', 'rustdns'));
this.bridge = new plugins.smartrust.RustBridge<TDnsCommands>({
binaryName: 'rustdns',
cliArgs: ['--management'],
requestTimeoutMs: 30_000,
readyTimeoutMs: 10_000,
localPaths,
searchSystemPath: false,
});
// Forward events from inner bridge
this.bridge.on('management:dnsQuery', (data: IDnsQueryEvent) => {
this.emit('dnsQuery', data);
});
this.bridge.on('management:started', () => {
this.emit('started');
});
this.bridge.on('management:stopped', () => {
this.emit('stopped');
});
this.bridge.on('management:error', (data: { message: string }) => {
this.emit('error', new Error(data.message));
});
this.bridge.on('stderr', (line: string) => {
// Forward Rust tracing output as debug logs
if (line.trim()) {
console.log(`[rustdns] ${line}`);
}
});
}
/**
* Spawn the Rust binary and wait for readiness.
*/
public async spawn(): Promise<boolean> {
return this.bridge.spawn();
}
/**
* Start the DNS server with given config.
*/
public async startServer(config: IRustDnsConfig): Promise<void> {
await this.bridge.sendCommand('start', { config });
}
/**
* Stop the DNS server.
*/
public async stopServer(): Promise<void> {
await this.bridge.sendCommand('stop', {});
}
/**
* Send a DNS query result back to Rust.
*/
public async sendQueryResult(
correlationId: string,
answers: IIpcDnsAnswer[],
answered: boolean
): Promise<void> {
await this.bridge.sendCommand('dnsQueryResult', {
correlationId,
answers,
answered,
});
}
/**
* Update TLS certificates.
*/
public async updateCerts(httpsKey: string, httpsCert: string): Promise<void> {
await this.bridge.sendCommand('updateCerts', { httpsKey, httpsCert });
}
/**
* Process a raw DNS packet via IPC (for manual/passthrough mode).
* Returns the DNS response as a Buffer.
*/
public async processPacket(packet: Buffer): Promise<Buffer> {
const result = await this.bridge.sendCommand('processPacket', {
packet: packet.toString('base64'),
});
return Buffer.from(result.packet, 'base64');
}
/**
* Ping the Rust binary for health check.
*/
public async ping(): Promise<boolean> {
const result = await this.bridge.sendCommand('ping', {});
return result.pong;
}
/**
* Kill the Rust process.
*/
public kill(): void {
this.bridge.kill();
}
/**
* Whether the bridge is running.
*/
public get running(): boolean {
return this.bridge.running;
}
}
/**
* Get the tsrust platform suffix for the current platform.
* Matches the naming convention used by @git.zone/tsrust.
*/
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;
}
+2 -1
View File
@@ -1 +1,2 @@
export * from './classes.dnsserver.js';
export * from './classes.dnsserver.js';
export * from './classes.rustdnsbridge.js';
+5 -8
View File
@@ -1,9 +1,8 @@
// node native
import crypto from 'crypto';
import dgram from 'dgram';
import { EventEmitter } from 'events';
import fs from 'fs';
import http from 'http';
import https from 'https';
import * as net from 'net';
import * as path from 'path';
@@ -11,26 +10,24 @@ export {
crypto,
dgram,
fs,
http,
https,
net,
path,
}
export const events = { EventEmitter };
// @push.rocks scope
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrust from '@push.rocks/smartrust';
export {
smartpromise,
smartrust,
}
// third party
import elliptic from 'elliptic';
import * as dnsPacket from 'dns-packet';
import * as minimatch from 'minimatch';
export {
dnsPacket,
elliptic,
minimatch,
}