Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
566a78cee4 | |||
74ac0c1287 | |||
5278c2ce78 | |||
439d08b023 |
15
changelog.md
15
changelog.md
@ -1,5 +1,20 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2024-09-19 - 6.2.0 - feat(dnssec)
|
||||||
|
Introduced DNSSEC support with ECDSA algorithm
|
||||||
|
|
||||||
|
- Added `DnsSec` class for handling DNSSEC operations.
|
||||||
|
- Updated `DnsServer` to support DNSSEC with ECDSA.
|
||||||
|
- Shifted DNS-related helper functions to `DnsServer` class.
|
||||||
|
- Integrated parsing and handling of DNSKEY and RRSIG records in `DnsServer`.
|
||||||
|
|
||||||
|
## 2024-09-19 - 6.1.1 - fix(ts_server)
|
||||||
|
Update DnsSec class to fully implement key generation and DNSKEY record creation.
|
||||||
|
|
||||||
|
- Added complete support for ECDSA and ED25519 algorithms in the DnsSec class.
|
||||||
|
- Implemented DNSKEY generation and KeyTag computation methods.
|
||||||
|
- Improved error handling and initialized the appropriate cryptographic instances based on the algorithm.
|
||||||
|
|
||||||
## 2024-09-18 - 6.1.0 - feat(smartdns)
|
## 2024-09-18 - 6.1.0 - feat(smartdns)
|
||||||
Add DNS Server and DNSSEC tools with comprehensive unit tests
|
Add DNS Server and DNSSEC tools with comprehensive unit tests
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartdns",
|
"name": "@push.rocks/smartdns",
|
||||||
"version": "6.1.0",
|
"version": "6.2.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A TypeScript library for smart DNS methods, supporting various DNS records and providers.",
|
"description": "A TypeScript library for smart DNS methods, supporting various DNS records and providers.",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
172
ts_server/classes.dnssec.ts
Normal file
172
ts_server/classes.dnssec.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
// 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 {
|
||||||
|
// Sign the data using the private key
|
||||||
|
const keyPair = this.ec!.keyFromPrivate(this.keyPair.privateKey, 'hex');
|
||||||
|
const signature = keyPair.sign(plugins.crypto.createHash('sha256').update(data).digest());
|
||||||
|
return Buffer.from(signature.toDER());
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,46 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
import { DnsSec } from './classes.dnssec.js';
|
||||||
|
import * as dnsPacket from 'dns-packet';
|
||||||
|
|
||||||
interface IDnsServerOptions {
|
interface IDnsServerOptions {
|
||||||
httpsKey: string;
|
httpsKey: string;
|
||||||
httpsCert: string;
|
httpsCert: string;
|
||||||
httpsPort: number;
|
httpsPort: number;
|
||||||
udpPort: number;
|
udpPort: number;
|
||||||
|
dnssecZone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DnsAnswer {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
class: string | number;
|
||||||
|
ttl: number;
|
||||||
|
data: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IDnsHandler {
|
interface IDnsHandler {
|
||||||
domainPattern: string;
|
domainPattern: string;
|
||||||
recordTypes: string[];
|
recordTypes: string[];
|
||||||
handler: (question: plugins.dnsPacket.Question) => plugins.dnsPacket.Answer | null;
|
handler: (question: dnsPacket.Question) => DnsAnswer | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define types for DNSSEC records if not provided
|
||||||
|
interface DNSKEYData {
|
||||||
|
flags: number;
|
||||||
|
algorithm: number;
|
||||||
|
key: Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RRSIGData {
|
||||||
|
typeCovered: string; // Changed to string to match dns-packet expectations
|
||||||
|
algorithm: number;
|
||||||
|
labels: number;
|
||||||
|
originalTTL: number;
|
||||||
|
expiration: number;
|
||||||
|
inception: number;
|
||||||
|
keyTag: number;
|
||||||
|
signerName: string;
|
||||||
|
signature: Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DnsServer {
|
export class DnsServer {
|
||||||
@ -18,30 +48,75 @@ export class DnsServer {
|
|||||||
private httpsServer: plugins.https.Server;
|
private httpsServer: plugins.https.Server;
|
||||||
private handlers: IDnsHandler[] = [];
|
private handlers: IDnsHandler[] = [];
|
||||||
|
|
||||||
constructor(private options: IDnsServerOptions) {}
|
// DNSSEC related properties
|
||||||
|
private dnsSec: DnsSec;
|
||||||
|
private dnskeyRecord: DNSKEYData;
|
||||||
|
private keyTag: number;
|
||||||
|
|
||||||
|
constructor(private options: IDnsServerOptions) {
|
||||||
|
// Initialize DNSSEC
|
||||||
|
this.dnsSec = new DnsSec({
|
||||||
|
zone: options.dnssecZone,
|
||||||
|
algorithm: 'ECDSA', // You can change this based on your needs
|
||||||
|
keySize: 256,
|
||||||
|
days: 365,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate DNSKEY and DS records
|
||||||
|
const { dsRecord, dnskeyRecord } = this.dnsSec.getDsAndKeyPair();
|
||||||
|
|
||||||
|
// Parse DNSKEY record into dns-packet format
|
||||||
|
this.dnskeyRecord = this.parseDNSKEYRecord(dnskeyRecord);
|
||||||
|
this.keyTag = this.computeKeyTag(this.dnskeyRecord);
|
||||||
|
}
|
||||||
|
|
||||||
public registerHandler(
|
public registerHandler(
|
||||||
domainPattern: string,
|
domainPattern: string,
|
||||||
recordTypes: string[],
|
recordTypes: string[],
|
||||||
handler: (question: plugins.dnsPacket.Question) => plugins.dnsPacket.Answer | null
|
handler: (question: dnsPacket.Question) => DnsAnswer | null
|
||||||
): void {
|
): void {
|
||||||
this.handlers.push({ domainPattern, recordTypes, handler });
|
this.handlers.push({ domainPattern, recordTypes, handler });
|
||||||
}
|
}
|
||||||
|
|
||||||
private processDnsRequest(request: plugins.dnsPacket.Packet): plugins.dnsPacket.Packet {
|
private processDnsRequest(request: dnsPacket.Packet): dnsPacket.Packet {
|
||||||
const response: plugins.dnsPacket.Packet = {
|
const response: dnsPacket.Packet = {
|
||||||
type: 'response',
|
type: 'response',
|
||||||
id: request.id,
|
id: request.id,
|
||||||
flags: plugins.dnsPacket.RECURSION_DESIRED | plugins.dnsPacket.RECURSION_AVAILABLE,
|
flags:
|
||||||
|
dnsPacket.AUTHORITATIVE_ANSWER |
|
||||||
|
dnsPacket.RECURSION_AVAILABLE |
|
||||||
|
(request.flags & dnsPacket.RECURSION_DESIRED ? dnsPacket.RECURSION_DESIRED : 0),
|
||||||
questions: request.questions,
|
questions: request.questions,
|
||||||
answers: [],
|
answers: [],
|
||||||
|
additionals: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dnssecRequested = this.isDnssecRequested(request);
|
||||||
|
|
||||||
for (const question of request.questions) {
|
for (const question of request.questions) {
|
||||||
console.log(`Query for ${question.name} of type ${question.type}`);
|
console.log(`Query for ${question.name} of type ${question.type}`);
|
||||||
|
|
||||||
let answered = false;
|
let answered = false;
|
||||||
|
|
||||||
|
// Handle DNSKEY queries if DNSSEC is requested
|
||||||
|
if (dnssecRequested && question.type === 'DNSKEY' && question.name === this.options.dnssecZone) {
|
||||||
|
const dnskeyAnswer: DnsAnswer = {
|
||||||
|
name: question.name,
|
||||||
|
type: 'DNSKEY',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 3600,
|
||||||
|
data: this.dnskeyRecord,
|
||||||
|
};
|
||||||
|
response.answers.push(dnskeyAnswer as plugins.dnsPacket.Answer);
|
||||||
|
|
||||||
|
// Sign the DNSKEY RRset
|
||||||
|
const rrsig = this.generateRRSIG('DNSKEY', [dnskeyAnswer], question.name);
|
||||||
|
response.answers.push(rrsig as plugins.dnsPacket.Answer);
|
||||||
|
|
||||||
|
answered = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for (const handlerEntry of this.handlers) {
|
for (const handlerEntry of this.handlers) {
|
||||||
if (
|
if (
|
||||||
plugins.minimatch.minimatch(question.name, handlerEntry.domainPattern) &&
|
plugins.minimatch.minimatch(question.name, handlerEntry.domainPattern) &&
|
||||||
@ -49,7 +124,20 @@ export class DnsServer {
|
|||||||
) {
|
) {
|
||||||
const answer = handlerEntry.handler(question);
|
const answer = handlerEntry.handler(question);
|
||||||
if (answer) {
|
if (answer) {
|
||||||
response.answers.push(answer);
|
// Ensure the answer has ttl and class
|
||||||
|
const dnsAnswer: DnsAnswer = {
|
||||||
|
...answer,
|
||||||
|
ttl: answer.ttl || 300,
|
||||||
|
class: answer.class || 'IN',
|
||||||
|
};
|
||||||
|
response.answers.push(dnsAnswer as plugins.dnsPacket.Answer);
|
||||||
|
|
||||||
|
if (dnssecRequested) {
|
||||||
|
// Sign the answer RRset
|
||||||
|
const rrsig = this.generateRRSIG(question.type, [dnsAnswer], question.name);
|
||||||
|
response.answers.push(rrsig as plugins.dnsPacket.Answer);
|
||||||
|
}
|
||||||
|
|
||||||
answered = true;
|
answered = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -58,23 +146,193 @@ export class DnsServer {
|
|||||||
|
|
||||||
if (!answered) {
|
if (!answered) {
|
||||||
console.log(`No handler found for ${question.name} of type ${question.type}`);
|
console.log(`No handler found for ${question.name} of type ${question.type}`);
|
||||||
|
response.flags |= dnsPacket.AUTHORITATIVE_ANSWER;
|
||||||
|
const soaAnswer: DnsAnswer = {
|
||||||
|
name: question.name,
|
||||||
|
type: 'SOA',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 3600,
|
||||||
|
data: {
|
||||||
|
mname: `ns1.${this.options.dnssecZone}`,
|
||||||
|
rname: `hostmaster.${this.options.dnssecZone}`,
|
||||||
|
serial: Math.floor(Date.now() / 1000),
|
||||||
|
refresh: 3600,
|
||||||
|
retry: 600,
|
||||||
|
expire: 604800,
|
||||||
|
minimum: 86400,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
response.answers.push(soaAnswer as plugins.dnsPacket.Answer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isDnssecRequested(request: dnsPacket.Packet): boolean {
|
||||||
|
if (!request.additionals) return false;
|
||||||
|
for (const additional of request.additionals) {
|
||||||
|
if (additional.type === 'OPT' && typeof additional.flags === 'number') {
|
||||||
|
// The DO bit is the 15th bit (0x8000)
|
||||||
|
if (additional.flags & 0x8000) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateRRSIG(
|
||||||
|
type: string,
|
||||||
|
rrset: DnsAnswer[],
|
||||||
|
name: string
|
||||||
|
): DnsAnswer {
|
||||||
|
// Prepare RRSIG data
|
||||||
|
const algorithm = this.dnsSec.getAlgorithmNumber();
|
||||||
|
const keyTag = this.keyTag;
|
||||||
|
const signerName = this.options.dnssecZone.endsWith('.') ? this.options.dnssecZone : `${this.options.dnssecZone}.`;
|
||||||
|
const inception = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago
|
||||||
|
const expiration = inception + 86400; // Valid for 1 day
|
||||||
|
const ttl = rrset[0].ttl || 300;
|
||||||
|
|
||||||
|
// Serialize the RRset in canonical form
|
||||||
|
const rrsetBuffer = this.serializeRRset(rrset);
|
||||||
|
|
||||||
|
// Sign the RRset
|
||||||
|
const signature = this.dnsSec.signData(rrsetBuffer);
|
||||||
|
|
||||||
|
// Construct the RRSIG record
|
||||||
|
const rrsig: DnsAnswer = {
|
||||||
|
name,
|
||||||
|
type: 'RRSIG',
|
||||||
|
class: 'IN',
|
||||||
|
ttl,
|
||||||
|
data: {
|
||||||
|
typeCovered: type, // Changed to type string
|
||||||
|
algorithm,
|
||||||
|
labels: name.split('.').length - 1,
|
||||||
|
originalTTL: ttl,
|
||||||
|
expiration,
|
||||||
|
inception,
|
||||||
|
keyTag,
|
||||||
|
signerName,
|
||||||
|
signature: signature,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return rrsig;
|
||||||
|
}
|
||||||
|
|
||||||
|
private serializeRRset(rrset: DnsAnswer[]): Buffer {
|
||||||
|
// Implement canonical DNS RRset serialization as per RFC 4034 Section 6
|
||||||
|
const buffers: Buffer[] = [];
|
||||||
|
for (const rr of rrset) {
|
||||||
|
if (rr.type === 'OPT') {
|
||||||
|
continue; // Skip OPT records
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = rr.name.endsWith('.') ? rr.name : rr.name + '.';
|
||||||
|
const nameBuffer = this.nameToBuffer(name.toLowerCase());
|
||||||
|
|
||||||
|
const typeValue = this.qtypeToNumber(rr.type);
|
||||||
|
const typeBuffer = Buffer.alloc(2);
|
||||||
|
typeBuffer.writeUInt16BE(typeValue, 0);
|
||||||
|
|
||||||
|
const classValue = this.classToNumber(rr.class);
|
||||||
|
const classBuffer = Buffer.alloc(2);
|
||||||
|
classBuffer.writeUInt16BE(classValue, 0);
|
||||||
|
|
||||||
|
const ttlValue = rr.ttl || 300;
|
||||||
|
const ttlBuffer = Buffer.alloc(4);
|
||||||
|
ttlBuffer.writeUInt32BE(ttlValue, 0);
|
||||||
|
|
||||||
|
// Serialize the data based on the record type
|
||||||
|
const dataBuffer = this.serializeRData(rr.type, rr.data);
|
||||||
|
|
||||||
|
const rdLengthBuffer = Buffer.alloc(2);
|
||||||
|
rdLengthBuffer.writeUInt16BE(dataBuffer.length, 0);
|
||||||
|
|
||||||
|
buffers.push(Buffer.concat([nameBuffer, typeBuffer, classBuffer, ttlBuffer, rdLengthBuffer, dataBuffer]));
|
||||||
|
}
|
||||||
|
return Buffer.concat(buffers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private serializeRData(type: string, data: any): Buffer {
|
||||||
|
// Implement serialization for each record type you support
|
||||||
|
switch (type) {
|
||||||
|
case 'A':
|
||||||
|
return Buffer.from(data.split('.').map((octet: string) => parseInt(octet, 10)));
|
||||||
|
case 'AAAA':
|
||||||
|
// Handle IPv6 addresses
|
||||||
|
return Buffer.from(data.split(':').flatMap((segment: string) => {
|
||||||
|
const num = parseInt(segment, 16);
|
||||||
|
return [num >> 8, num & 0xff];
|
||||||
|
}));
|
||||||
|
case 'DNSKEY':
|
||||||
|
const dnskeyData: DNSKEYData = data;
|
||||||
|
return Buffer.concat([
|
||||||
|
Buffer.from([dnskeyData.flags >> 8, dnskeyData.flags & 0xff]),
|
||||||
|
Buffer.from([3]), // Protocol field, always 3
|
||||||
|
Buffer.from([dnskeyData.algorithm]),
|
||||||
|
dnskeyData.key,
|
||||||
|
]);
|
||||||
|
case 'SOA':
|
||||||
|
// Implement SOA record serialization if needed
|
||||||
|
// For now, return an empty buffer or handle as needed
|
||||||
|
return Buffer.alloc(0);
|
||||||
|
// Add cases for other record types as needed
|
||||||
|
default:
|
||||||
|
throw new Error(`Serialization for record type ${type} is not implemented.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseDNSKEYRecord(dnskeyRecord: string): DNSKEYData {
|
||||||
|
// Parse the DNSKEY record string into dns-packet format
|
||||||
|
const parts = dnskeyRecord.trim().split(/\s+/);
|
||||||
|
const flags = parseInt(parts[3], 10);
|
||||||
|
const algorithm = parseInt(parts[5], 10);
|
||||||
|
const publicKeyBase64 = parts.slice(6).join('');
|
||||||
|
const key = Buffer.from(publicKeyBase64, 'base64');
|
||||||
|
|
||||||
|
return {
|
||||||
|
flags,
|
||||||
|
algorithm,
|
||||||
|
key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeKeyTag(dnskeyRecord: DNSKEYData): number {
|
||||||
|
// Compute key tag as per RFC 4034 Appendix B
|
||||||
|
const flags = dnskeyRecord.flags;
|
||||||
|
const algorithm = dnskeyRecord.algorithm;
|
||||||
|
const key = dnskeyRecord.key;
|
||||||
|
|
||||||
|
const dnskeyRdata = Buffer.concat([
|
||||||
|
Buffer.from([flags >> 8, flags & 0xff]),
|
||||||
|
Buffer.from([3]), // Protocol field, always 3
|
||||||
|
Buffer.from([algorithm]),
|
||||||
|
key,
|
||||||
|
]);
|
||||||
|
|
||||||
|
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 handleHttpsRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
private handleHttpsRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
||||||
if (req.method === 'POST' && req.url === '/dns-query') {
|
if (req.method === 'POST' && req.url === '/dns-query') {
|
||||||
let body: Buffer[] = [];
|
let body: Buffer[] = [];
|
||||||
|
|
||||||
req.on('data', chunk => {
|
req.on('data', (chunk) => {
|
||||||
body.push(chunk);
|
body.push(chunk);
|
||||||
}).on('end', () => {
|
}).on('end', () => {
|
||||||
const msg = Buffer.concat(body);
|
const msg = Buffer.concat(body);
|
||||||
const request = plugins.dnsPacket.decode(msg);
|
const request = dnsPacket.decode(msg);
|
||||||
const response = this.processDnsRequest(request);
|
const response = this.processDnsRequest(request);
|
||||||
const responseData = plugins.dnsPacket.encode(response);
|
const responseData = dnsPacket.encode(response);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/dns-message' });
|
res.writeHead(200, { 'Content-Type': 'application/dns-message' });
|
||||||
res.end(responseData);
|
res.end(responseData);
|
||||||
});
|
});
|
||||||
@ -95,9 +353,9 @@ export class DnsServer {
|
|||||||
|
|
||||||
this.udpServer = plugins.dgram.createSocket('udp4');
|
this.udpServer = plugins.dgram.createSocket('udp4');
|
||||||
this.udpServer.on('message', (msg, rinfo) => {
|
this.udpServer.on('message', (msg, rinfo) => {
|
||||||
const request = plugins.dnsPacket.decode(msg);
|
const request = dnsPacket.decode(msg);
|
||||||
const response = this.processDnsRequest(request);
|
const response = this.processDnsRequest(request);
|
||||||
const responseData = plugins.dnsPacket.encode(response);
|
const responseData = dnsPacket.encode(response);
|
||||||
this.udpServer.send(responseData, rinfo.port, rinfo.address);
|
this.udpServer.send(responseData, rinfo.port, rinfo.address);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -108,6 +366,7 @@ export class DnsServer {
|
|||||||
|
|
||||||
const udpListeningDeferred = plugins.smartpromise.defer<void>();
|
const udpListeningDeferred = plugins.smartpromise.defer<void>();
|
||||||
const httpsListeningDeferred = plugins.smartpromise.defer<void>();
|
const httpsListeningDeferred = plugins.smartpromise.defer<void>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.udpServer.bind(this.options.udpPort, '0.0.0.0', () => {
|
this.udpServer.bind(this.options.udpPort, '0.0.0.0', () => {
|
||||||
console.log(`UDP DNS server running on port ${this.options.udpPort}`);
|
console.log(`UDP DNS server running on port ${this.options.udpPort}`);
|
||||||
@ -144,4 +403,49 @@ export class DnsServer {
|
|||||||
|
|
||||||
await Promise.all([doneUdp.promise, doneHttps.promise]);
|
await Promise.all([doneUdp.promise, doneHttps.promise]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
|
||||||
|
private qtypeToNumber(type: string): number {
|
||||||
|
const QTYPE_NUMBERS: { [key: string]: number } = {
|
||||||
|
'A': 1,
|
||||||
|
'NS': 2,
|
||||||
|
'CNAME': 5,
|
||||||
|
'SOA': 6,
|
||||||
|
'PTR': 12,
|
||||||
|
'MX': 15,
|
||||||
|
'TXT': 16,
|
||||||
|
'AAAA': 28,
|
||||||
|
'SRV': 33,
|
||||||
|
'DNSKEY': 48,
|
||||||
|
'RRSIG': 46,
|
||||||
|
// Add more as needed
|
||||||
|
};
|
||||||
|
return QTYPE_NUMBERS[type.toUpperCase()] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private classToNumber(cls: string | number): number {
|
||||||
|
const CLASS_NUMBERS: { [key: string]: number } = {
|
||||||
|
'IN': 1,
|
||||||
|
'CH': 3,
|
||||||
|
'HS': 4,
|
||||||
|
// Add more as needed
|
||||||
|
};
|
||||||
|
if (typeof cls === 'number') {
|
||||||
|
return cls;
|
||||||
|
}
|
||||||
|
return CLASS_NUMBERS[cls.toUpperCase()] || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private nameToBuffer(name: string): Buffer {
|
||||||
|
const labels = name.split('.');
|
||||||
|
const buffers = labels.map(label => {
|
||||||
|
const len = Buffer.byteLength(label, 'utf8');
|
||||||
|
const buf = Buffer.alloc(1 + len);
|
||||||
|
buf.writeUInt8(len, 0);
|
||||||
|
buf.write(label, 1);
|
||||||
|
return buf;
|
||||||
|
});
|
||||||
|
return Buffer.concat([...buffers, Buffer.from([0])]); // Add root label
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,83 +0,0 @@
|
|||||||
import * as plugins from './plugins.js';
|
|
||||||
|
|
||||||
interface DnssecZone {
|
|
||||||
zone: string;
|
|
||||||
algorithm: string;
|
|
||||||
keySize: number;
|
|
||||||
days: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DnssecKeyPair {
|
|
||||||
private: string;
|
|
||||||
public: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DnsSec {
|
|
||||||
private zone: DnssecZone;
|
|
||||||
private keyPair: DnssecKeyPair;
|
|
||||||
private ec: any; // declare the ec instance
|
|
||||||
|
|
||||||
constructor(zone: DnssecZone) {
|
|
||||||
this.zone = zone;
|
|
||||||
this.ec = new plugins.elliptic.ec('secp256k1'); // Create an instance of the secp256k1 curve
|
|
||||||
this.keyPair = this.generateKeyPair();
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateKeyPair(): DnssecKeyPair {
|
|
||||||
const key = this.ec.genKeyPair();
|
|
||||||
const privatePem = key.getPrivate().toString('hex'); // get private key in hex format
|
|
||||||
// @ts-ignore
|
|
||||||
const publicPem = key.getPublic().toString('hex'); // get public key in hex format
|
|
||||||
|
|
||||||
return {
|
|
||||||
private: privatePem,
|
|
||||||
public: publicPem
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatPEM(pem: string, type: string): string {
|
|
||||||
const start = `-----BEGIN ${type}-----`;
|
|
||||||
const end = `-----END ${type}-----`;
|
|
||||||
|
|
||||||
const formatted = [start];
|
|
||||||
for (let i = 0; i < pem.length; i += 64) {
|
|
||||||
formatted.push(pem.slice(i, i + 64));
|
|
||||||
}
|
|
||||||
formatted.push(end);
|
|
||||||
return formatted.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
public getDSRecord(): string {
|
|
||||||
const publicPem = this.keyPair.public;
|
|
||||||
const publicKey = this.ec.keyFromPublic(publicPem); // Create a public key from the publicPEM
|
|
||||||
|
|
||||||
const digest = publicKey.getPublic(); // get public point
|
|
||||||
return `DS {id} 8 {algorithm} {digest} {hash-algorithm}\n`
|
|
||||||
.replace('{id}', '256') // zone hash
|
|
||||||
.replace('{algorithm}', this.getAlgorithm())
|
|
||||||
.replace('{digest}', `0x${digest.getX()}${digest.getY()}`)
|
|
||||||
.replace('{hash-algorithm}', '2');
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAlgorithm(): string {
|
|
||||||
switch (this.zone.algorithm) {
|
|
||||||
case 'ECDSA':
|
|
||||||
return '8';
|
|
||||||
case 'ED25519':
|
|
||||||
return '15';
|
|
||||||
case 'RSA':
|
|
||||||
return '1';
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported algorithm: ${this.zone.algorithm}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getKeyPair(): DnssecKeyPair {
|
|
||||||
return this.keyPair;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getDsAndKeyPair(): [DnssecKeyPair, string] {
|
|
||||||
const dsRecord = this.getDSRecord();
|
|
||||||
return [this.keyPair, dsRecord];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,12 @@
|
|||||||
// node native
|
// node native
|
||||||
|
import crypto from 'crypto';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
import dgram from 'dgram';
|
import dgram from 'dgram';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
crypto,
|
||||||
fs,
|
fs,
|
||||||
http,
|
http,
|
||||||
https,
|
https,
|
||||||
@ -19,7 +21,7 @@ export {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// third party
|
// third party
|
||||||
import * as elliptic from 'elliptic';
|
import elliptic from 'elliptic';
|
||||||
import * as dnsPacket from 'dns-packet';
|
import * as dnsPacket from 'dns-packet';
|
||||||
import * as minimatch from 'minimatch';
|
import * as minimatch from 'minimatch';
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user