Skip to content

Commit 009785d

Browse files
author
Hunts Chen
committed
add support of dns-json format
1 parent c2c7e6f commit 009785d

46 files changed

Lines changed: 743 additions & 159 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@dnspect/dns-ts",
3-
"version": "0.1.5",
3+
"version": "0.2.0",
44
"description": "DNS library in TypeScript",
55
"author": "Minghang Chen <chen@minghang.dev> (https://minghang.dev)",
66
"license": "MIT",

‎src/buffer.ts‎

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import { stringToBinary, EncodingScheme } from "./encoding";
22
import { Uint16, Uint8, Uint32 } from "./types";
33

44
/**
5-
* Maximum size of a TCP packet is 64K (65535 bytes), length of a DNS message is 2 bytes.
6-
* Thus, the message size itself should not exceed 65535-2, which is 0xfffd.
5+
* Maximum size of a DNS packet is 64K (65535 bytes) because the payload length field is a 16-bit.
76
*/
8-
export const MAX_MESSAGE_SIZE = 0xFFFD;
7+
export const MAX_MESSAGE_SIZE = 0xFFFF;
98

109
/**
1110
* Allows for reading bytes from a source.
@@ -49,7 +48,7 @@ export interface Reader {
4948
* at the index 'end'. Default to the end of the source if not specified.
5049
* @returns An array of bytes read
5150
*/
52-
read(offset?: number, end?: number): Uint8Array;
51+
slice(offset?: number, end?: number): Uint8Array;
5352
}
5453

5554
/**
@@ -123,13 +122,13 @@ export interface Writer {
123122
*
124123
* It is able to grow exponentially when it is writable.
125124
*/
126-
export class OctetBuffer implements Reader, Writer {
125+
export class PacketBuffer implements Reader, Writer {
127126
buf!: Uint8Array;
128127
private offset = 0;
129128
private writable = false;
130129

131130
/**
132-
* Initializes a writable OctetBuffer object with default 1232 bytes that are zero-filled.
131+
* Initializes a writable PacketBuffer object with default 1232 bytes that are zero-filled.
133132
*
134133
* @param buf
135134
*/
@@ -143,21 +142,21 @@ export class OctetBuffer implements Reader, Writer {
143142
*
144143
* @param size The initial size of the buffer.
145144
*/
146-
static alloc(size: number): OctetBuffer {
147-
return new OctetBuffer(new Uint8Array(size), true);
145+
static alloc(size: number): PacketBuffer {
146+
return new PacketBuffer(new Uint8Array(size), true);
148147
}
149148

150149
/**
151-
* Creates a readonly OctetBuffer object using the passed data.
150+
* Creates a readonly PacketBuffer object using the passed data.
152151
*
153152
* @param buf
154153
* @returns
155154
*/
156-
static from(buf: ArrayLike<number> | ArrayBufferLike): OctetBuffer {
155+
static from(buf: ArrayLike<number> | ArrayBufferLike): PacketBuffer {
157156
if (buf instanceof Uint8Array) {
158-
return new OctetBuffer(buf, false);
157+
return new PacketBuffer(buf, false);
159158
}
160-
return new OctetBuffer(new Uint8Array(buf), false);
159+
return new PacketBuffer(new Uint8Array(buf), false);
161160
}
162161

163162
checkWrite(offset: number, extra: number): void {
@@ -221,7 +220,7 @@ export class OctetBuffer implements Reader, Writer {
221220
return (this.buf[pos] * 0x1000000) + ((this.buf[pos + 1] << 16) | (this.buf[pos + 2] << 8) | this.buf[pos + 3]);
222221
}
223222

224-
read(offset = 0, end = this.buf.length): Uint8Array {
223+
slice(offset = 0, end = this.buf.length): Uint8Array {
225224
this.checkRead(offset, end - offset);
226225
return this.buf.slice(offset, end);
227226
}
@@ -273,7 +272,7 @@ export class OctetBuffer implements Reader, Writer {
273272
return this.write(data);
274273
}
275274

276-
freeze(len: number): OctetBuffer {
275+
freeze(len: number): PacketBuffer {
277276
this.buf = this.buf.subarray(0, len);
278277
this.writable = false;
279278
return this;

‎src/edns/client-subnet.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { expect } from "chai";
2+
import { ClientSubnet } from "./client-subnet";
3+
import { Prefix } from "@dnspect/ip-address-ts";
4+
5+
describe("test construction", () => {
6+
it("should fail to create client-subnet", () => {
7+
expect(() => ClientSubnet.from([0, 0, 0, 0])).to.throw(Error, `invalid address family: 0`);
8+
expect(() => ClientSubnet.from([0, 3, 0, 0])).to.throw(Error, `invalid address family: 3`);
9+
expect(() => ClientSubnet.from([0, 1])).to.throw(Error, `insufficient bytes remaining for read: needs 1, have 0`);
10+
expect(() => ClientSubnet.from([0, 1, 0, 0])).to.throw(Error, `insufficient bytes remaining for read: needs 4, have 0`);
11+
expect(() => ClientSubnet.from([0, 2, 0, 0])).to.throw(Error, `insufficient bytes remaining for read: needs 16, have 0`);
12+
expect(() => ClientSubnet.from([0, 1, 33, 0, 0, 0, 0, 0])).to.throw(Error, `invalid source prefix length 33 for address family 1`);
13+
expect(() => ClientSubnet.from([0, 1, 24, 33, 0, 0, 0, 0])).to.throw(Error, `invalid scope prefix length 33 for address family 1`);
14+
});
15+
16+
it("should create client-subnet from data", () => {
17+
expect(ClientSubnet.from([0, 1, 24, 12, 1, 2, 3, 0]).toString()).to.equal(`; CLIENT-SUBNET: 1.2.3.0/24/12`);
18+
});
19+
20+
it("should create client-subnet from IP prefix", () => {
21+
expect(ClientSubnet.fromPrefix(Prefix.parse('1.2.3.0/24')).toString()).to.equal(`; CLIENT-SUBNET: 1.2.3.0/24/0`);
22+
});
23+
});
24+
25+
describe("test properties", () => {
26+
it("should have correct optCode", () => {
27+
expect(ClientSubnet.from([0, 1, 24, 0, 1, 2, 3, 0]).sourcePrefixLength).to.equal(24);
28+
expect(ClientSubnet.from([0, 1, 24, 12, 1, 2, 3, 0]).scopePrefixLength).to.equal(12);
29+
});
30+
});

‎src/edns/client-subnet.ts‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { Address, Address4, Address6, Prefix } from "@dnspect/ip-address-ts";
2+
import { OptCode, Option } from "./option";
3+
import { Slice } from "../packet";
4+
import { Writer } from "../buffer";
5+
import { Uint8 } from "../types";
6+
import { ParseError } from "../error";
7+
8+
/**
9+
* ClientSubnet option is in active use to carry information about the network that originated a DNS
10+
* query and the network for which the subsequent response can be cached.
11+
*
12+
* Specified by {@link https://datatracker.ietf.org/doc/html/rfc7871 | RFC 7871}.
13+
*
14+
* +0 (MSB) +1 (LSB)
15+
* +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
16+
* 0: | OPTION-CODE |
17+
* +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
18+
* 2: | OPTION-LENGTH |
19+
* +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
20+
* 4: | FAMILY |
21+
* +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
22+
* 6: | SOURCE PREFIX-LENGTH | SCOPE PREFIX-LENGTH |
23+
* +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
24+
* 8: | ADDRESS... /
25+
* +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
26+
*/
27+
export class ClientSubnet extends Option {
28+
sourcePrefixLength!: Uint8;
29+
scopePrefixLength!: Uint8;
30+
address!: Address;
31+
32+
constructor(data: Slice | Prefix) {
33+
super(OptCode.ClientSubnet);
34+
35+
if (data instanceof Slice) {
36+
const family = data.readUint16();
37+
this.sourcePrefixLength = data.readUint8();
38+
this.scopePrefixLength = data.readUint8();
39+
40+
switch (family) {
41+
case 1:
42+
this.address = Address4.fromBytes(data.readUint8Array(4));
43+
break;
44+
case 2:
45+
this.address = Address6.fromBytes(data.readUint8Array(16));
46+
break;
47+
default:
48+
throw new ParseError(`invalid address family: ${family}`);
49+
}
50+
51+
if (this.address.bits() < this.sourcePrefixLength) {
52+
throw new ParseError(`invalid source prefix length ${this.sourcePrefixLength} for address family ${family}`);
53+
}
54+
55+
if (this.address.bits() < this.scopePrefixLength) {
56+
throw new ParseError(`invalid scope prefix length ${this.scopePrefixLength} for address family ${family}`);
57+
}
58+
} else {
59+
this.address = data.ip();
60+
this.sourcePrefixLength = data.length();
61+
this.scopePrefixLength = 0;
62+
}
63+
}
64+
65+
packOptionData(buf: Writer): number {
66+
let n = buf.writeUint16(this.address.isIPv4() ? 1 : 2);
67+
n += buf.writeUint8(this.sourcePrefixLength);
68+
n += buf.writeUint8(this.scopePrefixLength);
69+
n += buf.write(this.address.bytes());
70+
return n;
71+
}
72+
73+
/**
74+
* Refer to {@link https://datatracker.ietf.org/doc/html/rfc5001#section-2.4 | Presentation Format}.
75+
* @returns
76+
*/
77+
toString(): string {
78+
return `; CLIENT-SUBNET: ${this.address.toString()}/${this.sourcePrefixLength}/${this.scopePrefixLength}`;
79+
}
80+
81+
/**
82+
* Creates a ClientSubnet from an IP prefix.
83+
*
84+
* @param id
85+
* @param encoding
86+
* @returns
87+
*/
88+
static fromPrefix(prefix: Prefix): ClientSubnet {
89+
return new ClientSubnet(prefix);
90+
}
91+
92+
/**
93+
* Creates a ClientSubnet using the passed data.
94+
*
95+
* @param data ClientSubnet data
96+
* @returns
97+
*/
98+
static from(data: ArrayLike<number> | ArrayBufferLike): ClientSubnet {
99+
return new ClientSubnet(Slice.from(data));
100+
}
101+
}

‎src/edns/cookie.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class Cookie extends Option {
7878
* @param data Cookie data
7979
* @returns
8080
*/
81-
static from(data: ArrayLike<number>): Cookie {
81+
static from(data: ArrayLike<number> | ArrayBufferLike): Cookie {
8282
return new Cookie(Slice.from(data));
8383
}
8484
}

‎src/edns/ede.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { expect } from "chai";
2+
import { ExtendedError, ExtendedErrorCode } from "./ede";
3+
4+
describe("test construction", () => {
5+
it("should fail to create EDE", () => {
6+
expect(() => ExtendedError.from([0])).to.throw(Error, `insufficient bytes remaining for read: needs 2, have 1`);
7+
});
8+
9+
it("should create EDE from data", () => {
10+
expect(ExtendedError.from([0, 1]).toString()).to.equal(`; EDE: 1 (Unsupported DNSKEY Algorithm)`);
11+
expect(ExtendedError.from([0, 1, 65]).toString()).to.equal(`; EDE: 1 (Unsupported DNSKEY Algorithm): (A)`);
12+
});
13+
14+
it("should create EDE from code", () => {
15+
expect(ExtendedError.fromCode(ExtendedErrorCode.OtherError).toString()).to.equal(`; EDE: 0 (Other Error)`);
16+
expect(ExtendedError.fromCode(ExtendedErrorCode.OtherError, "custom").toString()).to.equal(`; EDE: 0 (Other Error): (custom)`);
17+
});
18+
});

‎src/edns/ede.ts‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@ export class ExtendedError extends Option {
1818
infoCode!: ExtendedErrorCode;
1919
extraText!: string;
2020

21-
constructor(data: Slice) {
21+
constructor(data: Slice | ExtendedErrorCode, text?: string) {
2222
super(OptCode.ExtendedError);
23-
this.infoCode = data.readUint16();
24-
this.extraText = data.readUTF8String();
23+
if (data instanceof Slice) {
24+
this.infoCode = data.readUint16();
25+
this.extraText = data.readUTF8String();
26+
} else {
27+
this.infoCode = data;
28+
this.extraText = text ?? '';
29+
}
2530
}
2631

2732
/**
@@ -49,6 +54,27 @@ export class ExtendedError extends Option {
4954
}
5055
return output;
5156
}
57+
58+
/**
59+
* Creates a new ExtendedError from the option data.
60+
*
61+
* @param data The option data.
62+
* @returns
63+
*/
64+
static from(data: ArrayLike<number> | ArrayBufferLike): ExtendedError {
65+
return new ExtendedError(Slice.from(data));
66+
}
67+
68+
/**
69+
* Creates a new ExtendedError from error code and optional text.
70+
*
71+
* @param infoCode
72+
* @param extraText
73+
* @returns
74+
*/
75+
static fromCode(infoCode: ExtendedErrorCode, extraText?: string): ExtendedError {
76+
return new ExtendedError(infoCode, extraText);
77+
}
5278
}
5379

5480
/**

‎src/edns/index.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@
77
* @packageDocumentation
88
*/
99

10+
import { NSID } from "./nsid";
11+
import { ClientSubnet } from "./client-subnet";
1012
import { Cookie } from "./cookie";
1113
import { ExtendedError } from "./ede";
12-
import { NSID } from "./nsid";
1314
import { OptCode, Option } from "./option";
1415
import { Slice } from "../packet";
1516

17+
export { NSID } from "./nsid";
18+
export { ClientSubnet } from "./client-subnet";
1619
export { Cookie } from "./cookie";
1720
export { ExtendedError, ExtendedErrorCode, EDE_PRIVATE_RANGE_BEGIN } from "./ede";
18-
export { NSID } from "./nsid";
1921
export { OptCode, Option } from "./option";
2022

2123
export function unpack(data: Slice): Option[] {
@@ -29,6 +31,9 @@ export function unpack(data: Slice): Option[] {
2931
case OptCode.NSID:
3032
options.push(new NSID(data.readSlice(len)));
3133
break;
34+
case OptCode.ClientSubnet:
35+
options.push(new ClientSubnet(data.readSlice(len)));
36+
break;
3237
case OptCode.Cookie:
3338
options.push(new Cookie(data.readSlice(len)));
3439
break;

‎src/edns/nsid.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ import { Writer } from "../buffer";
1212
export class NSID extends Option {
1313
identifier!: Uint8Array;
1414

15-
constructor(data: Slice) {
15+
constructor(data: Slice | Uint8Array) {
1616
super(OptCode.NSID);
17-
this.identifier = data.readUint8Array();
17+
if (data instanceof Slice) {
18+
this.identifier = data.readUint8Array();
19+
} else {
20+
this.identifier = data;
21+
}
1822
}
1923

2024
packOptionData(buf: Writer): number {
@@ -45,6 +49,6 @@ export class NSID extends Option {
4549
*/
4650
static fromString(id: string, encoding: "ascii" | "utf-8" = "utf-8"): NSID {
4751
const bin = stringToBinary(id, encoding);
48-
return new NSID(Slice.from(bin));
52+
return new NSID(bin);
4953
}
5054
}

‎src/edns/option.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,16 @@ export abstract class Option {
6969
/**
7070
*
7171
*/
72+
7273
export enum OptCode {
7374
/**
7475
* Name server identifier. RFC 5001
7576
*/
7677
NSID = 3,
78+
/**
79+
* Client Subnet. RFC 7871
80+
*/
81+
ClientSubnet = 8,
7782
/**
7883
* DNS Cookies. RFC 7873
7984
*/

0 commit comments

Comments
 (0)