From 65b05eb449ec9901778a95bc97b8d1c6d5d4de8c Mon Sep 17 00:00:00 2001 From: LittleChest Date: Wed, 8 Apr 2026 23:23:49 +0800 Subject: [PATCH] Update types --- dns-packet-tests.ts | 277 ++++++++++++++++++++ index.d.ts | 597 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 6 +- tsconfig.json | 24 ++ 4 files changed, 903 insertions(+), 1 deletion(-) create mode 100644 dns-packet-tests.ts create mode 100644 index.d.ts create mode 100644 tsconfig.json diff --git a/dns-packet-tests.ts b/dns-packet-tests.ts new file mode 100644 index 0000000..e525029 --- /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 = new Uint8Array(0); +const length: number = encodingLength(inPacket); +const out: Uint8Array = 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..cdbfb19 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,597 @@ +/// + +/** + * 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" + | "SPF" + | "SRV" + | "SSHFP" + | "SVCB" + | "TA" + | "TKEY" + | "TLSA" + | "TSIG" + | "TXT" + | "URI"; + +export type RecordClass = "IN" | "CS" | "CH" | "HS" | "ANY"; + +export type OpCode = "QUERY" | "IQUERY" | "STATUS" | "NOTIFY" | "UPDATE" | string; + +export type RCode = "NOERROR" | "FORMERR" | "SERVFAIL" | "NXDOMAIN" | "NOTIMP" | "REFUSED" | "YXDOMAIN" | "YXRRSET" | "NXRRSET" | "NOTAUTH" | "NOTZONE" | string; + +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: string | Uint8Array; +} + +export interface DsData { + keyTag: number; + algorithm: number; + digestType: number; + digest: string | Uint8Array; +} + +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: Uint8Array; + nextDomain: Uint8Array; + rrtypes: string[]; +} + +export interface RpData { + mbox: string; + txt: string; +} + +export interface RrsigData { + typeCovered: RecordType | string; + algorithm: number; + labels: number; + originalTTL: number; + expiration: number; + inception: number; + keyTag: number; + signersName: string; + signature: string | Uint8Array; +} + +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: string | Uint8Array; +} + +export type TxtData = string | Uint8Array | 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 | Uint8Array; + 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 | Uint8Array; + needBase64Decode?: boolean | undefined; +} + +export interface SvcParamUnknown { + key: string | number; + value?: unknown | undefined; + data?: Uint8Array | 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?: Uint8Array | 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?: OpCode | number | undefined; + rcode?: RCode | number | 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?: Uint8Array | ArrayBufferLike, offset?: number): Uint8Array; + +export namespace encode { + let bytes: number; +} + +export function decode(buf: Uint8Array | ArrayBufferLike, offset?: number): DecodedPacket; + +export namespace decode { + let bytes: number; +} + +export function encodingLength(packet: Packet): number; + +export function streamEncode(packet: Packet): Uint8Array; + +export namespace streamEncode { + let bytes: number; +} + +export function streamDecode(buf: Uint8Array | ArrayBufferLike, offset?: number): DecodedPacket | null; + +export namespace streamDecode { + let bytes: number; +} + +// Utility/Helper exports +export const name: Codec & { + encode( + str: string, + buf?: Uint8Array | ArrayBufferLike, + offset?: number, + options?: { mail?: boolean } + ): Uint8Array; + decode(buf: Uint8Array | ArrayBufferLike, offset?: number, options?: { mail?: boolean }): string; + encodingLength(name: string): number; +}; + +export const question: Codec; + +export const answer: Codec; + +export const svcparam: Codec & { + keyToNumber(keyName: string): number; + numberToKeyName(number: number): string; +}; + +export const svcb: Codec; + +export const httpssvc: Codec; + +export const a: Codec; + +export const aaaa: Codec; + +export const cname: Codec; + +export const dname: Codec; + +export const ptr: Codec; + +export const ns: Codec; + +export const mx: Codec; + +export const srv: Codec; + +export const caa: Codec & { + ISSUER_CRITICAL: number; +}; + +export const txt: Codec & { + encode(data: TxtData, buf?: Uint8Array, offset?: number): Uint8Array; +}; + +export const null_: Codec & { + encode(data: Uint8Array | string, buf?: Uint8Array, offset?: number): Uint8Array; + encodingLength(data?: Uint8Array | string): number; +}; + +export const hinfo: Codec; + +export const soa: Codec; + +export const naptr: Codec; + +export const dnskey: Codec & { + PROTOCOL_DNSSEC: number; + ZONE_KEY: number; + SECURE_ENTRYPOINT: number; +}; + +export const rrsig: Codec; + +export const rp: Codec; + +export const nsec: Codec; + +export const nsec3: Codec; + +export const ds: Codec; + +export const sshfp: Codec & { + getFingerprintLengthForHashType(hashType: number): number | undefined; +}; + +export const tlsa: Codec; + +export const opt: Codec; + +export const option: Codec; + +export const unknown: Codec & { + encode(data: Uint8Array | string, buf?: Uint8Array, offset?: number): Uint8Array; + encodingLength(data: Uint8Array | string): number; +}; + +// Codec interface for type-safe encode/decode implementations +export interface Codec { + encode(data: T, buf?: Uint8Array | ArrayBufferLike, offset?: number): Uint8Array; + decode(buf: Uint8Array | ArrayBufferLike, offset?: number): T; + encodingLength(data: T): number; +} + +export function record(type: "A" | "AAAA" | "CNAME" | "DNAME" | "NS" | "PTR"): Codec; +export function record(type: "MX"): Codec; +export function record(type: "SRV"): Codec; +export function record(type: "SOA"): Codec; +export function record(type: "TXT"): Codec; +export function record(type: "CAA"): Codec; +export function record(type: "HINFO"): Codec; +export function record(type: "NAPTR"): Codec; +export function record(type: "RP"): Codec; +export function record(type: "RRSIG"): Codec; +export function record(type: "DS"): Codec; +export function record(type: "DNSKEY"): Codec; +export function record(type: "NSEC"): Codec; +export function record(type: "NSEC3"): Codec; +export function record(type: "SSHFP"): Codec; +export function record(type: "TLSA"): Codec; +export function record(type: "SVCB"): Codec; +export function record(type: "HTTPS"): Codec; +export function record(type: RecordType): Codec; + +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" + ] +}