Compare commits

...

13 Commits

Author SHA1 Message Date
07047ee8cb Fixing wrong variable name
All checks were successful
the build was successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2018-12-03 09:04:40 +01:00
7d9ad57f52 Fixing error
Some checks failed
the build failed
2018-12-03 08:58:37 +01:00
55354c2381 Adding stream interface
Some checks failed
the build failed
2018-12-03 08:52:54 +01:00
da5285ac9b Adding new naming to test and benchmark
All checks were successful
the build was successful
2018-12-03 08:30:42 +01:00
d16f8c3d8a Normalizing names of plugin properties
Some checks failed
the build failed
2018-12-03 08:29:00 +01:00
ccee8ae5da Version bump
All checks were successful
the build was successful
2018-12-03 08:21:19 +01:00
172855b8cf Adding typscript compilation at install level for usage with repository
All checks were successful
the build was successful
2018-12-03 08:20:14 +01:00
07e2dff29f Optimizing export
All checks were successful
the build was successful
2018-12-03 07:59:29 +01:00
5bf7af6565 Add request flow handler
All checks were successful
the build was successful
2018-12-03 07:54:37 +01:00
69094524d1 Rewriting of backend
All checks were successful
the build was successful
2018-11-30 21:48:27 +01:00
e8dcf31461 Adding index.ts for bundling request and type
All checks were successful
the build was successful
2018-05-19 16:59:44 +02:00
23c27c3c4f Removing listener for export in different Module
All checks were successful
the build was successful
2018-05-19 16:10:42 +02:00
2b318d47aa Removing old code 2018-05-19 16:10:16 +02:00
31 changed files with 3929 additions and 3236 deletions

View File

@ -4,4 +4,5 @@ pipeline:
commands: commands:
- node --version && npm --version - node --version && npm --version
- npm install - npm install
- npm run build
- npm test - npm test

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules node_modules
*.pcapng *.pcapng
*.bin *.bin
lib/

32
Readme.md Normal file
View File

@ -0,0 +1,32 @@
# NodeName-Core
This repository is part of the NodeName Project, a simple to use and very flexible Nameserver implementation for NodeJS.
## What does this NodeName-Core contain?
NodeName-Core is responsible for several things:
- parsing incoming DNS requests
- serializing outgoing answers
- providing storage backend API definition
## API
``` JavaScript
const Core = new DnsCore();
Core.addStorage(new AnyStoragePlugin(options), timeout?);
// One or multiple storage plugins
// All Storage engines are requested at the same time all their results will be processed
// Optionally an timeout can be defined after wich this storage engine will be ignored
// If no others answer an error is returned in Response
Core.addMonitoring(new MonitoringPlugin(options));
// Async called Monmitoring plugin.
Core.addListener(new ListenetPlugin(options));
// The question plugin can handle on or more Questions and needs to
// make shure, that it creates the desired output. It is responsible
// for handeling the question and optionally add additional questions
// to be resolved.
Core.addQuestionHandler(new QuestionPlugin(options));
```

7
lib/listener.d.ts vendored
View File

@ -1,7 +0,0 @@
import { Request } from "./request";
export default class Listener {
private udp;
private tcp;
constructor(type: "udp4" | "udp6" | "tcp", onRequest: (request: Request) => any, host?: string);
close(): void;
}

View File

@ -1,71 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const net = require("net");
const dgram = require("dgram");
const request_1 = require("./request");
class Listener {
constructor(type, onRequest, host = "0.0.0.0") {
switch (type) {
case "udp6":
case "udp4":
this.udp = dgram.createSocket(type);
this.udp.on("listening", () => {
console.log(`UDP Server Listening on 53`);
});
this.udp.on("message", (message, remote) => {
let request = new request_1.Request(message, (data) => {
// console.log("sending:", new Request(data, (a) => 0));
this.udp.send(data, remote.port, remote.address);
});
onRequest(request);
});
this.udp.bind(53, host);
break;
case "tcp":
console.log("Using TCP is experimantal");
this.tcp = net.createServer((socket) => {
let length;
let got = 0;
let message = undefined;
socket.on("data", (data) => {
let offset = 0;
if (!message) {
length = data.readUInt16BE(0);
if (length > 4096)
return socket.destroy(); //Requests with more that 2k are ignored
message = Buffer.alloc(length);
offset = 2;
}
let read = (data.length - offset) > (length - got) ? (length - got) : (data.length - offset);
data.copy(message, got, offset, read + offset);
got += read;
//ToDo don't ignore probably following requests
if (got >= length) {
let request = new request_1.Request(message, (data) => {
socket.write(data);
});
got = 0;
message = undefined;
length = 0;
onRequest(request);
}
});
});
this.tcp.listen(53, host);
console.log(`TCP Server Listening on 53`);
break;
default:
throw new Error("Unknown socket type");
}
}
close() {
if (this.udp) {
this.udp.close();
}
else {
this.tcp.close();
}
}
}
exports.default = Listener;
//# sourceMappingURL=listener.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"listener.js","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":";;AAAA,2BAA0B;AAC1B,+BAA+B;AAC/B,uCAAoC;AAGpC;IAGG,YAAY,IAA6B,EAAE,SAAoC,EAAE,OAAe,SAAS;QACtG,QAAQ,IAAI,EAAE;YACX,KAAK,MAAM,CAAC;YACZ,KAAK,MAAM;gBACR,IAAI,CAAC,GAAG,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAA;gBACnC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,WAAW,EAAE,GAAG,EAAE;oBAC3B,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAA;gBAC5C,CAAC,CAAC,CAAA;gBAEF,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;oBACxC,IAAI,OAAO,GAAG,IAAI,iBAAO,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;wBACzC,wDAAwD;wBACxD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;oBACnD,CAAC,CAAC,CAAA;oBACF,SAAS,CAAC,OAAO,CAAC,CAAC;gBACtB,CAAC,CAAC,CAAA;gBAEF,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;gBACvB,MAAM;YACT,KAAK,KAAK;gBACP,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAA;gBACxC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,EAAE;oBACpC,IAAI,MAAc,CAAC;oBACnB,IAAI,GAAG,GAAW,CAAC,CAAC;oBACpB,IAAI,OAAO,GAAG,SAAS,CAAC;oBACxB,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;wBACxB,IAAI,MAAM,GAAG,CAAC,CAAC;wBACf,IAAI,CAAC,OAAO,EAAE;4BACX,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;4BAC9B,IAAI,MAAM,GAAG,IAAI;gCAAE,OAAO,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,wCAAwC;4BACpF,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;4BAC/B,MAAM,GAAG,CAAC,CAAC;yBACb;wBAED,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;wBAC7F,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,GAAG,MAAM,CAAC,CAAA;wBAC9C,GAAG,IAAI,IAAI,CAAC;wBACZ,+CAA+C;wBAC/C,IAAI,GAAG,IAAI,MAAM,EAAE;4BAChB,IAAI,OAAO,GAAG,IAAI,iBAAO,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;gCACzC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;4BACtB,CAAC,CAAC,CAAA;4BACF,GAAG,GAAG,CAAC,CAAC;4BACR,OAAO,GAAG,SAAS,CAAC;4BACpB,MAAM,GAAG,CAAC,CAAC;4BACX,SAAS,CAAC,OAAO,CAAC,CAAC;yBACrB;oBACJ,CAAC,CAAC,CAAC;gBACN,CAAC,CAAC,CAAC;gBACH,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;gBAC1B,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAA;gBACzC,MAAM;YACT;gBACG,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAA;SAC3C;IACJ,CAAC;IAED,KAAK;QACF,IAAI,IAAI,CAAC,GAAG,EAAE;YACX,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;SACnB;aAAM;YACJ,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;SACnB;IACJ,CAAC;CACH;AAnED,2BAmEC"}

64
lib/request.d.ts vendored
View File

@ -1,64 +0,0 @@
/// <reference types="node" />
import { IMessage, IMessageHeader, IMessageQuestion, MessageRecourceRecord, ErrorCodes } from "./types";
export declare function parseHeader(data: Buffer): IMessageHeader;
export declare function parseQuestions(count: number, packet: Buffer): IMessageQuestion[];
export declare function serializeName(name: string): Buffer;
export declare class Request implements IMessage {
private sendCallback;
private max_size;
private _header;
readonly header: Header;
private _questions;
readonly questions: Question[];
answers: RecourceRecord[];
authorities: RecourceRecord[];
additionals: RecourceRecord[];
constructor(packet: Buffer, sendCallback: (packet: Buffer) => any, max_size?: number);
error(error: ErrorCodes): void;
noRecursion(): void;
send(): void;
serialize(): Buffer;
}
export declare class Header implements IMessageHeader {
ID: number;
QR: 0 | 1;
OPCODE: number;
AA: 0 | 1;
TC: 0 | 1;
RD: 0 | 1;
RA: 0 | 1;
Z: 0 | 1;
AD: 0 | 1;
CD: 0 | 1;
RCODE: ErrorCodes;
QDCOUNT: number;
ANCOUNT: number;
NSCOUNT: number;
ARCOUNT: number;
constructor(header: IMessageHeader);
serialize(): Buffer;
}
export declare class Question implements IMessageQuestion {
QNAME: string;
QTYPE: number;
QCLASS: number;
constructor(question: IMessageQuestion);
serialize(): Buffer;
}
export declare class RecourceRecord implements MessageRecourceRecord {
constructor(data?: Partial<MessageRecourceRecord>);
/**
* This value can be set to identify if specific record is already set
*/
Identifier: string;
NAME: string;
private _TYPE;
TYPE: number;
private _CLASS;
CLASS: number;
private _TTL;
TTL: number;
RDATA: Buffer;
readonly RDLENGTH: number;
serialize(): Buffer;
}

View File

@ -1,271 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const binary_parser_1 = require("binary-parser");
const types_1 = require("./types");
const headerParser = new binary_parser_1.Parser()
.endianess("big")
.uint16("ID")
.bit1("QR")
.bit4("OPCODE")
.bit1("AA")
.bit1("TC")
.bit1("RD")
.bit1("RA")
.bit1("Z")
.bit1("AD")
.bit1("CD")
.bit4("RCODE")
.uint16("QDCOUNT")
.uint16("ANCOUNT")
.uint16("NSCOUNT")
.uint16("ARCOUNT");
function parseHeader(data) {
try {
return headerParser.parse(data);
}
catch (e) {
throw new Error("Header parsing failed" + e.message);
}
}
exports.parseHeader = parseHeader;
const labelParser = new binary_parser_1.Parser()
.endianess("big")
.uint8("dataLength")
.string("name", {
length: "dataLength",
encoding: "ascii"
});
const questionParser = new binary_parser_1.Parser()
.endianess("big")
.array("QNAME", {
type: labelParser,
readUntil: (item, buffer) => {
if (item.dataLength <= 0)
return true;
},
formatter: (value) => {
return value.map(e => e.name).join(".").slice(0, -1);
}
})
.uint16("QTYPE")
.uint16("QCLASS");
function parseQuestions(count, packet) {
try {
return new binary_parser_1.Parser()
.endianess("big")
.array("questions", {
type: questionParser,
length: count
}).parse(packet).questions;
}
catch (e) {
throw new Error("Question parsing failed" + e.message);
}
}
exports.parseQuestions = parseQuestions;
const MAX_LABEL_SIZE = 63;
function serializeName(name) {
let length = 0;
let parts = name.split(".");
parts.forEach(e => {
// Length of part and byte that holds the length information
if (e.length > MAX_LABEL_SIZE)
throw new Error("Label to large");
length += e.length + 1;
});
length += 1; //Adding last 0 length octet
let data = Buffer.alloc(length);
let offset = 0;
parts.forEach(e => {
data.writeUInt8(e.length, offset);
offset++;
data.write(e, offset, e.length);
offset += e.length;
});
data.writeUInt8(0, offset);
return data;
}
exports.serializeName = serializeName;
class Request {
constructor(packet, sendCallback, max_size = 512) {
this.sendCallback = sendCallback;
this.max_size = max_size;
this.answers = [];
this.authorities = [];
this.additionals = [];
let headerData = Buffer.alloc(12);
packet.copy(headerData, 0, 0, 12);
let bodyData = Buffer.alloc(packet.length - 12);
packet.copy(bodyData, 0, 12, packet.length);
this._header = new Header(parseHeader(headerData));
this._header.AD = 0;
this._header.RCODE = types_1.ErrorCodes.NoError;
this._header.RA = this._header.RD;
this._questions = parseQuestions(this._header.QDCOUNT, bodyData).map(e => new Question(e));
}
get header() {
return Object.assign({}, this._header);
}
get questions() {
return this._questions.map(e => Object.assign({}, e));
}
error(error) {
if (this._header.RCODE === types_1.ErrorCodes.NoError)
this._header.RCODE = error;
}
noRecursion() {
this._header.RA = 0;
}
send() {
this.sendCallback(this.serialize());
}
serialize() {
this._header.AA = 1;
this._header.ANCOUNT = this.answers.length;
this._header.ARCOUNT = this.additionals.length;
this._header.NSCOUNT = this.authorities.length;
this._header.QR = 1;
let questions = this._questions.map(e => e.serialize());
let answers = this.answers.map(e => e.serialize());
let authority = this.authorities.map(e => e.serialize());
let additional = this.additionals.map(e => e.serialize());
let length = 12;
questions.forEach(e => length += e.length);
answers.forEach(e => length += e.length);
authority.forEach(e => length += e.length);
additional.forEach(e => length += e.length);
// let questionsByteLength = 0;
// questions.forEach(e => questionsByteLength += e.length);
// let answersByteLength = 0;
// answers.forEach(e => answersByteLength += e.length)
// let authorityByteLength = 0;
// authority.forEach(e => authorityByteLength += e.length)
// let additionalByteLength = 0;
// additional.forEach(e => additionalByteLength += e.length)
// let length = 12 + questionsByteLength + answersByteLength + authorityByteLength + additionalByteLength; //Header is always 12 byte large
if (length > this.max_size) {
this._header.TC = 1;
//Will ignore data, that exceeds length
length = this.max_size;
}
let header = this._header.serialize();
let data = Buffer.alloc(length);
let offset = 0;
let append = (buffer) => {
if (offset <= length) {
buffer.copy(data, offset, 0, buffer.length);
offset += buffer.length;
}
};
append(header);
questions.forEach(append);
answers.forEach(append);
authority.forEach(append);
additional.forEach(append);
return data;
}
}
exports.Request = Request;
class Header {
constructor(header) {
for (let k in header) {
this[k] = header[k];
}
}
serialize() {
let data = Buffer.alloc(12);
data.writeUInt16BE(this.ID, 0);
var f = 0x0000;
f = f | (this.QR << 15);
f = f | (this.OPCODE << 11);
f = f | (this.AA << 10);
f = f | (this.TC << 9);
f = f | (this.RD << 8);
f = f | (this.RA << 7);
f = f | (this.Z << 6);
f = f | (this.AD << 5);
f = f | (this.CD << 4);
f = f | this.RCODE;
data.writeUInt16BE(f, 2);
data.writeUInt16BE(this.QDCOUNT, 4);
data.writeUInt16BE(this.ANCOUNT, 6);
data.writeUInt16BE(this.NSCOUNT, 8);
data.writeUInt16BE(this.ARCOUNT, 10);
return data;
}
}
exports.Header = Header;
class Question {
constructor(question) {
for (let k in question) {
this[k] = question[k];
}
}
serialize() {
let qname = serializeName(this.QNAME);
let data = Buffer.alloc(qname.length + 4);
qname.copy(data, 0, 0, qname.length);
let offset = qname.length;
data.writeUInt16BE(this.QTYPE, offset);
offset += 2;
data.writeUInt16BE(this.QCLASS, offset);
return data;
}
}
exports.Question = Question;
class RecourceRecord {
constructor(data) {
if (data) {
for (let key in data) {
this[key] = data[key];
}
}
}
set TYPE(value) {
if (value < 0 || value > 65535)
throw new TypeError("TYPE Range: 0 - 65.535");
this._TYPE = value;
}
get TYPE() {
return this._TYPE;
}
set CLASS(value) {
if (value < 0 || value > 65535)
throw new TypeError("CLASS Range: 0 - 65.535");
this._CLASS = value;
}
get CLASS() {
return this._CLASS;
}
set TTL(value) {
if (value < 0 || value > 4294967295)
throw new TypeError("TTL Range: 0 - 4.294.967.295");
this._TTL = value;
}
get TTL() {
return this._TTL;
}
get RDLENGTH() {
return this.RDATA.length;
}
serialize() {
// TODO: Implement compression
let name = serializeName(this.NAME);
let rdata = this.RDATA;
let data = Buffer.alloc(name.length + 10 + rdata.length); // For TYPE, CLASS, TTL, RLENGTH
name.copy(data, 0, 0, name.length);
let offset = name.length;
data.writeUInt16BE(this.TYPE, offset);
offset += 2;
data.writeUInt16BE(this.CLASS, offset);
offset += 2;
data.writeUInt32BE(this._TTL, offset);
offset += 4;
data.writeUInt16BE(rdata.length, offset);
offset += 2;
rdata.copy(data, offset, 0, rdata.length);
return data;
}
}
exports.RecourceRecord = RecourceRecord;
//# sourceMappingURL=request.js.map

File diff suppressed because one or more lines are too long

1
lib/test.d.ts vendored
View File

@ -1 +0,0 @@
export {};

View File

@ -1,361 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const chai_1 = require("chai");
const request_1 = require("./request");
const types_1 = require("./types");
function fromHex(data) {
return Buffer.from(data.replace(/\s/g, ""), "hex");
}
describe("parser", function () {
describe("header", function () {
describe("header parser", function () {
let should_templ = {
ID: 0,
QR: 0,
OPCODE: 0,
AA: 0,
TC: 0,
RD: 0,
RA: 0,
Z: 0,
AD: 0,
CD: 0,
RCODE: 0,
QDCOUNT: 0,
ANCOUNT: 0,
ARCOUNT: 0,
NSCOUNT: 0
};
let tests = [
{
name: "Testing ID field",
data: "0001 0000 0000 0000 0000 0000",
fields: {
ID: 1
}
},
{
name: "Testing ID field with max value",
data: "FFFF 0000 0000 0000 0000 0000",
fields: {
ID: 65535
}
},
{
name: "Testing QR field",
data: "0000 8000 0000 0000 0000 0000",
fields: {
QR: 1
}
},
{
name: "Testing OPCODE field value 2",
data: "0000 1000 0000 0000 0000 0000",
fields: {
OPCODE: 2
}
},
{
name: "Testing OPCODE field value 1",
data: "0000 0800 0000 0000 0000 0000",
fields: {
OPCODE: 1
}
},
{
name: "Testing AA field",
data: "0000 0400 0000 0000 0000 0000",
fields: {
AA: 1
}
},
{
name: "Testing TC field",
data: "0000 0200 0000 0000 0000 0000",
fields: {
TC: 1
}
},
{
name: "Testing RD field",
data: "0000 0100 0000 0000 0000 0000",
fields: {
RD: 1
}
},
{
name: "Testing RCODE field",
data: "0000 0002 0000 0000 0000 0000",
fields: {
RCODE: 2
}
},
{
name: "Testing QDCOUNT field max value",
data: "0000 0000 FFFF 0000 0000 0000",
fields: {
QDCOUNT: 65535
}
},
{
name: "Testing ANCOUNT field max value",
data: "0000 0000 0000 FFFF 0000 0000",
fields: {
ANCOUNT: 65535
}
},
{
name: "Testing NSCOUNT field max value",
data: "0000 0000 0000 0000 FFFF 0000",
fields: {
NSCOUNT: 65535
}
},
{
name: "Testing ARCOUNT field max value",
data: "0000 0000 0000 0000 0000 FFFF",
fields: {
ARCOUNT: 65535
}
},
{
name: "Testing all Flags and Values max",
data: "FFFF FFFF FFFF FFFF FFFF FFFF",
fields: {
ID: 65535,
QR: 1,
OPCODE: 15,
AA: 1,
TC: 1,
RD: 1,
RA: 1,
Z: 1,
AD: 1,
CD: 1,
RCODE: 15,
QDCOUNT: 65535,
ANCOUNT: 65535,
NSCOUNT: 65535,
ARCOUNT: 65535
}
}
];
tests.forEach(function (e) {
it(e.name, function () {
let testdata = fromHex(e.data);
let should = Object.assign({}, should_templ, e.fields); // Build in "clone" function
let header = request_1.parseHeader(testdata);
chai_1.assert.hasAllKeys(header, Object.keys(should), "Parsed header is missing some fields");
chai_1.assert.deepEqual(header, should, "Parsed header has not expected values!");
});
});
});
describe("header serializer", function () {
let empty_header = {
ID: 0,
QR: 0,
OPCODE: 0,
AA: 0,
TC: 0,
RD: 0,
RA: 0,
Z: 0,
AD: 0,
CD: 0,
RCODE: 0,
QDCOUNT: 0,
ANCOUNT: 0,
ARCOUNT: 0,
NSCOUNT: 0
};
let tests = [
{
name: "Fill header with 0s",
result: "0000 0000 0000 0000 0000 0000",
values: {}
},
{
name: "Set header id to 5",
result: "0005 0000 0000 0000 0000 0000",
values: {
ID: 5
}
},
{
name: "Set QR",
result: "0000 8000 0000 0000 0000 0000",
values: {
QR: 1
}
},
{
name: "Set OPCODE",
result: "0000 7800 0000 0000 0000 0000",
values: {
OPCODE: 15
}
},
{
name: "Set AA",
result: "0000 0400 0000 0000 0000 0000",
values: {
AA: 1
}
},
{
name: "Set TC",
result: "0000 0200 0000 0000 0000 0000",
values: {
TC: 1
}
},
{
name: "Set RD",
result: "0000 0100 0000 0000 0000 0000",
values: {
RD: 1
}
},
{
name: "Set RA",
result: "0000 0080 0000 0000 0000 0000",
values: {
RA: 1
}
},
{
name: "Set Z",
result: "0000 0040 0000 0000 0000 0000",
values: {
Z: 1
}
},
{
name: "Set AD",
result: "0000 0020 0000 0000 0000 0000",
values: {
AD: 1
}
},
{
name: "Set CD",
result: "0000 0010 0000 0000 0000 0000",
values: {
CD: 1
}
},
{
name: "Set RCODE",
result: "0000 000F 0000 0000 0000 0000",
values: {
RCODE: 15
}
},
{
name: "Set QDCOUNT",
result: "0000 0000 FFFF 0000 0000 0000",
values: {
QDCOUNT: 65535
}
},
{
name: "Set ANCOUNT",
result: "0000 0000 0000 FFFF 0000 0000",
values: {
ANCOUNT: 65535
}
},
{
name: "Set NSCOUNT",
result: "0000 0000 0000 0000 FFFF 0000",
values: {
NSCOUNT: 65535
}
},
{
name: "Set ARCOUNT",
result: "0000 0000 0000 0000 0000 FFFF",
values: {
ARCOUNT: 65535
}
},
];
tests.forEach(function (e) {
it(e.name, function () {
let header = Object.assign({}, empty_header, e.values);
let serialized = new request_1.Header(header).serialize();
chai_1.assert.equal(serialized.toString("hex"), e.result.replace(/\s/g, "").toLowerCase(), "Header serialization failed");
});
});
});
});
describe("question", function () {
let questionData = fromHex("0474 6573 7407 6578 616d 706c 6503 636f 6d00 0001 0001");
let questionObj = {
QNAME: "test.example.com",
QTYPE: types_1.QueryTypes.A,
QCLASS: 1
};
it("check question parser with one question", function () {
let res = request_1.parseQuestions(1, questionData);
let should = [questionObj];
chai_1.assert.deepEqual(res, should, "Question parser does not parse input correctly");
});
it("check question serialization", function () {
let res = new request_1.Question(questionObj).serialize();
chai_1.assert.equal(res.toString("hex"), questionData.toString("hex"), "Query serializer does not serialite correctly");
});
});
it("recource record serialization", function () {
let should = "07 6578616D706C65 03 636f6D 00 0001 0001 00000640 0004 0A000001";
let rr = new request_1.RecourceRecord();
rr.CLASS = 1;
rr.NAME = "example.com";
rr.TTL = 1600;
rr.TYPE = 1;
rr.RDATA = fromHex("0A 00 00 01");
let res = rr.serialize().toString("hex");
chai_1.assert.equal(res, should.replace(/\s/g, "").toLowerCase(), "Serialization not working properly");
});
it("recource record constructor value assign", function () {
let should = "07 6578616D706C65 03 636f6D 00 0001 0001 00000640 0004 0A000001";
let rr = new request_1.RecourceRecord({
CLASS: 1,
NAME: "example.com",
TTL: 1600,
TYPE: 1,
RDATA: fromHex("0A 00 00 01")
});
let res = rr.serialize().toString("hex");
chai_1.assert.equal(res, should.replace(/\s/g, "").toLowerCase(), "Serialization not working properly");
});
it("full response serialization", function () {
let reqData = fromHex("E835 0100 0001 0000 0000 0000 07 6578616D706c65 03636F6D 00 0001 0001");
let should = "E835 8580 0001000100000000 076578616D706C6503636F6D0000010001 07 6578616D706C65 03 636F6D 00 0001 0001 0000 0640 0004 0A000001";
let request = new request_1.Request(reqData, () => null);
let rr = new request_1.RecourceRecord();
rr.CLASS = 1;
rr.NAME = "example.com";
rr.TTL = 1600;
rr.TYPE = 1;
rr.RDATA = fromHex("0A 00 00 01");
request.answers.push(rr);
let data = request.serialize();
chai_1.assert.equal(data.toString("hex"), should.replace(/\s/g, "").toLowerCase(), "Whole packet serialization failed");
});
// it("full response serialization benchmark", function () {
// let reqData = fromHex("E835 0100 0001 0000 0000 0000 07 6578616D706c65 03636F6D 00 0001 0001");
// for (let i = 0; i < 100; i++) {
// let request = new Request(reqData, () => null)
// let rr = new RecourceRecord()
// rr.CLASS = 1
// rr.NAME = "example.com"
// rr.TTL = 1600
// rr.TYPE = 1
// rr.RDATA = fromHex("0A 00 00 01")
// request.answers.push(rr)
// request.serialize()
// }
// })
});
//# sourceMappingURL=test.js.map

File diff suppressed because one or more lines are too long

268
lib/types.d.ts vendored
View File

@ -1,268 +0,0 @@
/// <reference types="node" />
export declare enum ErrorCodes {
/**
* No error
*/
NoError = 0,
/**
* Format error - unable to interpret query
*/
FormatError = 1,
/**
* Server failure - internal problem
*/
ServerFailure = 2,
/**
* Name error - Only for authorative name server, domain name of query does not exist
*/
NameError = 3,
/**
* Not implemented - Request not supported
*/
NotImplemented = 4,
/**
* Refused - Nameserver refuses request
*/
Refused = 5,
}
export declare enum QueryTypes {
/**
* IPv4 address
*/
A = 1,
/**
* Nameserver
*/
NS = 2,
/**
* Obsolete
*/
MD = 3,
/**
* Obsolete
*/
MF = 4,
/**
* Alias
*/
CNAME = 5,
/**
* Start of authority
*/
SOA = 6,
/**
* Experimental
*/
MB = 7,
/**
* Experimental
*/
MG = 8,
/**
* Experimental
*/
MR = 9,
/**
* Experimental
*/
NULL = 10,
/**
* Service description
*/
WKS = 11,
/**
* Reverse entry (inaddr.arpa)
*/
PTR = 12,
/**
* Host information
*/
HINFO = 13,
/**
* Mailbox / Mail-list information
*/
MINFO = 14,
/**
* Mail exchange
*/
MX = 15,
/**
* Text strings
*/
TXT = 16,
/**
* IPv6 address
*/
AAAA = 28,
/**
* SRV records
*/
SRV = 33,
/**
* Request to transfer entire zone
*/
AXFR = 252,
/**
* Request for mailbox related records
*/
MAILA = 254,
/**
* Request for mail agend RRs
*/
MAILB = 253,
/**
* Any class
*/
ANY = 255,
}
export interface IMessageHeader {
/**
* A 16 bit identifier assigned by the program that
* generates any kind of query. This identifier is copied
* the corresponding reply and can be used by the requester
* to match up replies to outstanding queries.
*/
ID: number;
/**
* Defines if query or response
*/
QR: 0 | 1;
/**
* 4 Bit code, that defines type of query.
* 0 Standard query
* 1 Inverse query
* 2 Server status request
* 3-15 reserved for future use
*/
OPCODE: number;
/**
* Authorative Answer - only valid in responses and
* specifies that the responding name server is an
* authority for the domain name in question section
*/
AA: 0 | 1;
/**
* Truncation - specifies that his message was truncated doe to
* length grater than permitted on the transaction channel
*
* WARNING: NOT IMPLEMENTED IN THIS APPLICATION
*/
TC: 0 | 1;
/**
* Recursion Desired - set in query and copied to response
* if is set, directs name server to pursue the query recursively
*
* WARNING: NOT IMPLEMENTED IN THIS APPLICATION
*/
RD: 0 | 1;
/**
* Recursion Available - will be cleared in response to
* show the client that recursion is not available
*/
RA: 0 | 1;
/**
* Reserved for future usage, must be 0 in all queries
*/
Z: 0 | 1;
AD: 0 | 1;
CD: 0 | 1;
/**
* Response Code - 4 bit field is part of response
*
* 0 No error condition
* 1 Format error - unable to interpret query
* 2 Server failure - internal problem
* 3 Name error - Only for authorative name server, domain name of query does not exist
* 4 Not implemented - Request not supported
* 5 Refused - Nameserver refuses request
* 6-15 Reserved for future usage
*/
RCODE: ErrorCodes;
/**
* Number of entries in question section
* uint16
*/
QDCOUNT: number;
/**
* Number of entries in answer section
* uint16
*/
ANCOUNT: number;
/**
* Number of resource records in authority records section
* uint16
*/
NSCOUNT: number;
/**
* Number of resource records in additional records section
* uint16
*/
ARCOUNT: number;
}
export interface IMessageQuestion {
/**
* Domain name represented as sequence of labels
* Each label consists of a length octed followed
* by that number of octeds
*/
QNAME: string;
/**
* Two octed code wich specifies the type of the query.
*/
QTYPE: number;
/**
* Two octed code that specifies the class of the Query
* IS for internet
* WARNING: ONLY IN IS SUPPORTED BY THIS APPLICATION
*/
QCLASS: number;
}
export interface MessageRecourceRecord {
/**
* Domain name to wich resource record pertains
*/
NAME: string;
/**
* Two octets containing TT type code.
* Specifies meaning of data in RDATA field
*
* uint16
*/
TYPE: number;
/**
* Two octets specifying class of RDATA field
*
* uint16
*/
CLASS: number;
/**
* Specifies Time Interval (in seconds) that the record
* may be cached before discarded.
* Zero values are not cached
*
* uint32
*/
TTL: number;
/**
* Length of RDATA field
*
* uint16
*/
RDLENGTH: number;
/**
* a variable length string of ectets taht describes
* the resource. The format is defined by TYPE and CLASS
* field.
*
* If TYPE is A and CLASS is IN, RDATA is a 4 octet
* ARPA internet address.
*/
RDATA: Buffer;
}
export interface IMessage {
header: IMessageHeader;
questions: IMessageQuestion[];
answers: MessageRecourceRecord[];
authorities: MessageRecourceRecord[];
additionals: MessageRecourceRecord[];
}

View File

@ -1,121 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var ErrorCodes;
(function (ErrorCodes) {
/**
* No error
*/
ErrorCodes[ErrorCodes["NoError"] = 0] = "NoError";
/**
* Format error - unable to interpret query
*/
ErrorCodes[ErrorCodes["FormatError"] = 1] = "FormatError";
/**
* Server failure - internal problem
*/
ErrorCodes[ErrorCodes["ServerFailure"] = 2] = "ServerFailure";
/**
* Name error - Only for authorative name server, domain name of query does not exist
*/
ErrorCodes[ErrorCodes["NameError"] = 3] = "NameError";
/**
* Not implemented - Request not supported
*/
ErrorCodes[ErrorCodes["NotImplemented"] = 4] = "NotImplemented";
/**
* Refused - Nameserver refuses request
*/
ErrorCodes[ErrorCodes["Refused"] = 5] = "Refused";
})(ErrorCodes = exports.ErrorCodes || (exports.ErrorCodes = {}));
var QueryTypes;
(function (QueryTypes) {
/**
* IPv4 address
*/
QueryTypes[QueryTypes["A"] = 1] = "A";
/**
* Nameserver
*/
QueryTypes[QueryTypes["NS"] = 2] = "NS";
/**
* Obsolete
*/
QueryTypes[QueryTypes["MD"] = 3] = "MD";
/**
* Obsolete
*/
QueryTypes[QueryTypes["MF"] = 4] = "MF";
/**
* Alias
*/
QueryTypes[QueryTypes["CNAME"] = 5] = "CNAME";
/**
* Start of authority
*/
QueryTypes[QueryTypes["SOA"] = 6] = "SOA";
/**
* Experimental
*/
QueryTypes[QueryTypes["MB"] = 7] = "MB";
/**
* Experimental
*/
QueryTypes[QueryTypes["MG"] = 8] = "MG";
/**
* Experimental
*/
QueryTypes[QueryTypes["MR"] = 9] = "MR";
/**
* Experimental
*/
QueryTypes[QueryTypes["NULL"] = 10] = "NULL";
/**
* Service description
*/
QueryTypes[QueryTypes["WKS"] = 11] = "WKS";
/**
* Reverse entry (inaddr.arpa)
*/
QueryTypes[QueryTypes["PTR"] = 12] = "PTR";
/**
* Host information
*/
QueryTypes[QueryTypes["HINFO"] = 13] = "HINFO";
/**
* Mailbox / Mail-list information
*/
QueryTypes[QueryTypes["MINFO"] = 14] = "MINFO";
/**
* Mail exchange
*/
QueryTypes[QueryTypes["MX"] = 15] = "MX";
/**
* Text strings
*/
QueryTypes[QueryTypes["TXT"] = 16] = "TXT";
/**
* IPv6 address
*/
QueryTypes[QueryTypes["AAAA"] = 28] = "AAAA";
/**
* SRV records
*/
QueryTypes[QueryTypes["SRV"] = 33] = "SRV";
/**
* Request to transfer entire zone
*/
QueryTypes[QueryTypes["AXFR"] = 252] = "AXFR";
/**
* Request for mailbox related records
*/
QueryTypes[QueryTypes["MAILA"] = 254] = "MAILA";
/**
* Request for mail agend RRs
*/
QueryTypes[QueryTypes["MAILB"] = 253] = "MAILB";
/**
* Any class
*/
QueryTypes[QueryTypes["ANY"] = 255] = "ANY";
})(QueryTypes = exports.QueryTypes || (exports.QueryTypes = {}));
//# sourceMappingURL=types.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;AAAA,IAAY,UA8BX;AA9BD,WAAY,UAAU;IACnB;;OAEG;IACH,iDAAW,CAAA;IAEX;;OAEG;IACH,yDAAW,CAAA;IAEX;;OAEG;IACH,6DAAa,CAAA;IAEb;;OAEG;IACH,qDAAS,CAAA;IAET;;OAEG;IACH,+DAAc,CAAA;IAEd;;OAEG;IACH,iDAAO,CAAA;AACV,CAAC,EA9BW,UAAU,GAAV,kBAAU,KAAV,kBAAU,QA8BrB;AAED,IAAY,UA8GX;AA9GD,WAAY,UAAU;IACnB;;OAEG;IACH,qCAAQ,CAAA;IAER;;OAEG;IACH,uCAAS,CAAA;IAET;;OAEG;IACH,uCAAS,CAAA;IAET;;OAEG;IACH,uCAAS,CAAA;IAET;;OAEG;IACH,6CAAY,CAAA;IAEZ;;OAEG;IACH,yCAAU,CAAA;IAEV;;OAEG;IACH,uCAAS,CAAA;IAET;;OAEG;IACH,uCAAS,CAAA;IAET;;OAEG;IACH,uCAAS,CAAA;IAET;;OAEG;IACH,4CAAW,CAAA;IAEX;;OAEG;IACH,0CAAU,CAAA;IAEV;;OAEG;IACH,0CAAU,CAAA;IAEV;;OAEG;IACH,8CAAY,CAAA;IAEZ;;OAEG;IACH,8CAAY,CAAA;IAEZ;;OAEG;IACH,wCAAS,CAAA;IAET;;OAEG;IACH,0CAAU,CAAA;IAEV;;OAEG;IACH,4CAAW,CAAA;IAEX;;OAEG;IACH,0CAAU,CAAA;IAEV;;OAEG;IACH,6CAAW,CAAA;IAEX;;OAEG;IACH,+CAAY,CAAA;IAEZ;;OAEG;IACH,+CAAY,CAAA;IAEZ;;OAEG;IACH,2CAAU,CAAA;AACb,CAAC,EA9GW,UAAU,GAAV,kBAAU,KAAV,kBAAU,QA8GrB"}

2814
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,32 @@
{ {
"name": "nodename-core", "name": "nodename-core",
"version": "0.1.10", "version": "0.3.2",
"main": "lib/listener.js", "main": "lib/index.js",
"types": "lib/listener.d.ts", "types": "lib/index.d.ts",
"author": "Fabian Stamm <dev@fabianstamm.de>", "author": "Fabian Stamm <dev@fabianstamm.de>",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"watch": "tsc --watch", "watch": "tsc --watch",
"prepublish": "tsc",
"install": "tsc",
"test": "mocha src/test.ts --require ts-node/register", "test": "mocha src/test.ts --require ts-node/register",
"testwatch": "mocha src/test.ts --require ts-node/register --watch --watch-extensions ts" "testwatch": "mocha src/test.ts --require ts-node/register --watch --watch-extensions ts"
}, },
"devDependencies": { "devDependencies": {
"@types/binary-parser": "^1.3.0", "@types/binary-parser": "^1.3.0",
"@types/chai": "^4.1.3", "@types/chai": "^4.1.7",
"@types/mocha": "^5.2.0", "@types/lru-cache": "^4.1.1",
"@types/node": "^10.0.8", "@types/mocha": "^5.2.5",
"chai": "^4.1.2", "@types/node": "^10.12.11",
"mocha": "^5.1.1", "chai": "^4.2.0",
"nodemon": "^1.17.4", "mocha": "^5.2.0",
"typescript": "^2.8.3", "nodemon": "^1.18.7",
"ts-node": "^6.0.3" "ts-node": "^7.0.1",
"typescript": "^3.2.1"
}, },
"dependencies": { "dependencies": {
"binary-parser": "^1.3.2" "binary-parser": "^1.3.2",
"lru-cache": "^5.1.1"
} }
} }

171
src/benchmark.ts Normal file
View File

@ -0,0 +1,171 @@
import { Request, parseInput } from "./request";
import { RDATARecord, RecourceRecord } from "./record";
import { MonitoringPlugin, QuestionPlugin, DnsCore, AnswerHandler, ListenerPlugin, StoragePlugin, Record } from "./core";
import assert = require("assert");
import { RecordTypes } from "./types";
import { Question } from "./question";
function fromHex(data: string) {
return Buffer.from(data.replace(/\s/g, ""), "hex");
}
const itr = 100000;
console.log("All tests x" + itr);
let reqData = fromHex("E835 0100 0001 0000 0000 0000 07 6578616D706c65 03636F6D 00 0001 0001");
console.time("input parser")
for (let i = 0; i < itr; i++) {
parseInput(reqData);
}
console.timeEnd("input parser")
console.time("complete")
for (let i = 0; i < itr; i++) {
let request = new Request(reqData, "")
let rr = new RDATARecord()
rr.CLASS = 1
rr.NAME = "example.com"
rr.TTL = 1600
rr.TYPE = 1
rr.RDATA = fromHex("0A 00 00 01")
request.addAnswer(rr)
request.serialize()
}
console.timeEnd("complete")
console.time("complete 10 answers")
for (let i = 0; i < itr; i++) {
let request = new Request(reqData, "")
let rr = new RDATARecord()
rr.CLASS = 1
rr.NAME = "example.com"
rr.TTL = 1600
rr.TYPE = 1
rr.RDATA = fromHex("0A 00 00 01")
for (let i = 0; i < 10; i++) request.addAnswer(rr);
request.serialize()
}
console.timeEnd("complete 10 answers")
class TestMonitoringPlugin implements MonitoringPlugin {
constructor() { }
async init(core) { }
priority = 0
onRequest(domain, hostname, type) { }
}
class ARR extends RecourceRecord {
TYPE = RecordTypes.A
constructor(domain: string, ip: string) {
super();
this.NAME = domain
this.TTL = 1600
let data = Buffer.alloc(4)
let idx = 0;
ip.split(".").forEach(e => {
data.writeUInt8(Number(e), idx);
idx++;
})
this.dataLock = () => {
return {
length: 4,
serialize: (buffer, offset) => {
data.copy(buffer, offset);
return offset + 4;
}
}
};
}
}
class TestQuestionPlugin implements QuestionPlugin {
private Core: DnsCore
question_types = [RecordTypes.A]
priority = 0
async init(core) {
this.Core = core;
}
async handleQuestion(question: Question, request: AnswerHandler, next: () => void) {
assert(this.question_types.find(e => e === <any>question.QTYPE), "Handler was called with not supported question type")
let parts = question.QNAME.split(".")
let domain = parts.splice(-2, 2).join(".")
let hostname = parts.join(".");
hostname = hostname !== "" ? hostname : "*"
let records = await this.Core.storageManager.getRecords(domain, hostname, question.QTYPE)
records.forEach(e => request.addAnswer(new ARR(question.QNAME, e.value)));
}
}
class TestListenerPlugin implements ListenerPlugin {
async init(core) { }
priority = 0;
callback: (data: Buffer, sender: string, max_size?: number) => Promise<Buffer>;
registerCallback(callback) {
this.callback = callback;
}
sendRequest(message: Buffer) {
return this.callback(message, "localhost")
}
}
class TestStoragePlugin implements StoragePlugin {
re: Record = { domain: "example.com", hostname: "test", type: RecordTypes.A, value: "10.0.0.1", ttl: 1600 }
constructor(record?: Record, private count: number = 1) {
if (record)
this.re = record;
}
priority = 0
no_cache = false
record_types = [RecordTypes.A]
isResponsible(domain: string) {
// console.log("Is responsible")
return true
}
async init(core) { }
async getRecords(domain, hostname, type) {
if (domain !== this.re.domain || hostname !== this.re.hostname || type != this.re.type) return undefined;
let r = [];
for (let i = 0; i < this.count; i++) {
r.push(this.re);
}
return r;
}
async getAllRecordsForDomain(domain) {
if (domain !== this.re.domain) return undefined;
return [this.re];
}
}
let core = new DnsCore();
let listener = new TestListenerPlugin();
core.addListener(listener)
core.addMonitoring(new TestMonitoringPlugin());
core.addQuestion(new TestQuestionPlugin())
let storage = new TestStoragePlugin({
domain: "example.com",
hostname: "test",
type: RecordTypes.A,
ttl: 500,
value: "10.0.0.1"
}, 3)
core.addStorage(storage);
core.start().then(async () => {
let reqData = fromHex("E835 0100 0001 0000 0000 0000 04 74657374 07 6578616D706c65 03 636F6D 00 0001 0001");
console.time("core handler");
for (let i = 0; i < itr; i++) {
await listener.sendRequest(reqData)
}
console.timeEnd("core handler");
});

323
src/core.ts Normal file
View File

@ -0,0 +1,323 @@
import { Request } from "./request";
import { RecordTypes } from "./types";
import * as LRU from "lru-cache";
import { Writable } from "stream";
import { Question } from "./question";
import { RecourceRecord } from "./record";
// export class AnswerSteam extends Writable {
// }
export interface Record {
type: RecordTypes
domain: string
hostname: string
value: string
ttl: number
priority?: number
additional?: string
}
export interface Plugin {
/**
* This sets the priority of this Plugin.
*
* The lower the value, the higher priority.
* It is recommended to make this value changeable over the cosntructor options field.
*/
priority: number
init(core: DnsCore): Promise<void>
}
export type ListenerCallback =
((data: Buffer, sender: string, max_size?: number) => Promise<Buffer>) |
((data: Buffer, sender: string, answer: Writable) => Promise<void>)
export interface ListenerPlugin extends Plugin {
/**
* This method is for registering a callback that is called
* when packet is received and ready for parsing.
* The function returns the Answer data. Errors shouldn't be possible
* by this function
*/
registerCallback(callback: ListenerCallback): void
}
export class ListenerManager {
private _listener: ListenerPlugin[] = []
add(listener: ListenerPlugin) {
this._listener.push(listener)
}
setup(core: DnsCore) {
return Promise.all(this._listener.map(e => e.init(core)))
}
registerCallback(callback: ListenerCallback) {
this._listener.forEach(e => e.registerCallback(callback))
}
}
export interface MonitoringPlugin extends Plugin {
onRequest(domain: string, hostname: string, type: RecordTypes, additionalInformations): void
}
export class MonitoringManager {
private _monitoring: MonitoringPlugin[] = []
add(monitoring: MonitoringPlugin) {
this._monitoring.push(monitoring)
}
setup(core: DnsCore) {
return Promise.all(this._monitoring.map(e => e.init(core)))
}
onRequest(domain: string, hostname: string, type: RecordTypes, additionalInformations) {
this._monitoring.forEach(e => e.onRequest(domain, hostname, type, additionalInformations))
}
}
export interface StoragePlugin extends Plugin {
/**
* Here you can define wich record types this storage plugin is capable of.
*/
record_types: RecordTypes[]
/**
* Disables record caching for this Storage
*/
no_cache: boolean;
/**
* Returns if storage plugin is responsible for this domain.
* This check is synchroneus because it should be quick.
*
* @param domain The domain for check
*/
isResponsible(domain: string): boolean
getRecords(domain: string, hostname: string, type: RecordTypes): Promise<Record[] | undefined>
getAllRecordsForDomain(domain: string): Promise<Record[]>
}
type SortedStorage = {
[P in keyof any]: StoragePlugin[] | undefined
}
const cacheoptions: LRU.Options = {
length: () => 1,
max: 500,
maxAge: 10 * 60 * 1000
}
class RecordCache {
private cache = new LRU<string, Record[]>(cacheoptions);
getRecords(domain: string, hostname: string, type: RecordTypes) {
let key = `${domain};;${hostname};;${RecordTypes[type]}`;
let rec = this.cache.get(key)
if (!rec) {
return undefined;
}
return rec;
}
addRecords(domain: string, hostname: string, type: RecordTypes, records: Record[], ttl: number) {
let key = `${domain};;${hostname};;${RecordTypes[type]}`;
this.cache.set(key, records, ttl);
}
}
export class StorageManager {
private _storages: StoragePlugin[] = []
private _sorted: SortedStorage;
private _cache = new RecordCache();
add(storage: StoragePlugin) {
this._storages.push(storage)
}
private presort() {
this._storages = this._storages.sort((e1, e2) => e1.priority - e2.priority)
this._sorted = <any>{}
for (let key in RecordTypes) {
let key_n = Number(key)
if (key_n !== Number.NaN) {
//The _storages list is sorted with priority.
this._sorted[key_n] = this._storages.filter(e => e.record_types.find(t => t === key_n) !== undefined)
}
}
}
setup(core: DnsCore) {
this.presort()
return Promise.all(this._storages.map(e => e.init(core)))
}
async getAllRecordsForDomain(domain: string) {
return this._storages.find(e => e.isResponsible(domain)).getAllRecordsForDomain(domain);
}
async getRecords(domain: string, hostname: string, type: RecordTypes) {
// First check if record is cached. Since the dns entries should not chacnge quickly
// using the cach for quicker response times is the way to go.
let records = this._cache.getRecords(domain, hostname, type);
if (records) return records;
let storages = this._sorted[type].filter(s => s.isResponsible(domain));
if (!storages || storages.length <= 0) return undefined
let nocache = false;
for (let storage of storages) {
nocache = storage.no_cache || nocache;
records = await storage.getRecords(domain, hostname, type)
if (records) {
if (storage.no_cache) this._cache.addRecords(domain, hostname, type, records, records.length <= 0 ? 3600 : records[0].ttl)
return records;
}
}
// If any of the checked storage engines that is
// responsible for the domain has the noCache flag,
// caching will be disabled for all.
if (!nocache)
this._cache.addRecords(domain, hostname, type, [], 0);
return [];
}
}
export interface AnswerHandler {
addQuestion: (question: Question) => void;
addAnswer: (record: RecourceRecord) => void;
addAdditional: (record: RecourceRecord) => void;
addAutority: (record: RecourceRecord) => void
}
export interface QuestionPlugin extends Plugin {
/**
* This field defines wich question types this plugin is capable of handling.
*/
question_types: RecordTypes[];
init(core: DnsCore): Promise<void>
/**
*
* @param question The question this plugin should solve
* @param request This gives the plugin access to relevant
* functions for queing depending questions or adding answers etc.
* @param next This function should be called if the plugin is not able to resolve
* the request. This will trigger other handlers.
*/
handleQuestion(question: Question, request: AnswerHandler, next: () => void): Promise<void>
}
export class QuestionManager {
private _questions: QuestionPlugin[] = [];
private _sorted = <any>{};
add(question: QuestionPlugin) {
this._questions.push(question);
}
private sort() {
this._questions = this._questions.sort((e1, e2) => e1.priority - e2.priority);
this._sorted = {};
for (let key in RecordTypes) {
let key_n = Number(key)
if (key_n !== Number.NaN) {
this._sorted[key_n] = this._questions.filter(e => e.question_types.find(t => t === key_n) !== undefined)
}
}
}
setup(core: DnsCore) {
this.sort();
return Promise.all(this._questions.map(e => e.init(core)))
}
async handleQuestion(question: Question, request: AnswerHandler) {
let handlers: QuestionPlugin[] = this._sorted[question.QTYPE];
if (!handlers || handlers.length <= 0) return;
let index = 0;
while (index < handlers.length) {
let i = index;
await handlers[i].handleQuestion(question, request, () => {
index++;
})
if (i === index) break;
}
}
}
export class DnsCore {
storageManager: StorageManager;
addStorage(plugin: StoragePlugin) {
this.storageManager.add(plugin);
}
monitoringManager: MonitoringManager;
addMonitoring(plugin: MonitoringPlugin) {
this.monitoringManager.add(plugin);
}
listenerManager: ListenerManager;
addListener(plugin: ListenerPlugin) {
this.listenerManager.add(plugin);
}
questionManager: QuestionManager;
addQuestion(plugin: QuestionPlugin) {
this.questionManager.add(plugin);
}
constructor() {
this.listenerManager = new ListenerManager()
this.monitoringManager = new MonitoringManager()
this.storageManager = new StorageManager()
this.questionManager = new QuestionManager()
}
async start() {
await this.storageManager.setup(this);
await this.monitoringManager.setup(this);
await this.listenerManager.setup(this);
await this.questionManager.setup(this);
this.listenerManager.registerCallback(this.onMessage.bind(this));
}
private onMessage: ListenerCallback = async (data: Buffer, sender: string, r) => {
let max_size = 128 * 1024;
let stream = true;
if (!r || typeof r === "number") {
max_size = r;
stream = false;
}
let request = new Request(data, sender, max_size);
let questionqueue: Promise<void>[] = [];
let handleQuestion = question => {
let prom = this.questionManager.handleQuestion(question, {
addAdditional: record => {
request.addAdditionals(record)
},
addAnswer: record => {
request.addAnswer(record);
},
addAutority: record => {
request.addAuthorities(record);
},
addQuestion: question => {
handleQuestion(question)
}
})
questionqueue.push(prom);
}
request.questions.map(q => handleQuestion(q));
await Promise.all(questionqueue);
let res = request.serialize();
if (stream) {
(<Writable>r).write(res);
} else {
return res;
}
}
}

75
src/header.ts Normal file
View File

@ -0,0 +1,75 @@
import { IMessageHeader, ErrorCodes } from "./types";
import { Serializeable } from "./serializeable";
export class Header implements IMessageHeader, Serializeable {
ID: number;
QR: 0 | 1;
OPCODE: number
AA: 0 | 1;
TC: 0 | 1;
RD: 0 | 1;
RA: 0 | 1;
Z: 0 | 1;
AD: 0 | 1;
CD: 0 | 1;
RCODE: ErrorCodes;
QDCOUNT: number;
ANCOUNT: number;
NSCOUNT: number;
ARCOUNT: number;
constructor(header: IMessageHeader) {
for (let k in header) {
this[k] = header[k];
}
}
lock() {
var f = 0x0000;
f = f | (this.QR << 15);
f = f | (this.OPCODE << 11);
f = f | (this.AA << 10);
f = f | (this.TC << 9);
f = f | (this.RD << 8);
f = f | (this.RA << 7);
f = f | (this.Z << 6);
f = f | (this.AD << 5);
f = f | (this.CD << 4);
f = f | this.RCODE;
let id = this.ID;
let qdc = this.QDCOUNT;
let anc = this.ANCOUNT;
let nsc = this.NSCOUNT;
let arc = this.ARCOUNT;
return {
length: 12,
serialize: (buffer: Buffer, offset: number) => {
buffer.writeUInt16BE(id, offset);
offset += 2;
buffer.writeUInt16BE(f, offset);
offset += 2;
buffer.writeUInt16BE(qdc, offset);
offset += 2;
buffer.writeUInt16BE(anc, offset);
offset += 2;
buffer.writeUInt16BE(nsc, offset);
offset += 2;
buffer.writeUInt16BE(arc, offset);
return offset + 2;
}
}
}
// serialize() {
// let data = Buffer.alloc(12);
// data.writeUInt16BE(this.ID, 0);
// data.writeUInt16BE(f, 2);
// data.writeUInt16BE(this.QDCOUNT, 4)
// data.writeUInt16BE(this.ANCOUNT, 6)
// data.writeUInt16BE(this.NSCOUNT, 8)
// data.writeUInt16BE(this.ARCOUNT, 10)
// return data;
// }
}

4
src/index.ts Normal file
View File

@ -0,0 +1,4 @@
export { DnsCore, ListenerPlugin, MonitoringPlugin, StoragePlugin, QuestionPlugin, AnswerHandler, Record } from "./core"
export { RecourceRecord, RDATARecord } from "./record"
export { Request } from "./request"
export { Label } from "./label"

60
src/label.ts Normal file
View File

@ -0,0 +1,60 @@
import { Serializeable } from "./serializeable";
const MAX_LABEL_SIZE = 63;
export class Label implements Serializeable {
constructor(public name?: string) { }
public lock() {
let parts = this.name.split(".");
let length = 0;
parts.forEach(e => {
if (e.length > MAX_LABEL_SIZE) throw new Error("Label to large");
length += e.length + 1;
})
length += 1;
return {
length: length,
serialize: (buffer: Buffer, offset: number) => {
parts.forEach(e => {
buffer.writeUInt8(e.length, offset)
offset++
buffer.write(e, offset, e.length)
offset += e.length
})
buffer.writeUInt8(0, offset);
offset += 1;
return offset;
}
}
}
// set name(name: string) {
// if (name != this._name) {
// let length = 0;
// let parts = name.split(".")
// parts.forEach(e => {
// if (e.length > MAX_LABEL_SIZE) throw new Error("Label to large");
// length += e.length + 1;
// })
// length += 1; //Adding last 0 length octet
// this._length = length;
// this._name_parts = parts;
// this._name = name;
// }
// }
// get name() {
// return this._name;
// }
// get length() {
// return this._length;
// }
// serialize(buffer: Buffer, offset: number) {
// }
}

View File

@ -1,73 +0,0 @@
import * as net from "net"
import * as dgram from "dgram";
import { Request } from "./request";
import { PassThrough } from "stream";
export default class Listener {
private udp: dgram.Socket
private tcp: net.Server
constructor(type: "udp4" | "udp6" | "tcp", onRequest: (request: Request) => any, host: string = "0.0.0.0") {
switch (type) {
case "udp6":
case "udp4":
this.udp = dgram.createSocket(type)
this.udp.on("listening", () => {
console.log(`UDP Server Listening on 53`)
})
this.udp.on("message", (message, remote) => {
let request = new Request(message, (data) => {
// console.log("sending:", new Request(data, (a) => 0));
this.udp.send(data, remote.port, remote.address)
})
onRequest(request);
})
this.udp.bind(53, host)
break;
case "tcp":
console.log("Using TCP is experimantal")
this.tcp = net.createServer((socket) => {
let length: number;
let got: number = 0;
let message = undefined;
socket.on("data", (data) => {
let offset = 0;
if (!message) {
length = data.readUInt16BE(0);
if (length > 4096) return socket.destroy(); //Requests with more that 2k are ignored
message = Buffer.alloc(length);
offset = 2;
}
let read = (data.length - offset) > (length - got) ? (length - got) : (data.length - offset);
data.copy(message, got, offset, read + offset)
got += read;
//ToDo don't ignore probably following requests
if (got >= length) {
let request = new Request(message, (data) => {
socket.write(data);
})
got = 0;
message = undefined;
length = 0;
onRequest(request);
}
});
});
this.tcp.listen(53, host);
console.log(`TCP Server Listening on 53`)
break;
default:
throw new Error("Unknown socket type")
}
}
close() {
if (this.udp) {
this.udp.close();
} else {
this.tcp.close();
}
}
}

0
src/lock.ts Normal file
View File

37
src/question.ts Normal file
View File

@ -0,0 +1,37 @@
import { IMessageQuestion } from "./types";
import { Label } from "./label";
import { Serializeable } from "./serializeable";
export class Question implements IMessageQuestion, Serializeable {
private _QNAME = new Label();
get QNAME() { return this._QNAME.name };
_QTYPE: number;
get QTYPE() { return this._QTYPE };
_QCLASS: number;
get QCLASS() { return this._QCLASS };
constructor(question: IMessageQuestion) {
this._QCLASS = question.QCLASS;
this._QTYPE = question.QTYPE;
this._QNAME.name = question.QNAME;
}
lock() {
let name = this._QNAME.lock();
let type = this._QTYPE;
let cl = this._QCLASS;
return {
length: name.length + 4,
serialize: (buffer: Buffer, offset: number) => {
offset = name.serialize(buffer, offset);
buffer.writeUInt16BE(type, offset);
offset += 2;
buffer.writeUInt16BE(cl, offset);
offset += 2;
return offset;
}
}
}
}

92
src/record.ts Normal file
View File

@ -0,0 +1,92 @@
import { Serializeable, Serializer } from "./serializeable";
import { MessageRecourceRecord } from "./types";
import { Label } from "./label";
export class RecourceRecord implements Partial<MessageRecourceRecord>, Serializeable {
/**
* This value can be set to identify if specific record is already set
*/
public Identifier: string;
private _NAME = new Label();
public get NAME() { return this._NAME.name };
public set NAME(name) { this._NAME.name = name };
private _TYPE: number;
public get TYPE() { return this._TYPE }
public set TYPE(value) {
if (value < 0 || value > 65535) throw new TypeError("TYPE Range: 0 - 65.535")
this._TYPE = value;
}
private _CLASS: number = 1;
public get CLASS() { return this._CLASS }
public set CLASS(value) {
if (value < 0 || value > 65535) throw new TypeError("CLASS Range: 0 - 65.535")
this._CLASS = value;
}
private _TTL: number;
public get TTL() { return this._TTL }
public set TTL(value) {
if (value < 0 || value > 4294967295) throw new TypeError("TTL Range: 0 - 4.294.967.295")
this._TTL = value;
}
constructor(data?: Partial<MessageRecourceRecord>) {
if (data) {
for (let key in data) {
this[key] = data[key];
}
}
}
protected dataLock?: () => Serializer;
public lock() {
if (!this.dataLock) return {
length: 0,
serialize: (buffer: Buffer, offset: number) => offset
}
let name = this._NAME.lock();
let type = this.TYPE;
let cl = this.CLASS;
let ttl = this.TTL;
let data = this.dataLock();
let length = name.length + 2 + 2 + 4 + 2 + data.length;
return {
length: length,
serialize: (buffer: Buffer, offset: number) => {
offset = name.serialize(buffer, offset);
buffer.writeUInt16BE(type, offset)
offset += 2
buffer.writeUInt16BE(cl, offset)
offset += 2
buffer.writeUInt32BE(ttl, offset)
offset += 4
buffer.writeUInt16BE(data.length, offset)
offset += 2
return data.serialize(buffer, offset);
}
}
}
}
export class RDATARecord extends RecourceRecord {
public RDATA: Buffer;
get RDLENGTH() {
return this.RDATA.length;
}
dataLock = () => {
return {
length: this.RDATA.length,
serialize: (buffer: Buffer, offset: number) => {
this.RDATA.copy(buffer, offset);
return offset + this.RDATA.length;
}
}
}
}

View File

@ -1,7 +1,11 @@
import { Parser } from "binary-parser" import { Parser } from "binary-parser"
import { IMessage, IMessageHeader, IMessageQuestion, MessageRecourceRecord, ErrorCodes } from "./types" import { IMessageHeader, IMessageQuestion, MessageRecourceRecord, ErrorCodes } from "./types"
import { Serializer, Serializeable } from "./serializeable";
import { Question } from "./question";
import { RecourceRecord } from "./record";
import { Header } from "./header";
const headerParser: Parser = new Parser() export const headerParser: Parser = new Parser()
.endianess("big") .endianess("big")
.uint16("ID") .uint16("ID")
.bit1("QR") .bit1("QR")
@ -19,15 +23,7 @@ const headerParser: Parser = new Parser()
.uint16("NSCOUNT") .uint16("NSCOUNT")
.uint16("ARCOUNT") .uint16("ARCOUNT")
export function parseHeader(data: Buffer): IMessageHeader { export const labelParser = new Parser()
try {
return <any>headerParser.parse(data);
} catch (e) {
throw new Error("Header parsing failed" + e.message)
}
}
const labelParser = new Parser()
.endianess("big") .endianess("big")
.uint8("dataLength") .uint8("dataLength")
.string("name", { .string("name", {
@ -35,7 +31,7 @@ const labelParser = new Parser()
encoding: "ascii" encoding: "ascii"
}) })
const questionParser = new Parser() export const questionParser = new Parser()
.endianess("big") .endianess("big")
.array("QNAME", { .array("QNAME", {
type: labelParser, type: labelParser,
@ -50,69 +46,54 @@ const questionParser = new Parser()
.uint16("QTYPE") .uint16("QTYPE")
.uint16("QCLASS") .uint16("QCLASS")
export function parseQuestions(count: number, packet: Buffer): IMessageQuestion[] {
try {
return <any>new Parser()
.endianess("big")
.array("questions", {
type: questionParser,
length: count
}).parse(packet).questions;
} catch (e) {
throw new Error("Question parsing failed" + e.message)
}
}
const MAX_LABEL_SIZE = 63; const packetParser = new Parser()
.endianess("big")
export function serializeName(name: string) { .nest("header", { type: headerParser })
let length = 0; .array("questions", {
let parts = name.split("."); type: questionParser,
parts.forEach(e => { length: "header.QDCOUNT"
// Length of part and byte that holds the length information
if (e.length > MAX_LABEL_SIZE) throw new Error("Label to large");
length += e.length + 1;
}) })
length += 1; //Adding last 0 length octet export function parseInput(data: Buffer): { header: IMessageHeader, questions: IMessageQuestion[] } {
let data = Buffer.alloc(length); let res = packetParser.parse(data);
let offset = 0; return <any>res;
parts.forEach(e => {
data.writeUInt8(e.length, offset)
offset++
data.write(e, offset, e.length)
offset += e.length
})
data.writeUInt8(0, offset);
return data;
} }
export class Request implements IMessage { export class Request {
private _header: Header; private _header: Header;
get header() { get header() {
return Object.assign({}, this._header); return { ...this._header };
} }
private _questions: Question[]; private _questions: Question[];
get questions() { get questions() {
return this._questions.map(e => Object.assign({}, e)); return this._questions.map(e => new Question(e));
} }
answers: RecourceRecord[] = []; private _answers: Serializer[] = [];
authorities: RecourceRecord[] = []; addAnswer(rr: RecourceRecord) {
additionals: RecourceRecord[] = []; this._answers.push(rr.lock())
}
constructor(packet: Buffer, private sendCallback: (packet: Buffer) => any, private max_size = 512) { private _authorities: Serializer[] = [];
let headerData = Buffer.alloc(12); addAuthorities(rr: RecourceRecord) {
packet.copy(headerData, 0, 0, 12); this._authorities.push(rr.lock())
let bodyData = Buffer.alloc(packet.length - 12); }
packet.copy(bodyData, 0, 12, packet.length);
this._header = new Header(parseHeader(headerData)); private _additionals: Serializer[] = [];
addAdditionals(rr: RecourceRecord) {
this._additionals.push(rr.lock())
}
constructor(packet: Buffer, public sender: string, private max_size = 0) {
let parsed = parseInput(packet);
this._header = new Header(parsed.header);
this._header.AD = 0; this._header.AD = 0;
this._header.RCODE = ErrorCodes.NoError; this._header.RCODE = ErrorCodes.NoError;
this._header.RA = this._header.RD; this._header.RA = this._header.RD;
this._questions = parseQuestions(this._header.QDCOUNT, bodyData).map(e => new Question(e)); this._questions = parsed.questions.map(e => new Question(e));
} }
error(error: ErrorCodes) { error(error: ErrorCodes) {
@ -124,55 +105,39 @@ export class Request implements IMessage {
this._header.RA = 0; this._header.RA = 0;
} }
send() {
this.sendCallback(this.serialize());
}
serialize() { serialize() {
this._header.AA = 1; this._header.AA = 1;
this._header.ANCOUNT = this.answers.length; this._header.ANCOUNT = this._answers.length;
this._header.ARCOUNT = this.additionals.length; this._header.ARCOUNT = this._additionals.length;
this._header.NSCOUNT = this.authorities.length; this._header.NSCOUNT = this._authorities.length;
this._header.QR = 1; this._header.QR = 1;
let questions = this._questions.map(e => e.serialize())
let answers = this.answers.map(e => e.serialize())
let authority = this.authorities.map(e => e.serialize())
let additional = this.additionals.map(e => e.serialize())
let length = 12; let header = this._header.lock();
let questions = this._questions.map(e => e.lock())
let answers = this._answers;
let authority = this._authorities;
let additional = this._additionals;
let length = header.length;
questions.forEach(e => length += e.length); questions.forEach(e => length += e.length);
answers.forEach(e => length += e.length) answers.forEach(e => length += e.length)
authority.forEach(e => length += e.length) authority.forEach(e => length += e.length)
additional.forEach(e => length += e.length) additional.forEach(e => length += e.length)
// let questionsByteLength = 0;
// questions.forEach(e => questionsByteLength += e.length);
// let answersByteLength = 0; if (this.max_size && length > this.max_size) {
// answers.forEach(e => answersByteLength += e.length)
// let authorityByteLength = 0;
// authority.forEach(e => authorityByteLength += e.length)
// let additionalByteLength = 0;
// additional.forEach(e => additionalByteLength += e.length)
// let length = 12 + questionsByteLength + answersByteLength + authorityByteLength + additionalByteLength; //Header is always 12 byte large
if (length > this.max_size) {
this._header.TC = 1; this._header.TC = 1;
//Will ignore data, that exceeds length //Will ignore data, that exceeds length
length = this.max_size; length = this.max_size;
} }
let header = this._header.serialize();
let data = Buffer.alloc(length) let data = Buffer.alloc(length)
let offset = 0; let offset = 0;
let append = (buffer: Buffer) => { let append = (ser: Serializer) => {
if (offset <= length) { if (offset <= length) {
buffer.copy(data, offset, 0, buffer.length) offset = ser.serialize(data, offset);
offset += buffer.length;
} }
} }
append(header) append(header)
@ -183,144 +148,3 @@ export class Request implements IMessage {
return data; return data;
} }
} }
export class Header implements IMessageHeader {
ID: number;
QR: 0 | 1;
OPCODE: number
AA: 0 | 1;
TC: 0 | 1;
RD: 0 | 1;
RA: 0 | 1;
Z: 0 | 1;
AD: 0 | 1;
CD: 0 | 1;
RCODE: ErrorCodes;
QDCOUNT: number;
ANCOUNT: number;
NSCOUNT: number;
ARCOUNT: number;
constructor(header: IMessageHeader) {
for (let k in header) {
this[k] = header[k];
}
}
serialize() {
let data = Buffer.alloc(12);
data.writeUInt16BE(this.ID, 0);
var f = 0x0000;
f = f | (this.QR << 15);
f = f | (this.OPCODE << 11);
f = f | (this.AA << 10);
f = f | (this.TC << 9);
f = f | (this.RD << 8);
f = f | (this.RA << 7);
f = f | (this.Z << 6);
f = f | (this.AD << 5);
f = f | (this.CD << 4);
f = f | this.RCODE;
data.writeUInt16BE(f, 2);
data.writeUInt16BE(this.QDCOUNT, 4)
data.writeUInt16BE(this.ANCOUNT, 6)
data.writeUInt16BE(this.NSCOUNT, 8)
data.writeUInt16BE(this.ARCOUNT, 10)
return data;
}
}
export class Question implements IMessageQuestion {
QNAME: string;
QTYPE: number;
QCLASS: number;
constructor(question: IMessageQuestion) {
for (let k in question) {
this[k] = question[k]
}
}
serialize() {
let qname = serializeName(this.QNAME);
let data = Buffer.alloc(qname.length + 4);
qname.copy(data, 0, 0, qname.length);
let offset = qname.length;
data.writeUInt16BE(this.QTYPE, offset);
offset += 2;
data.writeUInt16BE(this.QCLASS, offset);
return data;
}
}
export class RecourceRecord implements MessageRecourceRecord {
constructor(data?: Partial<MessageRecourceRecord>) {
if (data) {
for (let key in data) {
this[key] = data[key];
}
}
}
/**
* This value can be set to identify if specific record is already set
*/
Identifier: string;
NAME: string
private _TYPE: number;
set TYPE(value) {
if (value < 0 || value > 65535) throw new TypeError("TYPE Range: 0 - 65.535")
this._TYPE = value;
}
get TYPE() {
return this._TYPE;
}
private _CLASS: number;
set CLASS(value) {
if (value < 0 || value > 65535) throw new TypeError("CLASS Range: 0 - 65.535")
this._CLASS = value;
}
get CLASS() {
return this._CLASS;
}
private _TTL: number;
set TTL(value) {
if (value < 0 || value > 4294967295) throw new TypeError("TTL Range: 0 - 4.294.967.295")
this._TTL = value;
}
get TTL() {
return this._TTL;
}
RDATA: Buffer;
get RDLENGTH() {
return this.RDATA.length;
}
public serialize() {
// TODO: Implement compression
let name = serializeName(this.NAME);
let rdata = this.RDATA;
let data = Buffer.alloc(name.length + 10 + rdata.length) // For TYPE, CLASS, TTL, RLENGTH
name.copy(data, 0, 0, name.length);
let offset = name.length;
data.writeUInt16BE(this.TYPE, offset)
offset += 2
data.writeUInt16BE(this.CLASS, offset)
offset += 2
data.writeUInt32BE(this._TTL, offset)
offset += 4
data.writeUInt16BE(rdata.length, offset)
offset += 2
rdata.copy(data, offset, 0, rdata.length)
return data;
}
}

19
src/serializeable.ts Normal file
View File

@ -0,0 +1,19 @@
export interface Serializer {
/**
*
* @param buffer Buffer to write to (should be capable to hold this.length)
* @param offset The offset at wich to start writing
* @returns The new offset (should be equal to original offset + this.length)
*/
serialize(buffer: Buffer, offset: number): number;
/**
* Returns length of serialized data
* @readonly
*/
length: number;
}
export interface Serializeable {
lock(): Serializer;
}

View File

@ -1,7 +1,10 @@
import { assert, expect } from "chai"; import { assert } from "chai";
import { parseHeader, Header, parseQuestions, Question, RecourceRecord, Request } from "./request" import { parseInput, Request, questionParser, headerParser } from "./request"
import { IMessageHeader, IMessageQuestion, QueryTypes } from "./types"; import { IMessageHeader, RecordTypes } from "./types";
import { RDATARecord, RecourceRecord } from "./record";
import { Question } from "./question";
import { Header } from "./header";
function fromHex(data: string) { function fromHex(data: string) {
return Buffer.from(data.replace(/\s/g, ""), "hex"); return Buffer.from(data.replace(/\s/g, ""), "hex");
@ -11,21 +14,7 @@ describe("parser", function () {
describe("header", function () { describe("header", function () {
describe("header parser", function () { describe("header parser", function () {
let should_templ: IMessageHeader = { let should_templ: IMessageHeader = {
ID: 0, ID: 0, QR: 0, OPCODE: 0, AA: 0, TC: 0, RD: 0, RA: 0, Z: 0, AD: 0, CD: 0, RCODE: 0, QDCOUNT: 0, ANCOUNT: 0, ARCOUNT: 0, NSCOUNT: 0
QR: 0,
OPCODE: 0,
AA: 0,
TC: 0,
RD: 0,
RA: 0,
Z: 0,
AD: 0,
CD: 0,
RCODE: 0,
QDCOUNT: 0,
ANCOUNT: 0,
ARCOUNT: 0,
NSCOUNT: 0
} }
let tests: { name: string, data: string, fields: Partial<IMessageHeader> }[] = [ let tests: { name: string, data: string, fields: Partial<IMessageHeader> }[] = [
@ -124,29 +113,15 @@ describe("parser", function () {
name: "Testing all Flags and Values max", name: "Testing all Flags and Values max",
data: "FFFF FFFF FFFF FFFF FFFF FFFF", data: "FFFF FFFF FFFF FFFF FFFF FFFF",
fields: { fields: {
ID: 65535, ID: 65535, QR: 1, OPCODE: 15, AA: 1, TC: 1, RD: 1, RA: 1, Z: 1, AD: 1, CD: 1, RCODE: 15, QDCOUNT: 65535, ANCOUNT: 65535, NSCOUNT: 65535, ARCOUNT: 65535
QR: 1,
OPCODE: 15,
AA: 1,
TC: 1,
RD: 1,
RA: 1,
Z: 1,
AD: 1,
CD: 1,
RCODE: 15,
QDCOUNT: 65535,
ANCOUNT: 65535,
NSCOUNT: 65535,
ARCOUNT: 65535
} }
} }
] ]
tests.forEach(function (e) { tests.forEach(function (e) {
it(e.name, function () { it(e.name, function () {
let testdata = fromHex(e.data) let testdata = fromHex(e.data)
let should: IMessageHeader = Object.assign({}, should_templ, e.fields) // Build in "clone" function let should: IMessageHeader = { ...should_templ, ...e.fields }
let header = parseHeader(testdata); let header = <any>headerParser.parse(testdata);
assert.hasAllKeys(header, Object.keys(should), "Parsed header is missing some fields") assert.hasAllKeys(header, Object.keys(should), "Parsed header is missing some fields")
assert.deepEqual(header, should, "Parsed header has not expected values!") assert.deepEqual(header, should, "Parsed header has not expected values!")
}) })
@ -155,21 +130,7 @@ describe("parser", function () {
describe("header serializer", function () { describe("header serializer", function () {
let empty_header: IMessageHeader = { let empty_header: IMessageHeader = {
ID: 0, ID: 0, QR: 0, OPCODE: 0, AA: 0, TC: 0, RD: 0, RA: 0, Z: 0, AD: 0, CD: 0, RCODE: 0, QDCOUNT: 0, ANCOUNT: 0, ARCOUNT: 0, NSCOUNT: 0
QR: 0,
OPCODE: 0,
AA: 0,
TC: 0,
RD: 0,
RA: 0,
Z: 0,
AD: 0,
CD: 0,
RCODE: 0,
QDCOUNT: 0,
ANCOUNT: 0,
ARCOUNT: 0,
NSCOUNT: 0
} }
let tests: { name: string, result: string, values: Partial<IMessageHeader> }[] = [ let tests: { name: string, result: string, values: Partial<IMessageHeader> }[] = [
{ {
@ -287,8 +248,11 @@ describe("parser", function () {
tests.forEach(function (e) { tests.forEach(function (e) {
it(e.name, function () { it(e.name, function () {
let header = Object.assign({}, empty_header, e.values) let header = Object.assign({}, empty_header, e.values)
let serialized = new Header(header).serialize(); let slz = new Header(header).lock();
assert.equal(serialized.toString("hex"), e.result.replace(/\s/g, "").toLowerCase(), "Header serialization failed"); assert.equal(slz.length, 12, "Header returns wrong length");
let res = Buffer.alloc(slz.length);
assert.equal(slz.serialize(res, 0), 12, "Header returns wrong offset");
assert.equal(res.toString("hex"), e.result.replace(/\s/g, "").toLowerCase(), "Header serialization failed");
}) })
}) })
}) })
@ -298,75 +262,257 @@ describe("parser", function () {
let questionData = fromHex("0474 6573 7407 6578 616d 706c 6503 636f 6d00 0001 0001") let questionData = fromHex("0474 6573 7407 6578 616d 706c 6503 636f 6d00 0001 0001")
let questionObj = { let questionObj = {
QNAME: "test.example.com", QNAME: "test.example.com",
QTYPE: QueryTypes.A, QTYPE: RecordTypes.A,
QCLASS: 1 QCLASS: 1
} }
it("check question parser with one question", function () { it("check question parser with one question", function () {
let res = parseQuestions(1, questionData) let res = questionParser.parse(questionData)
let should: IMessageQuestion[] = [questionObj]
assert.deepEqual(res, should, "Question parser does not parse input correctly") assert.deepEqual(res, questionObj, "Question parser does not parse input correctly")
}) })
it("check question serialization", function () { it("check question serialization", function () {
let res = new Question(questionObj).serialize() let slz = new Question(questionObj).lock()
assert.equal(slz.length, 22, "Question serializer returns wrong length")
let res = Buffer.alloc(slz.length);
assert.equal(slz.serialize(res, 0), 22, "Question serializer returns wrong offset")
assert.equal(res.toString("hex"), questionData.toString("hex"), "Query serializer does not serialite correctly"); assert.equal(res.toString("hex"), questionData.toString("hex"), "Query serializer does not serialite correctly");
}) })
}) })
it("parse valid request", () => {
let should = {
header: { ID: 59445, QR: 0, OPCODE: 0, AA: 0, TC: 0, RD: 1, RA: 0, Z: 0, AD: 0, CD: 0, RCODE: 0, QDCOUNT: 1, ANCOUNT: 0, NSCOUNT: 0, ARCOUNT: 0 },
questions: [{ QNAME: 'example.com', QTYPE: 1, QCLASS: 1 }]
}
let data = fromHex("E835 0100 0001 0000 0000 0000 07 6578616D706c65 03636F6D 00 0001 0001");
let res = parseInput(data);
assert.deepEqual(res, should);
})
it("recource record serialization", function () { it("recource record serialization", function () {
let should = "07 6578616D706C65 03 636f6D 00 0001 0001 00000640 0004 0A000001" let should = "07 6578616D706C65 03 636f6D 00 0001 0001 00000640 0004 0A000001"
let rr = new RecourceRecord() let rr = new RDATARecord()
rr.CLASS = 1 rr.CLASS = 1
rr.NAME = "example.com" rr.NAME = "example.com"
rr.TTL = 1600 rr.TTL = 1600
rr.TYPE = 1 rr.TYPE = 1
rr.RDATA = fromHex("0A 00 00 01") rr.RDATA = fromHex("0A 00 00 01")
let res = rr.serialize().toString("hex") let srl = rr.lock();
assert.equal(res, should.replace(/\s/g, "").toLowerCase(), "Serialization not working properly") let res = Buffer.alloc(srl.length);
srl.serialize(res, 0);
assert.equal(res.toString("hex"), should.replace(/\s/g, "").toLowerCase(), "Serialization not working properly")
}) })
it("recource record constructor value assign", function () { it("recource record constructor value assign", function () {
let should = "07 6578616D706C65 03 636f6D 00 0001 0001 00000640 0004 0A000001" let should = "07 6578616D706C65 03 636f6D 00 0001 0001 00000640 0004 0A000001"
let rr = new RecourceRecord({ let rr = new RDATARecord({
CLASS: 1, CLASS: 1,
NAME: "example.com", NAME: "example.com",
TTL: 1600, TTL: 1600,
TYPE: 1, TYPE: 1,
RDATA: fromHex("0A 00 00 01") RDATA: fromHex("0A 00 00 01")
}) })
let res = rr.serialize().toString("hex") let srl = rr.lock();
assert.equal(res, should.replace(/\s/g, "").toLowerCase(), "Serialization not working properly") assert.equal(srl.length, 27, "Record serializer returns wrong length")
let res = Buffer.alloc(srl.length);
assert.equal(srl.serialize(res, 0), 27, "Record serializer returns wrong offset")
assert.equal(res.toString("hex"), should.replace(/\s/g, "").toLowerCase(), "Record serialization not working properly")
}) })
it("full response serialization", function () { it("full response serialization", function () {
let reqData = fromHex("E835 0100 0001 0000 0000 0000 07 6578616D706c65 03636F6D 00 0001 0001"); let reqData = fromHex("E835 0100 0001 0000 0000 0000 07 6578616D706c65 03636F6D 00 0001 0001");
let should = "E835 8580 0001000100000000 076578616D706C6503636F6D0000010001 07 6578616D706C65 03 636F6D 00 0001 0001 0000 0640 0004 0A000001" let should = "E835 8580 0001000100000000 076578616D706C6503636F6D0000010001 07 6578616D706C65 03 636F6D 00 0001 0001 0000 0640 0004 0A000001"
let request = new Request(reqData, () => null) let request = new Request(reqData, "")
let rr = new RecourceRecord() let rr = new RDATARecord()
rr.CLASS = 1 rr.CLASS = 1
rr.NAME = "example.com" rr.NAME = "example.com"
rr.TTL = 1600 rr.TTL = 1600
rr.TYPE = 1 rr.TYPE = 1
rr.RDATA = fromHex("0A 00 00 01") rr.RDATA = fromHex("0A 00 00 01")
request.answers.push(rr) request.addAnswer(rr);
let data = request.serialize() let data = request.serialize()
assert.equal(data.toString("hex"), should.replace(/\s/g, "").toLowerCase(), "Whole packet serialization failed") assert.equal(data.toString("hex"), should.replace(/\s/g, "").toLowerCase(), "Whole packet serialization failed")
}) })
})
// it("full response serialization benchmark", function () { import { DnsCore, StoragePlugin, Record, MonitoringPlugin, QuestionPlugin, AnswerHandler, ListenerPlugin } from "./core"
// let reqData = fromHex("E835 0100 0001 0000 0000 0000 07 6578616D706c65 03636F6D 00 0001 0001");
// for (let i = 0; i < 100; i++) { describe("DNS Core", function () {
// let request = new Request(reqData, () => null) it("Initialization", () => {
// let rr = new RecourceRecord() let core = new DnsCore();
// rr.CLASS = 1 assert.exists(core, "Core constructor working")
// rr.NAME = "example.com" })
// rr.TTL = 1600
// rr.TYPE = 1 it("Adding fake plugins and initialize", () => {
// rr.RDATA = fromHex("0A 00 00 01") let core = new DnsCore();
// request.answers.push(rr)
// request.serialize() core.addListener({
// } init: async (c) => { assert.exists(c, "Core not defined on Plugin init") },
// }) priority: 0,
}) registerCallback: () => { }
})
core.addMonitoring({
init: async (c) => { assert.exists(c, "Core not defined on Plugin init") },
priority: 0,
onRequest: () => { }
})
core.addQuestion({
init: async (c) => { assert.exists(c, "Core not defined on Plugin init") },
question_types: [RecordTypes.A],
priority: 0,
handleQuestion: async () => { },
})
core.addStorage({
getAllRecordsForDomain: async () => undefined,
getRecords: async () => undefined,
init: async (c) => { assert.exists(c, "Core not defined on Plugin init") },
isResponsible: () => true,
no_cache: false,
priority: 0,
record_types: [RecordTypes.A]
})
return core.start();
})
it("Test Request flow", async function () {
let core = new DnsCore();
let listener = new TestListenerPlugin();
core.addListener(listener)
let monitoring = new TestMonitoringPlugin({
domain: "example.com",
hostname: "test",
type: "A"
});
core.addMonitoring(monitoring)
let question = new TestQuestionPlugin();
core.addQuestion(question)
let storage = new TestStoragePlugin({
domain: "example.com",
hostname: "test",
type: RecordTypes.A,
ttl: 500,
value: "10.0.0.1"
})
core.addStorage(storage);
await core.start();
let reqData = fromHex("E835 0100 0001 0000 0000 0000 04 74657374 07 6578616D706c65 03 636F6D 00 0001 0001");
let should = "E835 8580 0001000100000000 04 74657374 076578616D706C6503636F6D0000010001 04 74657374 07 6578616D706C65 03 636F6D 00 0001 0001 0000 0640 0004 0A000001"
let resData = await listener.sendRequest(reqData)
assert.equal(resData.toString("hex"), should.replace(/\s/g, "").toLowerCase(), "Whole packet serialization failed")
})
})
class TestStoragePlugin implements StoragePlugin {
re: Record = { domain: "example.com", hostname: "test", type: RecordTypes.A, value: "10.0.0.1", ttl: 1600 }
constructor(record?: Record) {
if (record)
this.re = record;
}
priority = 0
no_cache = false
record_types = [RecordTypes.A]
isResponsible(domain: string) {
// console.log("Is responsible")
return true
}
async init(core) { assert.exists(core, "TestStoragePlugin init no core object") }
async getRecords(domain, hostname, type) {
if (domain !== this.re.domain || hostname !== this.re.hostname || type != this.re.type) return undefined;
return [this.re];
}
async getAllRecordsForDomain(domain) {
if (domain !== this.re.domain) return undefined;
return [this.re];
}
}
class TestMonitoringPlugin implements MonitoringPlugin {
constructor(private should: { domain: string, hostname: string, type: string }) { }
async init(core) { assert.exists(core, "TestMonitoringPlugin init no core object") }
priority = 0
onRequest(domain, hostname, type) {
assert.equal(this.should.domain, domain, "Domain value wrong")
assert.equal(this.should.hostname, hostname, "Domain value wrong")
assert.equal(this.should.type, type, "Domain value wrong")
}
}
class ARR extends RecourceRecord {
TYPE = RecordTypes.A
constructor(domain: string, ip: string) {
super();
this.NAME = domain
this.TTL = 1600
let data = Buffer.alloc(4)
let idx = 0;
ip.split(".").forEach(e => {
data.writeUInt8(Number(e), idx);
idx++;
})
this.dataLock = () => {
return {
length: 4,
serialize: (buffer, offset) => {
data.copy(buffer, offset);
return offset + 4;
}
}
};
// this.RDATA = data;
}
}
class TestQuestionPlugin implements QuestionPlugin {
private Core: DnsCore
question_types = [RecordTypes.A]
priority = 0
async init(core) {
assert.exists(core, "TestQuestionPlugin init no core object")
this.Core = core;
}
async handleQuestion(question: Question, request: AnswerHandler, next: () => void) {
assert(this.question_types.find(e => e === <any>question.QTYPE), "Handler was called with not supported questin type")
let parts = question.QNAME.split(".")
let domain = parts.splice(-2, 2).join(".")
let hostname = parts.join(".");
hostname = hostname !== "" ? hostname : "*"
let records = await this.Core.storageManager.getRecords(domain, hostname, question.QTYPE)
records.forEach(e => request.addAnswer(new ARR(question.QNAME, e.value)));
}
}
class TestListenerPlugin implements ListenerPlugin {
async init(core) { assert.exists(core, "TestListenerPlugin init no core object") }
priority = 0;
callback: (data: Buffer, sender: string, max_size?: number) => Promise<Buffer>;
registerCallback(callback) {
this.callback = callback;
}
sendRequest(message: Buffer) {
return this.callback(message, "localhost")
}
}

View File

@ -30,7 +30,7 @@ export enum ErrorCodes {
Refused Refused
} }
export enum QueryTypes { export enum RecordTypes {
/** /**
* IPv4 address * IPv4 address
*/ */
@ -254,14 +254,13 @@ export interface IMessageQuestion {
/** /**
* Two octed code that specifies the class of the Query * Two octed code that specifies the class of the Query
* IS for internet * IN for internet
* WARNING: ONLY IN IS SUPPORTED BY THIS APPLICATION
*/ */
QCLASS: number; QCLASS: number;
} }
export interface MessageRecourceRecord { export interface MessageRecourceRecord {
/** /**
* Domain name to wich resource record pertains * Domain name to wich resource record points
*/ */
NAME: string; NAME: string;
@ -297,7 +296,7 @@ export interface MessageRecourceRecord {
RDLENGTH: number; RDLENGTH: number;
/** /**
* a variable length string of ectets taht describes * a variable length string of octets that describes
* the resource. The format is defined by TYPE and CLASS * the resource. The format is defined by TYPE and CLASS
* field. * field.
* *
@ -306,10 +305,3 @@ export interface MessageRecourceRecord {
*/ */
RDATA: Buffer; RDATA: Buffer;
} }
export interface IMessage {
header: IMessageHeader;
questions: IMessageQuestion[];
answers: MessageRecourceRecord[];
authorities: MessageRecourceRecord[];
additionals: MessageRecourceRecord[];
}

1661
yarn.lock

File diff suppressed because it is too large Load Diff