feat(rust): add Rust-based DNS server backend with IPC management and TypeScript bridge
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
}
|
||||
+457
-929
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -1 +1,2 @@
|
||||
export * from './classes.dnsserver.js';
|
||||
export * from './classes.dnsserver.js';
|
||||
export * from './classes.rustdnsbridge.js';
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user