├── .gitignore ├── mod.ts ├── src ├── records │ ├── types.ts │ ├── mod.ts │ ├── txt.ts │ ├── ns.ts │ ├── cname.ts │ ├── a.ts │ ├── aaaa.ts │ ├── srv.ts │ ├── mx.ts │ └── soa.ts ├── errors.ts ├── validators.ts ├── server.ts ├── query.ts └── protocol.ts ├── deps.ts ├── .github └── workflows │ └── test.yml ├── README.md ├── LICENSE └── test.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./src/records/mod.ts"; 2 | export * from "./src/errors.ts"; 3 | export * from "./src/protocol.ts"; 4 | export * from "./src/query.ts"; 5 | export * from "./src/server.ts"; 6 | export * from "./src/validators.ts"; 7 | -------------------------------------------------------------------------------- /src/records/types.ts: -------------------------------------------------------------------------------- 1 | export type DNSRecordType = 2 | | "A" 3 | | "AAAA" 4 | | "CNAME" 5 | | "MX" 6 | | "NS" 7 | | "SOA" 8 | | "SRV" 9 | | "TXT"; 10 | 11 | export interface IRecord { 12 | type: DNSRecordType; 13 | target: any; 14 | } 15 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/x/event@2.0.0/mod.ts"; 2 | export { 3 | isValid, 4 | process, 5 | parse, 6 | IPv4, 7 | IPv6, 8 | parseCIDR, 9 | } from "https://esm.sh/ipaddr.js"; 10 | export { Buffer } from "https://deno.land/std@0.137.0/node/buffer.ts"; 11 | -------------------------------------------------------------------------------- /src/records/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./a.ts"; 2 | export * from "./aaaa.ts"; 3 | export * from "./cname.ts"; 4 | export * from "./mx.ts"; 5 | export * from "./ns.ts"; 6 | export * from "./soa.ts"; 7 | export * from "./srv.ts"; 8 | export * from "./txt.ts"; 9 | export * from "./types.ts"; 10 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export enum DNSErrorCodes { 2 | NO_ERROR, 3 | PROTOCOL_ERROR, 4 | CANNOT_PROCESS, 5 | NO_NAME, 6 | NOT_IMPLEMENTED, 7 | REFUSED, 8 | EXCEPTION, 9 | } 10 | 11 | export class DNSError extends Error { 12 | constructor(msg: string, public code: DNSErrorCodes) { 13 | super(msg); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: denolib/setup-deno@v2 9 | with: 10 | deno-version: v1.x 11 | - name: Run Tests 12 | run: deno test --unstable --allow-net test.ts -------------------------------------------------------------------------------- /src/records/txt.ts: -------------------------------------------------------------------------------- 1 | import { validateNsText } from "../validators.ts"; 2 | import { DNSRecordType, IRecord } from "./types.ts"; 3 | 4 | export class TXTRecord implements IRecord { 5 | type: DNSRecordType = "TXT"; 6 | target: string; 7 | 8 | constructor(target: string) { 9 | if (!validateNsText(target)) throw new Error("Invalid TXT Record Value"); 10 | this.target = target; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/records/ns.ts: -------------------------------------------------------------------------------- 1 | import { validateNsName } from "../validators.ts"; 2 | import { DNSRecordType, IRecord } from "./types.ts"; 3 | 4 | export class NSRecord implements IRecord { 5 | type: DNSRecordType = "NS"; 6 | target: string; 7 | 8 | constructor(target: string) { 9 | if (!validateNsName(target)) 10 | throw new Error("Invalid Target value for NS Record"); 11 | this.target = target; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/records/cname.ts: -------------------------------------------------------------------------------- 1 | import { DNSRecordType, IRecord } from "./types.ts"; 2 | import {validateNsName} from "../validators.ts"; 3 | 4 | export class CNAMERecord implements IRecord { 5 | type: DNSRecordType = "CNAME"; 6 | target: string; 7 | 8 | constructor(fqdn: string) { 9 | if(typeof fqdn !== "string" || !validateNsName(fqdn)) 10 | throw new Error("Expected valid FQDN for CNAME Record"); 11 | this.target = fqdn; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/records/a.ts: -------------------------------------------------------------------------------- 1 | import { IPv4 } from "../../deps.ts"; 2 | import { DNSRecordType, IRecord } from "./types.ts"; 3 | 4 | export class ARecord implements IRecord { 5 | type: DNSRecordType = "A"; 6 | target: string; 7 | 8 | constructor(ip: string | IPv4) { 9 | if (typeof ip === "object" && ip instanceof IPv4) 10 | this.target = ip.toString(); 11 | else { 12 | if (!IPv4.isIPv4(ip)) 13 | throw new Error("Expected valid IPv4 Arress for A Record"); 14 | this.target = ip; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/records/aaaa.ts: -------------------------------------------------------------------------------- 1 | import { IPv6 } from "../../deps.ts"; 2 | import { DNSRecordType, IRecord } from "./types.ts"; 3 | 4 | export class AAAARecord implements IRecord { 5 | type: DNSRecordType = "AAAA"; 6 | target: string; 7 | 8 | constructor(ip: string | IPv6) { 9 | if (typeof ip === "object" && ip instanceof IPv6) 10 | this.target = ip.toString(); 11 | else { 12 | if (!IPv6.isIPv6(ip)) 13 | throw new Error("Expected valid IPv6 Arress for AAAA Record"); 14 | this.target = ip; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dename 2 | 3 | DNS Server framework for Deno. Port of [node-named](https://github.com/trevoro/node-named). 4 | 5 | ## Usage 6 | 7 | ```ts 8 | import { DNSServer, CNAMERecord } from "https://deno.land/x/dename/mod.ts"; 9 | 10 | const server = new DNSServer({ 11 | "domain": new CNAMERecord("points-to.this"), 12 | }); 13 | 14 | server.on("listen", () => { 15 | console.log("Listening ~"); 16 | }); 17 | 18 | server.listen({ port: 6969 }); 19 | ``` 20 | 21 | ## Contributing 22 | 23 | You're always welcome to contribute! We use `deno fmt` to format the code. 24 | 25 | ## License 26 | 27 | Check [LICENSE](LICENSE) for more info. 28 | 29 | Copyright 2021 @ DjDeveloperr -------------------------------------------------------------------------------- /src/validators.ts: -------------------------------------------------------------------------------- 1 | export const NS_NAME_REGEX = /^([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])(\.([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9]))*$/i; 2 | 3 | export function validateNsName(value: string) { 4 | if (typeof value !== "string") return false; 5 | if (value.length > 255) return false; 6 | return NS_NAME_REGEX.test(value); 7 | } 8 | 9 | export function validateUint32BE(value: number | string) { 10 | if (typeof value === "string") value = parseInt(value); 11 | if (typeof value !== "number") return false; 12 | return !isNaN(value) && value < 4294967295; 13 | } 14 | 15 | export function validateUint16BE(value: number | string) { 16 | if (typeof value === "string") value = parseInt(value); 17 | if (typeof value !== "number") return false; 18 | return !isNaN(value) && value < 65535; 19 | } 20 | 21 | export function validateNsText(value: string) { 22 | if (typeof value !== "string") return false; 23 | return value.length < 256; 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 DjDeveloperr 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/records/srv.ts: -------------------------------------------------------------------------------- 1 | import { validateNsText, validateUint16BE } from "../validators.ts"; 2 | import { DNSRecordType, IRecord } from "./types.ts"; 3 | 4 | export interface SRVRecordOptions { 5 | host: string; 6 | port: number; 7 | priority?: number; 8 | weight?: number; 9 | } 10 | 11 | export class SRVRecord implements IRecord { 12 | type: DNSRecordType = "SRV"; 13 | target: string; 14 | port: number; 15 | priority: number = 0; 16 | weight: number = 10; 17 | 18 | constructor(options: SRVRecordOptions) { 19 | if (!validateNsText(options.host)) 20 | throw new Error("Invalid host name for SRV Record"); 21 | if (!validateUint16BE(options.port)) 22 | throw new Error("Invalid port for SRV Record"); 23 | const weight = options.weight ?? this.weight; 24 | if (!validateUint16BE(weight)) 25 | throw new Error("Invalid weight value for SRV Record"); 26 | const priority = options.priority ?? this.priority; 27 | if (!validateUint16BE(priority)) 28 | throw new Error("Invalid priority value for SRV Record"); 29 | 30 | this.target = options.host; 31 | this.port = options.port; 32 | this.weight = weight; 33 | this.priority = priority; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/records/mx.ts: -------------------------------------------------------------------------------- 1 | import { 2 | validateNsName, 3 | validateUint16BE, 4 | validateUint32BE, 5 | } from "../validators.ts"; 6 | import { DNSRecordType, IRecord } from "./types.ts"; 7 | 8 | export interface MXRecordOptions { 9 | exchange: string; 10 | priority?: number; 11 | ttl?: number; 12 | } 13 | 14 | export class MXRecord implements IRecord { 15 | type: DNSRecordType = "MX"; 16 | target: void; 17 | exchange: string; 18 | priority: number = 0; 19 | ttl: number = 600; 20 | 21 | constructor(options: MXRecordOptions) { 22 | if (typeof options.exchange !== "string") 23 | throw new Error("Expected Exchange value for MX Record"); 24 | 25 | if (!validateNsName(options.exchange)) 26 | throw new Error("Invalid Exchange value for MX Record"); 27 | this.exchange = options.exchange; 28 | 29 | if (typeof options.priority === "number") { 30 | if (!validateUint16BE(options.priority)) 31 | throw new Error("Invalid Priority value, expected Uint16BE"); 32 | this.priority = options.priority; 33 | } 34 | if (typeof options.ttl === "number") { 35 | if (!validateUint32BE(options.ttl)) 36 | throw new Error("Invalid TTL value, expected Uint32BE"); 37 | this.ttl = options.ttl; 38 | } 39 | 40 | this.target = undefined; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/records/soa.ts: -------------------------------------------------------------------------------- 1 | import { validateNsName, validateUint32BE } from "../validators.ts"; 2 | import { DNSRecordType, IRecord } from "./types.ts"; 3 | 4 | export interface SOARecordOptions { 5 | host: string; 6 | admin?: string; 7 | serial?: number; 8 | retry?: number; 9 | expire?: number; 10 | ttl?: number; 11 | refresh?: number; 12 | } 13 | 14 | export class SOARecord implements IRecord { 15 | type: DNSRecordType = "SOA"; 16 | target: void; 17 | host: string; 18 | admin: string; 19 | serial: number; 20 | retry: number; 21 | expire: number; 22 | ttl: number; 23 | refresh: number; 24 | 25 | constructor(options: SOARecordOptions) { 26 | if (!validateNsName(options.host)) 27 | throw new Error("Invalid host name for SOA Record"); 28 | 29 | const admin = options.admin ?? "hostmaster." + options.host; 30 | if (!validateNsName(admin)) throw new Error("Invalid Admin for SOA Record"); 31 | 32 | const serial = options.serial ?? 0; 33 | const refresh = options.refresh ?? 10; 34 | const retry = options.retry ?? 10; 35 | const expire = options.expire ?? 10; 36 | const ttl = options.ttl ?? 10; 37 | 38 | if (!validateUint32BE(serial)) 39 | throw new Error("Invalid serial for SOA Record"); 40 | if (!validateUint32BE(refresh)) 41 | throw new Error("Invalid refresh value for SOA Record"); 42 | if (!validateUint32BE(retry)) 43 | throw new Error("Invalid retry value for SOA Record"); 44 | if (!validateUint32BE(expire)) 45 | throw new Error("Invalid expire value for SOA Record"); 46 | if (!validateUint32BE(ttl)) 47 | throw new Error("Invalid ttl value for SOA Record"); 48 | 49 | this.host = options.host; 50 | this.admin = admin; 51 | this.serial = serial; 52 | this.refresh = refresh; 53 | this.retry = retry; 54 | this.expire = expire; 55 | this.ttl = ttl; 56 | this.target = undefined; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AAAARecord, 3 | DNSServer, 4 | MXRecord, 5 | NSRecord, 6 | SOARecord, 7 | ARecord, 8 | CNAMERecord, 9 | TXTRecord, 10 | SRVRecord, 11 | } from "./mod.ts"; 12 | import { assertEquals } from "https://deno.land/std@0.90.0/testing/asserts.ts"; 13 | 14 | const server = new DNSServer({ 15 | txt: new TXTRecord("txt"), 16 | cname: new CNAMERecord("google.com"), 17 | a: new ARecord("127.0.0.1"), 18 | aaaa: new AAAARecord("2001:6db8:10b8:20b8:30b8:40b8:3257:9652"), 19 | mx: new MXRecord({ 20 | exchange: "mail.example.com", 21 | ttl: 1936, 22 | }), 23 | ns: new NSRecord("ns.example.com"), 24 | soa: new SOARecord({ 25 | host: "soa.example.com", 26 | }), 27 | srv: new SRVRecord({ 28 | host: "voip.example.com", 29 | port: 6969, 30 | }), 31 | }); 32 | 33 | server.on("listen", () => { 34 | console.log("Listening!"); 35 | }); 36 | 37 | server.listen({ port: 6969 }); 38 | 39 | const nameServer = { 40 | ipAddr: "127.0.0.1", 41 | port: 6969, 42 | }; 43 | 44 | const options: Deno.ResolveDnsOptions = { nameServer }; 45 | 46 | Deno.test({ 47 | name: "TXT Record", 48 | async fn() { 49 | const res = await Deno.resolveDns("txt", "TXT", options); 50 | assertEquals(res.length, 1); 51 | assertEquals(res[0].length, 1); 52 | assertEquals(res[0][0], "txt"); 53 | }, 54 | }); 55 | 56 | Deno.test({ 57 | name: "A Record", 58 | async fn() { 59 | const res = await Deno.resolveDns("a", "A", options); 60 | assertEquals(res.length, 1); 61 | assertEquals(res[0], "127.0.0.1"); 62 | }, 63 | }); 64 | 65 | Deno.test({ 66 | name: "AAAA Record", 67 | async fn() { 68 | const res = await Deno.resolveDns("aaaa", "AAAA", options); 69 | assertEquals(res.length, 1); 70 | assertEquals(res[0], "2001:6db8:10b8:20b8:30b8:40b8:3257:9652"); 71 | }, 72 | }); 73 | 74 | Deno.test({ 75 | name: "MX Record", 76 | async fn() { 77 | const res = await Deno.resolveDns("mx", "MX", options); 78 | assertEquals(res.length, 1); 79 | assertEquals(res[0].exchange, "mail.example.com."); 80 | }, 81 | }); 82 | 83 | Deno.test({ 84 | name: "CNAME Record", 85 | async fn() { 86 | const res = await Deno.resolveDns("cname", "CNAME", options); 87 | assertEquals(res.length, 1); 88 | assertEquals(res[0], "google.com."); 89 | }, 90 | }); 91 | 92 | Deno.test({ 93 | name: "SRV Record", 94 | async fn() { 95 | const res = await Deno.resolveDns("srv", "SRV", options); 96 | assertEquals(res.length, 1); 97 | assertEquals(res[0].target, "voip.example.com."); 98 | assertEquals(res[0].port, 6969); 99 | }, 100 | }); 101 | 102 | Deno.test({ 103 | name: "Clean Up", 104 | sanitizeResources: false, 105 | sanitizeOps: false, 106 | fn() { 107 | server.close(); 108 | }, 109 | }); 110 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { Buffer, EventEmitter } from "../deps.ts"; 2 | import { DNSQuery } from "./query.ts"; 3 | import { IRecord } from "./records/types.ts"; 4 | 5 | export type RecordWithTTL = { record: IRecord; ttl?: number }; 6 | export type SavedRecord = { 7 | [name: string]: IRecord | IRecord[] | RecordWithTTL | RecordWithTTL[]; 8 | }; 9 | 10 | export type DNSServerEvents = { 11 | listen: []; 12 | clientError: [Error]; 13 | error: [Error]; 14 | close: []; 15 | query: [DNSQuery]; 16 | uncaughtException: [Error]; 17 | }; 18 | 19 | export interface ListenOptions { 20 | port: number; 21 | address?: string; 22 | } 23 | 24 | export class DNSServer extends EventEmitter { 25 | socket?: Deno.DatagramConn; 26 | records: { [name: string]: RecordWithTTL[] } = {}; 27 | 28 | constructor(records: SavedRecord = {}) { 29 | super(); 30 | Object.entries(records).forEach(([name, rec]) => { 31 | if (Array.isArray(rec)) { 32 | rec.forEach((e: IRecord | RecordWithTTL) => this.register(name, e)); 33 | } else { 34 | this.register(name, rec); 35 | } 36 | }); 37 | 38 | this.on("query", async (query) => { 39 | try { 40 | if (this.records[query.name] !== undefined) { 41 | const res = this.records[query.name]; 42 | res.forEach((e) => query.answer(e.record, e.ttl)); 43 | await query.respond(); 44 | } 45 | } catch (e) { 46 | this.emit("error", e); 47 | } 48 | }); 49 | } 50 | 51 | register(name: string, record: IRecord | RecordWithTTL) { 52 | let records = this.records[name] ?? []; 53 | this.records[name] = records; 54 | if ((record as RecordWithTTL).record !== undefined) { 55 | records.push(record as RecordWithTTL); 56 | } else { 57 | records.push({ record: record as IRecord }); 58 | } 59 | return this; 60 | } 61 | 62 | close() { 63 | this.socket?.close(); 64 | } 65 | 66 | async listen(options: ListenOptions) { 67 | this.socket = Deno.listenDatagram({ 68 | port: options.port, 69 | transport: "udp", 70 | hostname: options.address ?? "0.0.0.0", 71 | }); 72 | 73 | this.emit("listen"); 74 | try { 75 | for await (const [_buf, addr] of this.socket) { 76 | const buffer = new Buffer(_buf); 77 | let query: DNSQuery; 78 | 79 | try { 80 | query = DNSQuery.create(DNSQuery.parseRaw(buffer, addr)); 81 | } catch (e) { 82 | this.emit("clientError", new Error(`Invalid DNS Datagram`)); 83 | continue; 84 | } 85 | 86 | if (query === undefined || query === null) { 87 | continue; 88 | } 89 | 90 | const self = this; 91 | query._respond = async function () { 92 | await self.send(query); 93 | }; 94 | 95 | this.emit("query", query); 96 | } 97 | } catch (e) { 98 | this.emit("error", e); 99 | } 100 | 101 | this.emit("close"); 102 | } 103 | 104 | async send(res: DNSQuery) { 105 | if (!this.socket) throw new Error("Not listening"); 106 | 107 | try { 108 | res._flags.qr = 1; 109 | res.encode(); 110 | } catch (e) { 111 | this.emit("uncaughtException", new Error("Unable to encode response")); 112 | return; 113 | } 114 | 115 | const buf = res._raw!; 116 | 117 | await this.socket.send(buf, res._client!); 118 | return this; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/query.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from "../deps.ts"; 2 | import { decode, encode, QueryType } from "./protocol.ts"; 3 | import { IRecord } from "./records/types.ts"; 4 | 5 | function define(obj: any, prop: string, value: any) { 6 | Object.defineProperty(obj, prop, { 7 | value, 8 | enumerable: false, 9 | }); 10 | } 11 | 12 | export interface Answer { 13 | name: string; 14 | rtype: number; 15 | rttl: number; 16 | rdata: IRecord; 17 | } 18 | 19 | /** Represents an DNS Query received by the server */ 20 | export class DNSQuery { 21 | id: string; 22 | name: string; 23 | type: string; 24 | truncated?: boolean; 25 | authoritative?: boolean; 26 | recursionAvailable?: boolean; 27 | _responseCode = 0; 28 | _qdCount!: number; 29 | _anCount!: number; 30 | _nsCount!: number; 31 | _srCount!: number; 32 | _flags: { [name: string]: number } = {}; 33 | _question: any; 34 | _answers: Answer[] = []; 35 | _raw: Buffer | null = null; 36 | _client: Deno.NetAddr | null = null; 37 | _authority: any; 38 | _additional: any; 39 | _respond!: CallableFunction; 40 | 41 | constructor(data: any) { 42 | this.id = data.id; 43 | 44 | define(this, "_question", data.question); 45 | define(this, "_flags", data.flags); 46 | define(this, "_qdCount", data.qdCount); 47 | this._anCount = data.anCount ?? 0; 48 | define(this, "_nsCount", data.nsCount ?? 0); 49 | define(this, "_srCount", data.srCount ?? 0); 50 | 51 | this.name = data.question.name; 52 | this.type = QueryType[data.question.type]; 53 | } 54 | 55 | get answers() { 56 | return this._answers.map((e) => ({ 57 | name: e.name, 58 | type: QueryType[e.rtype], 59 | record: e.rdata, 60 | ttl: e.rttl, 61 | })); 62 | } 63 | 64 | get operation() { 65 | switch (this._flags.opcode) { 66 | case 0: 67 | return "query"; 68 | case 2: 69 | return "status"; 70 | case 4: 71 | return "notify"; 72 | case 5: 73 | return "update"; 74 | default: 75 | throw new Error("Invalid Operation ID: " + this._flags.opcode); 76 | } 77 | } 78 | 79 | encode() { 80 | const encoded = encode( 81 | { 82 | header: { 83 | id: this.id, 84 | flags: this._flags, 85 | qdCount: this._qdCount, 86 | anCount: this._anCount, 87 | nsCount: this._nsCount, 88 | srCount: this._srCount, 89 | }, 90 | question: this._question, 91 | answers: this._answers, 92 | authority: this._authority, 93 | additional: this._additional, 94 | }, 95 | "answerMessage" 96 | ); 97 | this._raw = encoded; 98 | return this; 99 | } 100 | 101 | answer(record: IRecord | IRecord[], ttl?: number, name?: string) { 102 | if (Array.isArray(record)) { 103 | record.forEach((r) => { 104 | this.answer(r, ttl, name); 105 | }); 106 | return this; 107 | } 108 | if (!name) name = this._question.name; 109 | if (typeof name !== "string") throw new Error("name must be string"); 110 | if (typeof record !== "object") throw new Error("record must be IRecord"); 111 | if (ttl !== undefined && typeof ttl !== "number") 112 | throw new Error("ttl must be number"); 113 | 114 | if (typeof QueryType[record.type] !== "number") 115 | throw new Error("Unknown Record Type: " + record.type); 116 | 117 | const answer = { 118 | name, 119 | rtype: QueryType[record.type], 120 | rclass: 1, 121 | rttl: ttl ?? 5, 122 | rdata: record, 123 | }; 124 | 125 | this._answers.push(answer); 126 | this._anCount++; 127 | return this; 128 | } 129 | 130 | async respond() { 131 | await this._respond(); 132 | return this; 133 | } 134 | 135 | static parseRaw(raw: any, src: any) { 136 | let dobj, 137 | b = raw; 138 | dobj = decode(b, "queryMessage", 0); 139 | if (!dobj.val) return null; 140 | const d = dobj.val; 141 | 142 | return { 143 | id: d.header.id, 144 | flags: d.header.flags, 145 | qdCount: d.header.qdCount, 146 | anCount: d.header.anCount, 147 | nsCount: d.header.nsCount, 148 | srCount: d.header.srCount, 149 | question: d.question, 150 | src, 151 | raw, 152 | }; 153 | } 154 | 155 | static create(req: any) { 156 | const query = new DNSQuery(req); 157 | query._raw = req._raw; 158 | query._client = req.src; 159 | return query; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/protocol.ts: -------------------------------------------------------------------------------- 1 | import { IPv6, Buffer } from "../deps.ts"; 2 | 3 | export enum Constants { 4 | QCLASS_IN = 0x01, // the internet 5 | QCLASS_CS = 0x02, // obsolete 6 | QCLASS_CH = 0x03, // chaos class. yes this actually exists 7 | QCLASS_HS = 0x04, // Hesiod 8 | DNS_ENOERR = 0x00, // No error 9 | DNS_EFORMAT = 0x01, // Formatting Error 10 | DNS_ESERVER = 0x02, // server it unable to process 11 | DNS_ENONAME = 0x03, // name does not exist 12 | DNS_ENOTIMP = 0x04, // feature not implemented on this server 13 | DNS_EREFUSE = 0x05, // refused for policy reasons 14 | } 15 | 16 | export const Formats: { 17 | [name: string]: { 18 | [name: string]: { type: string | { format: string } }; 19 | }; 20 | } = { 21 | answer: { 22 | name: { type: "_nsName" }, 23 | rtype: { type: "UInt16BE" }, 24 | rclass: { type: "UInt16BE" }, 25 | rttl: { type: "UInt32BE" }, 26 | rdata: { type: "_nsData" }, 27 | }, 28 | question: { 29 | name: { type: "_nsName" }, 30 | type: { type: "UInt16BE" }, 31 | qclass: { type: "UInt16BE" }, 32 | }, 33 | header: { 34 | id: { type: "UInt16BE" }, 35 | flags: { type: "_nsFlags" }, 36 | qdCount: { type: "UInt16BE" }, 37 | anCount: { type: "UInt16BE" }, 38 | nsCount: { type: "UInt16BE" }, 39 | srCount: { type: "UInt16BE" }, 40 | }, 41 | soa: { 42 | host: { type: "_nsName" }, 43 | admin: { type: "_nsName" }, 44 | serial: { type: "UInt32BE" }, 45 | refresh: { type: "UInt32BE" }, 46 | retry: { type: "UInt32BE" }, 47 | expire: { type: "UInt32BE" }, 48 | ttl: { type: "UInt32BE" }, 49 | }, 50 | mx: { 51 | priority: { type: "UInt16BE" }, 52 | exchange: { type: "_nsName" }, 53 | }, 54 | txt: { 55 | text: { type: "_nsText" }, 56 | }, 57 | srv: { 58 | priority: { type: "UInt16BE" }, 59 | weight: { type: "UInt16BE" }, 60 | port: { type: "UInt16BE" }, 61 | target: { type: "_nsName" }, 62 | }, 63 | queryMessage: { 64 | header: { type: { format: "header" } }, 65 | question: { type: { format: "question" } }, 66 | }, 67 | answerMessage: { 68 | header: { type: { format: "header" } }, 69 | question: { type: { format: "question" } }, 70 | answers: { type: "_nsAnswers" }, 71 | }, 72 | }; 73 | 74 | export function convertIPv4ToUint(addr: string) { 75 | if (typeof addr !== "string") throw new Error("addr must be string"); 76 | const octets = addr.split(/\./).map((octet) => parseInt(octet, 10)); 77 | if (octets.length !== 4 || octets.some((e) => isNaN(e))) 78 | throw new Error("Invalid addr"); 79 | 80 | return ( 81 | octets[0] * Math.pow(256, 3) + 82 | octets[1] * Math.pow(256, 2) + 83 | octets[2] * 256 + 84 | octets[3] 85 | ); 86 | } 87 | 88 | export function convertIPv6ToArray(addr: string) { 89 | if (typeof addr !== "string") throw new Error("addr must be string"); 90 | let res; 91 | try { 92 | res = IPv6.parse(addr); 93 | } catch (e) { 94 | return null; 95 | } 96 | return res.parts; 97 | } 98 | 99 | const Serializers = { 100 | UInt32BE: { 101 | encoder(v: number) { 102 | const b = new Buffer(4); 103 | b.writeUInt32BE(v, 0); 104 | return b; 105 | }, 106 | decoder(v: Buffer, p: number) { 107 | return v.readUInt32BE(p); 108 | }, 109 | }, 110 | UInt16BE: { 111 | encoder(v: number) { 112 | const b = new Buffer(2); 113 | b.writeUInt16BE(v, 0); 114 | return b; 115 | }, 116 | decoder(v: Buffer, p: number) { 117 | const res = v.readUInt16BE(p); 118 | return { val: res, len: 2 }; 119 | }, 120 | }, 121 | _nsAnswers: { 122 | encoder(v: Buffer) { 123 | let s = 0, 124 | p = 0, 125 | answers = []; 126 | for (const i in v) { 127 | let r = encode(v[i], "answer"); 128 | answers.push(r); 129 | s = s + r.length; 130 | } 131 | const b = new Buffer(s); 132 | for (const n in answers) { 133 | answers[n].copy(b, p); 134 | p = p + answers[n].length; 135 | } 136 | return b; 137 | }, 138 | }, 139 | _nsFlags: { 140 | encoder: function (v: any) { 141 | if (typeof v !== "object") { 142 | throw new TypeError("flag must be an object"); 143 | } 144 | let b = new Buffer(2); 145 | let f = 0x0000; 146 | f = f | (v.qr << 15); 147 | f = f | (v.opcode << 11); 148 | f = f | (v.aa << 10); 149 | f = f | (v.tc << 9); 150 | f = f | (v.rd << 8); 151 | f = f | (v.ra << 7); 152 | f = f | (v.z << 6); 153 | f = f | (v.ad << 5); 154 | f = f | (v.cd << 4); 155 | f = f | v.rcode; 156 | b.writeUInt16BE(f, 0); 157 | return b; 158 | }, 159 | decoder(v: Buffer, p: number) { 160 | let flags, f; 161 | flags = v.readUInt16BE(p); 162 | f = { 163 | qr: flags & 0x8000 ? true : false, 164 | opcode: flags & 0x7800, 165 | aa: flags & 0x0400 ? true : false, 166 | tc: flags & 0x0200 ? true : false, 167 | rd: flags & 0x0100 ? true : false, 168 | ra: flags & 0x0080 ? true : false, 169 | z: flags & 0x0040 ? true : false, 170 | ad: flags & 0x0020 ? true : false, 171 | cd: flags & 0x0010 ? true : false, 172 | rcode: flags & 0x000f, 173 | }; 174 | return { val: f, len: 2 }; 175 | }, 176 | }, 177 | _nsIP4: { 178 | encoder(v: string) { 179 | let a, b; 180 | a = convertIPv4ToUint(v); 181 | b = new Buffer(4); 182 | b.writeUInt32BE(a, 0); 183 | return b; 184 | }, 185 | }, 186 | _nsIP6: { 187 | encoder(v: string) { 188 | let a, 189 | b, 190 | i = 0; 191 | a = convertIPv6ToArray(v); 192 | b = new Buffer(16); 193 | for (let i = 0; i < 8; i++) { 194 | b.writeUInt16BE(a![i], i * 2); 195 | } 196 | return b; 197 | }, 198 | }, 199 | _nsName: { 200 | encoder(v: string) { 201 | if (typeof v !== "string") 202 | throw new TypeError("name (string) is required"); 203 | let n = v.split(/\./); 204 | 205 | let b = new Buffer(n.toString().length + 2); 206 | let o = 0; // offset 207 | 208 | for (let i = 0; i < n.length; i++) { 209 | let l = n[i].length; 210 | b[o] = l; 211 | b.write(n[i], ++o, l); 212 | o += l; 213 | } 214 | b[o] = 0x00; 215 | 216 | return b; 217 | }, 218 | decoder(v: Buffer, p: number) { 219 | let rlen, 220 | start = p, 221 | name = []; 222 | 223 | rlen = v.readUInt8(p); 224 | while (rlen != 0x00) { 225 | p++; 226 | let t = v.slice(p, p + rlen); 227 | name.push(t.toString()); 228 | p = p + rlen; 229 | rlen = v.readUInt8(p); 230 | } 231 | 232 | return { val: name.join("."), len: p - start + 1 }; 233 | }, 234 | }, 235 | _nsText: { 236 | encoder(v: string) { 237 | let b; 238 | b = new Buffer(v.length + 1); 239 | b.writeUInt8(v.length, 0); 240 | b.write(v, 1); 241 | return b; 242 | }, 243 | }, 244 | _nsData: { 245 | encoder(v: any, t: number) { 246 | let r, b, l; 247 | 248 | switch (t) { 249 | case QueryType.A: 250 | r = Serializers["_nsIP4"].encoder(v.target); 251 | break; 252 | case QueryType.CNAME: 253 | r = Serializers["_nsName"].encoder(v.target); 254 | break; 255 | case QueryType.NS: 256 | r = Serializers["_nsName"].encoder(v.target); 257 | break; 258 | case QueryType.SOA: 259 | r = encode(v, "soa"); 260 | break; 261 | case QueryType.MX: 262 | r = encode(v, "mx"); 263 | break; 264 | case QueryType.TXT: 265 | r = Serializers["_nsText"].encoder(v.target); 266 | break; 267 | case QueryType.AAAA: 268 | r = Serializers["_nsIP6"].encoder(v.target); 269 | break; 270 | case QueryType.SRV: 271 | r = encode(v, "srv"); 272 | break; 273 | default: 274 | throw new Error("unrecognized nsdata type"); 275 | } 276 | 277 | l = r.length; 278 | b = new Buffer(l + 2); 279 | b.writeUInt16BE(l, 0); 280 | r.copy(b, 2); 281 | return b; 282 | }, 283 | }, 284 | }; 285 | 286 | export enum QueryType { 287 | A = 0x01, 288 | NS = 0x02, 289 | MD = 0x03, 290 | MF = 0x04, 291 | CNAME = 0x05, 292 | SOA = 0x06, 293 | MB = 0x07, 294 | MG = 0x08, 295 | MR = 0x09, 296 | NULL = 0x0a, 297 | WKS = 0x0b, 298 | PTR = 0x0c, 299 | HINFO = 0x0d, 300 | MINFO = 0x0e, 301 | MX = 0x0f, 302 | TXT = 0x10, 303 | AAAA = 0x1c, 304 | SRV = 0x21, 305 | AXFR = 0xfc, 306 | MAILA = 0xfe, 307 | MAILB = 0xfd, 308 | ANY = 0xff, 309 | } 310 | 311 | export function encode(obj: any, format: string) { 312 | let size = 0, 313 | pos = 0, 314 | fmt, 315 | result, 316 | results = []; 317 | 318 | fmt = Formats[format]; 319 | 320 | for (const f in fmt) { 321 | let type, res; 322 | type = fmt[f].type; 323 | 324 | if (typeof type === "string") { 325 | if (type == "_nsData") { 326 | res = Serializers["_nsData"].encoder(obj[f], obj["rtype"]); 327 | } else { 328 | res = (Serializers as any)[type].encoder(obj[f]); 329 | } 330 | } else if (typeof type === "object") { 331 | const refType = type.format; 332 | res = encode(obj[f], refType); 333 | } else { 334 | throw new TypeError("invalid type"); 335 | } 336 | 337 | results.push(res); 338 | size = size + res.length; 339 | } 340 | 341 | result = new Buffer(size); 342 | 343 | for (const i in results) { 344 | let buf = results[i]; 345 | buf.copy(result, pos); 346 | pos = pos + buf.length; 347 | } 348 | 349 | return result; 350 | } 351 | 352 | export function decode(raw: string, format: string, pos: number) { 353 | let fmt, 354 | result: any = {}; 355 | 356 | if (!pos) pos = 0; 357 | fmt = Formats[format]; 358 | 359 | for (let f in fmt) { 360 | let type, res; 361 | type = fmt[f].type; 362 | 363 | if (typeof type === "string") { 364 | res = (Serializers as any)[type].decoder(raw, pos); 365 | } else if (typeof type === "object") { 366 | const reftype = type.format; 367 | res = decode(raw, reftype, pos); 368 | } else { 369 | throw new TypeError("invalid type"); 370 | } 371 | 372 | pos += res.len; 373 | result[f] = res.val; 374 | } 375 | 376 | return { val: result, len: pos }; 377 | } 378 | 379 | export const DNS_ENOERR = 0x00, 380 | DNS_EFORMAT = 0x01, 381 | DNS_ESERVER = 0x02, 382 | DNS_ENONAME = 0x03, 383 | DNS_ENOTIMP = 0x04, 384 | DNS_EREFUSE = 0x05; 385 | --------------------------------------------------------------------------------