diff --git a/dns-packet-tests.ts b/dns-packet-tests.ts new file mode 100644 index 0000000..2c15a64 --- /dev/null +++ b/dns-packet-tests.ts @@ -0,0 +1,277 @@ +import { + Answer, + decode, + DecodedPacket, + encode, + encodingLength, + Packet, + Question, + RECURSION_DESIRED, + streamDecode, + streamEncode, +} from "dns-packet"; + +const answer: Answer = { + type: "A", + name: "localhost", + ttl: 3600, + data: "127.0.0.1", + class: "ANY", + flush: true, +}; + +const question: Question = { + type: "A", + name: "localhost", + class: "IN", +}; + +const inPacket: Packet = { + additionals: [answer], + authorities: [answer], + answers: [answer], + flags: 0, + id: 0, + questions: [question], + type: "query", +}; + +const inputBuf = Buffer.alloc(0); +const length: number = encodingLength(inPacket); +const out: Buffer = encode(inPacket, inputBuf, length - length); +const outPacket: DecodedPacket = decode(out, 0); +const flag_qr: boolean = outPacket.flag_qr; +const flag_aa: boolean = outPacket.flag_aa; +const flag_tc: boolean = outPacket.flag_tc; +const flag_rd: boolean = outPacket.flag_rd; +const flag_ra: boolean = outPacket.flag_ra; +const flag_z: boolean = outPacket.flag_z; +const flag_ad: boolean = outPacket.flag_ad; +const flag_cd: boolean = outPacket.flag_cd; + +encode(outPacket); + +const records: Answer[] = [ + { + type: "A", + name: "localhost", + data: "127.0.0.1", + }, + { + type: "AAAA", + name: "localhost", + data: "::1", + }, + { + type: "CNAME", + name: "localhost", + data: "example.com", + }, + { + type: "DNSKEY", + name: "localhost", + data: { + algorithm: 1, + flags: 257, + key: Buffer.from("test"), + }, + }, + { + type: "DS", + name: "localhost", + data: { + keyTag: 12345, + algorithm: 8, + digestType: 1, + digest: Buffer.from("test"), + }, + }, + { + type: "NAPTR", + name: "localhost", + data: { + order: 100, + preference: 10, + flags: "s", + services: "SIP+D2U", + regexp: "!^.*$!sip:customer-service@example.com!", + replacement: "_sip._udp.example.com", + }, + }, + { + type: "NS", + name: "localhost", + data: "ns1.localhost", + }, + { + type: "NSEC", + name: "localhost", + data: { + nextDomain: "a.domain", + rrtypes: ["A", "TXT", "RRSIG"], + }, + }, + { + type: "NSEC3", + name: "localhost", + data: { + algorithm: 1, + flags: 0, + iterations: 2, + salt: Buffer.from("test"), + nextDomain: Buffer.from("test"), // Hashed per RFC5155 + rrtypes: ["A", "TXT", "RRSIG"], + }, + }, + { + type: "MX", + name: "localhost", + data: { + preference: 10, + exchange: "mx.localhost", + }, + }, + { + type: "TXT", + name: "localhost", + data: "test", + }, + { + type: "TXT", + name: "localhost", + data: Buffer.from("test"), + }, + { + type: "TXT", + name: "localhost", + data: ["foo", "bar"], + }, + { + type: "SRV", + name: "_imap._tcp.localhost", + data: { + priority: 10, + weight: 60, + port: 5060, + target: "imap.example.com", + }, + }, + { + type: "SOA", + name: "localhost", + data: { + mname: "localhost", + rname: "hostmaster.localhost", + serial: 2021122101, + }, + }, + { + type: "CAA", + name: "localhost", + data: { + issuerCritical: false, + tag: "issue", + value: "ca.example.com", + }, + }, + { + type: "TXT", + name: "version.bind", + class: "CH", + data: "1.2.3", + }, + { + type: "OPT", + name: ".", + udpPayloadSize: 65535, + extendedRcode: 255, + ednsVersion: 255, + flags: 65535, + flag_do: true, + options: [ + { + code: 8, + type: "CLIENT_SUBNET", + sourcePrefixLength: 0, + scopePrefixLength: 0, + ip: "127.0.0.1", + }, + { + code: 8, + ip: "127.0.0.1", + }, + { + code: 11, + type: "TCP_KEEPALIVE", + }, + { + code: 11, + timeout: 2468, + }, + { + code: 12, + length: 13, + }, + { + code: 14, + tags: [], + }, + { + code: 14, + tags: [256], + }, + ], + }, + { + type: "RP", + name: "localhost", + data: { + mbox: "admin.example.com", + txt: "txt.example.com", + }, + }, + { + type: "RRSIG", + name: "localhost", + data: { + typeCovered: "A", + algorithm: 8, + labels: 1, + originalTTL: 3600, + expiration: Date.now(), + inception: Date.now(), + keyTag: 12345, + signersName: "a.name", + signature: Buffer.from("test"), + }, + }, + { + type: "SSHFP", + name: "localhost", + data: { + algorithm: 1, + hash: 1, + fingerprint: "A108C9F834354D5B37AF988141C9294822F5BC00", + }, + }, +]; +encode({ answers: records }); + +// https://github.com/mafintosh/dns-packet/blob/5aebb85c3221292e994d01b68cadf067e78efabf/examples/tcp.js +function getRandomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +let buf = streamEncode({ + type: "query", + id: getRandomInt(1, 65534), + flags: RECURSION_DESIRED, + questions: [ + { + type: "A", + name: "google.com", + }, + ], +}); +streamDecode(buf); +buf = buf.slice(2 + streamDecode.bytes); diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..d642e13 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,668 @@ +/// + +/** + * The currently defined set of DNS record types. + */ +export type RecordType = + | "A" + | "AAAA" + | "AFSDB" + | "APL" + | "AXFR" + | "CAA" + | "CDNSKEY" + | "CDS" + | "CERT" + | "CNAME" + | "DNAME" + | "DHCID" + | "DLV" + | "DNSKEY" + | "DS" + | "HINFO" + | "HIP" + | "HTTPS" + | "IXFR" + | "IPSECKEY" + | "KEY" + | "KX" + | "LOC" + | "MX" + | "NAPTR" + | "NS" + | "NSEC" + | "NSEC3" + | "NSEC3PARAM" + | "NULL" + | "OPT" + | "PTR" + | "RRSIG" + | "RP" + | "SIG" + | "SOA" + | "SRV" + | "SSHFP" + | "SVCB" + | "TA" + | "TKEY" + | "TLSA" + | "TSIG" + | "TXT" + | "URI"; + +export type RecordClass = "IN" | "CS" | "CH" | "HS" | "ANY"; + +export interface Question { + type: RecordType; + name: string; + class?: RecordClass | undefined; + qu_bit?: boolean | undefined; +} + +// Data interfaces for various record types +export interface CaaData { + issuerCritical?: boolean | undefined; + flags?: number | undefined; + tag: "issue" | "issuewild" | "iodef"; + value: string; +} + +export interface DnskeyData { + flags: number; + algorithm: number; + key: Buffer; +} + +export interface DsData { + keyTag: number; + algorithm: number; + digestType: number; + digest: Buffer; +} + +export interface HInfoData { + cpu: string; + os: string; +} + +export interface MxData { + preference?: number | undefined; + exchange: string; +} + +export interface NaptrData { + order: number; + preference: number; + flags: string; + services: string; + regexp: string; + replacement: string; +} + +export interface NsecData { + nextDomain: string; + rrtypes: string[]; +} + +export interface Nsec3Data { + algorithm: number; + flags: number; + iterations: number; + salt: Buffer; + nextDomain: Buffer; + rrtypes: string[]; +} + +export interface RpData { + mbox: string; + txt: string; +} + +export interface RrsigData { + typeCovered: string; + algorithm: number; + labels: number; + originalTTL: number; + expiration: number; + inception: number; + keyTag: number; + signersName: string; + signature: Buffer; +} + +export interface SrvData { + port: number; + target: string; + priority?: number | undefined; + weight?: number | undefined; +} + +export interface SoaData { + mname: string; + rname: string; + serial?: number | undefined; + refresh?: number | undefined; + retry?: number | undefined; + expire?: number | undefined; + minimum?: number | undefined; +} + +export interface SshfpData { + algorithm: number; + hash: number; + fingerprint: string; +} + +export interface TlsaData { + usage: number; + selector: number; + matchingType: number; + certificate: Buffer; +} + +export type TxtData = string | Buffer | Array; + +// SVCB/HTTPS Parameter types +export interface SvcParamMandatory { + key: "mandatory" | 0; + value: Array; +} + +export interface SvcParamAlpn { + key: "alpn" | 1; + value: string | string[]; +} + +export interface SvcParamNoDefaultAlpn { + key: "no-default-alpn" | 2; + value?: number | undefined; +} + +export interface SvcParamPort { + key: "port" | 3; + value: number; +} + +export interface SvcParamIpv4Hint { + key: "ipv4hint" | 4; + value: string | string[]; +} + +export interface SvcParamEchConfig { + key: "echconfig" | 5; + value: string; + needBase64Decode?: boolean | undefined; +} + +export interface SvcParamIpv6Hint { + key: "ipv6hint" | 6; + value: string | string[]; +} + +export interface SvcParamDohPath { + key: "dohpath" | 7; + value: string; +} + +export interface SvcParamOdoh { + key: "odoh" | 32769; + value: string; + needBase64Decode?: boolean | undefined; +} + +export interface SvcParamUnknown { + key: string | number; + value?: unknown | undefined; + data?: Buffer | undefined; +} + +export type SvcParam = + | SvcParamMandatory + | SvcParamAlpn + | SvcParamNoDefaultAlpn + | SvcParamPort + | SvcParamIpv4Hint + | SvcParamEchConfig + | SvcParamIpv6Hint + | SvcParamDohPath + | SvcParamOdoh + | SvcParamUnknown; + +export interface HttpsData { + priority: number; + name: string; + values?: Record | undefined; +} + +export interface SvcbData { + priority: number; + name: string; + values?: Record | undefined; +} + +// Generic answer types +export interface GenericAnswer { + type: T; + name: string; +} + +export interface BaseAnswer extends GenericAnswer { + ttl?: number | undefined; + class?: RecordClass | undefined; + flush?: boolean | undefined; + data: D; +} + +// Answer type groupings +export type StringRecordType = "A" | "AAAA" | "CNAME" | "DNAME" | "NS" | "PTR"; + +export type OtherRecordType = + | "AFSDB" + | "APL" + | "AXFR" + | "CDNSKEY" + | "CDS" + | "CERT" + | "DHCID" + | "DLV" + | "HIP" + | "IPSECKEY" + | "IXFR" + | "KEY" + | "KX" + | "LOC" + | "NSEC3PARAM" + | "NULL" + | "SIG" + | "TA" + | "TKEY" + | "TSIG" + | "URI"; + +// Specific answer types +export type StringAnswer = BaseAnswer; +export type BufferAnswer = BaseAnswer; +export type CaaAnswer = BaseAnswer<"CAA", CaaData>; +export type DnskeyAnswer = BaseAnswer<"DNSKEY", DnskeyData>; +export type DSAnswer = BaseAnswer<"DS", DsData>; +export type HInfoAnswer = BaseAnswer<"HINFO", HInfoData>; +export type MxAnswer = BaseAnswer<"MX", MxData>; +export type NaptrAnswer = BaseAnswer<"NAPTR", NaptrData>; +export type Nsec3Answer = BaseAnswer<"NSEC3", Nsec3Data>; +export type NsecAnswer = BaseAnswer<"NSEC", NsecData>; +export type RpAnswer = BaseAnswer<"RP", RpData>; +export type RrsigAnswer = BaseAnswer<"RRSIG", RrsigData>; +export type SoaAnswer = BaseAnswer<"SOA", SoaData>; +export type SrvAnswer = BaseAnswer<"SRV", SrvData>; +export type SshfpAnswer = BaseAnswer<"SSHFP", SshfpData>; +export type TlsaAnswer = BaseAnswer<"TLSA", TlsaData>; +export type TxtAnswer = BaseAnswer<"TXT", TxtData>; +export type SvcbAnswer = BaseAnswer<"SVCB", SvcbData>; +export type HttpsAnswer = BaseAnswer<"HTTPS", HttpsData>; + +// OPT record +interface OptCodes { + OPTION_0: 0; + LLQ: 1; + UL: 2; + NSID: 3; + OPTION_4: 4; + DAU: 5; + DHU: 6; + N3U: 7; + CLIENT_SUBNET: 8; + EXPIRE: 9; + COOKIE: 10; + TCP_KEEPALIVE: 11; + PADDING: 12; + CHAIN: 13; + KEY_TAG: 14; + DEVICEID: 26946; + OPTION_65535: 65535; +} + +type OptCodeType = keyof OptCodes; +type OptCode = OptCodes[K]; + +interface GenericOpt { + code: OptCode; + type?: T | undefined; + data?: Buffer | undefined; +} + +interface ClientSubnetOpt extends GenericOpt<"CLIENT_SUBNET"> { + family?: number | undefined; + sourcePrefixLength?: number | undefined; + scopePrefixLength?: number | undefined; + ip: string | undefined; +} + +interface KeepAliveOpt extends GenericOpt<"TCP_KEEPALIVE"> { + timeout?: number | undefined; +} + +interface PaddingOpt extends GenericOpt<"PADDING"> { + length?: number | undefined; +} + +interface TagOpt extends GenericOpt<"KEY_TAG"> { + tags: number[]; +} + +export type PacketOpt = ClientSubnetOpt | KeepAliveOpt | PaddingOpt | TagOpt; + +export interface OptAnswer extends GenericAnswer<"OPT"> { + udpPayloadSize: number; + extendedRcode: number; + ednsVersion: number; + flags: number; + + /** + * Whether or not the DNS DO bit is set + */ + flag_do: boolean; + + options: PacketOpt[]; +} + +// Complete answer union type +export type Answer = + | StringAnswer + | BufferAnswer + | CaaAnswer + | DnskeyAnswer + | DSAnswer + | HInfoAnswer + | HttpsAnswer + | MxAnswer + | NaptrAnswer + | Nsec3Answer + | NsecAnswer + | OptAnswer + | RpAnswer + | RrsigAnswer + | SoaAnswer + | SrvAnswer + | SshfpAnswer + | SvcbAnswer + | TlsaAnswer + | TxtAnswer; + +export interface Packet { + /** + * Whether the packet is a query or a response. This field may be + * omitted if it is clear from the context of usage what type of packet + * it is. + */ + type?: "query" | "response" | undefined; + + id?: number | undefined; + + /** + * A bit-mask combination of zero or more of: + * {@link AUTHORITATIVE_ANSWER}, + * {@link TRUNCATED_RESPONSE}, + * {@link RECURSION_DESIRED}, + * {@link RECURSION_AVAILABLE}, + * {@link AUTHENTIC_DATA}, + * {@link CHECKING_DISABLED}, + * {@link DNSSEC_OK}. + */ + flags?: number | undefined; + + opcode?: string | undefined; + rcode?: string | undefined; + + questions?: Question[] | undefined; + answers?: Answer[] | undefined; + additionals?: Answer[] | undefined; + authorities?: Answer[] | undefined; +} + +/** + * Decoded packet with individual flag bits extracted. + */ +export interface DecodedPacket extends Packet { + flag_qr: boolean; + flag_aa: boolean; + flag_tc: boolean; + flag_rd: boolean; + flag_ra: boolean; + flag_z: boolean; + flag_ad: boolean; + flag_cd: boolean; +} + +// Constants +export const AUTHORITATIVE_ANSWER: number; +export const TRUNCATED_RESPONSE: number; +export const RECURSION_DESIRED: number; +export const RECURSION_AVAILABLE: number; +export const AUTHENTIC_DATA: number; +export const CHECKING_DISABLED: number; +export const DNSSEC_OK: number; +export const NXDOMAIN: number; + +// Main functions +export function encode(packet: Packet, buf?: Buffer, offset?: number): Buffer; + +export namespace encode { + let bytes: number; +} + +export function decode(buf: Buffer, offset?: number): DecodedPacket; + +export namespace decode { + let bytes: number; +} + +export function encodingLength(packet: Packet): number; + +export function streamEncode(packet: Packet): Buffer; + +export namespace streamEncode { + let bytes: number; +} + +export function streamDecode(buf: Buffer): DecodedPacket | null; + +export namespace streamDecode { + let bytes: number; +} + +// Utility/Helper exports +export const name: { + encode( + str: string, + buf?: Buffer, + offset?: number, + options?: { mail?: boolean } + ): Buffer; + decode(buf: Buffer, offset?: number, options?: { mail?: boolean }): string; + encodingLength(name: string): number; +}; + +export const question: { + encode(q: Question, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): Question; + encodingLength(q: Question): number; +}; + +export const answer: { + encode(a: Answer, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): Answer; + encodingLength(a: Answer): number; +}; + +export const svcparam: { + encode(param: SvcParam, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): SvcParam; + encodingLength(param: SvcParam): number; + keyToNumber(keyName: string): number; + numberToKeyName(number: number): string; +}; + +export const svcb: { + encode(data: SvcbData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): SvcbData; + encodingLength(data: SvcbData): number; +}; + +export const httpssvc: { + encode(data: HttpsData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): HttpsData; + encodingLength(data: HttpsData): number; +}; + +export const a: { + encode(host: string, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): string; + encodingLength(): number; +}; + +export const aaaa: { + encode(host: string, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): string; + encodingLength(): number; +}; + +export const cname: { + encode(data: string, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): string; + encodingLength(data: string): number; +}; + +export const dname: { + encode(data: string, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): string; + encodingLength(data: string): number; +}; + +export const ptr: { + encode(data: string, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): string; + encodingLength(data: string): number; +}; + +export const ns: { + encode(data: string, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): string; + encodingLength(data: string): number; +}; + +export const mx: { + encode(data: MxData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): MxData; + encodingLength(data: MxData): number; +}; + +export const srv: { + encode(data: SrvData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): SrvData; + encodingLength(data: SrvData): number; +}; + +export const caa: { + encode(data: CaaData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): CaaData; + encodingLength(data: CaaData): number; + ISSUER_CRITICAL: number; +}; + +export const txt: { + encode(data: TxtData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): Buffer[]; + encodingLength(data: TxtData): number; +}; + +export const null_: { + encode(data: Buffer | string, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): Buffer; + encodingLength(data?: Buffer | string): number; +}; + +export const hinfo: { + encode(data: HInfoData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): HInfoData; + encodingLength(data: HInfoData): number; +}; + +export const soa: { + encode(data: SoaData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): SoaData; + encodingLength(data: SoaData): number; +}; + +export const naptr: { + encode(data: NaptrData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): NaptrData; + encodingLength(data: NaptrData): number; +}; + +export const dnskey: { + encode(key: DnskeyData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): DnskeyData; + encodingLength(key: DnskeyData): number; + PROTOCOL_DNSSEC: number; + ZONE_KEY: number; + SECURE_ENTRYPOINT: number; +}; + +export const rrsig: { + encode(sig: RrsigData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): RrsigData; + encodingLength(sig: RrsigData): number; +}; + +export const rp: { + encode(data: RpData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): RpData; + encodingLength(data: RpData): number; +}; + +export const nsec: { + encode(record: NsecData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): NsecData; + encodingLength(record: NsecData): number; +}; + +export const nsec3: { + encode(record: Nsec3Data, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): Nsec3Data; + encodingLength(record: Nsec3Data): number; +}; + +export const ds: { + encode(digest: DsData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): DsData; + encodingLength(digest: DsData): number; +}; + +export const sshfp: { + encode(record: SshfpData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): SshfpData; + encodingLength(record: SshfpData): number; + getFingerprintLengthForHashType(hashType: number): number | undefined; +}; + +export const tlsa: { + encode(cert: TlsaData, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): TlsaData; + encodingLength(cert: TlsaData): number; +}; + +export const opt: { + encode(options: PacketOpt[], buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): PacketOpt[]; + encodingLength(options: PacketOpt[]): number; +}; + +export const unknown: { + encode(data: Buffer, buf?: Buffer, offset?: number): Buffer; + decode(buf: Buffer, offset?: number): Buffer; + encodingLength(data: Buffer): number; +}; + +export function record(type: RecordType): any; + +export {}; diff --git a/package.json b/package.json index 9b79b96..d402b40 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,17 @@ "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" }, + "types": "index.d.ts", "devDependencies": { + "@types/node": "*", "eslint": "^5.14.1", "eslint-config-standard": "^12.0.0", "eslint-plugin-import": "^2.16.0", "eslint-plugin-node": "^8.0.1", "eslint-plugin-promise": "^4.0.1", "eslint-plugin-standard": "^4.0.0", - "tape": "^4.10.1" + "tape": "^4.10.1", + "typescript": "^6.0.2" }, "keywords": [ "dns", @@ -38,6 +41,7 @@ ], "files": [ "index.js", + "index.d.ts", "types.js", "rcodes.js", "opcodes.js", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..71c0d55 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "node16", + "lib": [ + "es6" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "types": ["node"], + "noEmit": true, + "forceConsistentCasingInFileNames": true, + "ignoreDeprecations": "6.0", + "baseUrl": ".", + "paths": { + "dns-packet": ["./index.d.ts"] + } + }, + "files": [ + "index.d.ts", + "dns-packet-tests.ts" + ] +}