@gajanan_107/dns3 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +427 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# @gajanan_107/dns3
|
|
2
|
+
|
|
3
|
+
A DNS framework for Node.js that puts you in control of every step of DNS resolution.
|
|
4
|
+
|
|
5
|
+
Most DNS packages resolve queries for you behind the scenes — you hand them a domain and they hand you an IP. **dns3 does not do that.** Instead, it gives you the raw tools to handle each step yourself: receive the packet, parse it, check your own database, check your own cache, forward to upstream if needed, parse the response, and send bytes back to the client.
|
|
6
|
+
|
|
7
|
+
This means you will actually understand what happens when someone types `google.com` in their browser.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why dns3
|
|
12
|
+
|
|
13
|
+
When you use a regular DNS library, you never see the packet. You never see the `id`, the `flags`, the `questions` section. You never understand why a response has to carry the same `id` as the request. You never see that what travels over the wire is just raw bytes — not JSON, not objects.
|
|
14
|
+
|
|
15
|
+
dns3 exposes all of that. Every step is explicit. You write each step yourself. Your cache is yours. Your database is yours. dns3 only gives you the functions to move between raw bytes and structured data.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @gajanan_107/dns3
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## How DNS actually works
|
|
28
|
+
|
|
29
|
+
Before looking at code, understand what happens when a DNS query arrives:
|
|
30
|
+
|
|
31
|
+
1. A client (browser, OS) sends a **raw UDP packet** to your server. That packet is just bytes.
|
|
32
|
+
2. Inside those bytes is a **question**: *"What is the IP address of `google.com`?"*
|
|
33
|
+
3. Your server checks if it already knows the answer — from a local database or a cache.
|
|
34
|
+
4. If it does not know, it forwards the **original raw bytes** to an upstream DNS server like `8.8.8.8`.
|
|
35
|
+
5. The upstream server sends back **raw bytes** containing the answer.
|
|
36
|
+
6. Your server parses those bytes to read the answer, optionally stores it in cache, then sends bytes back to the original client.
|
|
37
|
+
|
|
38
|
+
dns3 maps directly to these steps. Nothing is hidden.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
const dns3 = require('@gajanan_107/dns3');
|
|
46
|
+
const { parseRequest, parseResponse, createResponseFromRequest, upstreamResponse } = dns3;
|
|
47
|
+
|
|
48
|
+
const server = dns3.createServer();
|
|
49
|
+
|
|
50
|
+
// Your own storage — use MongoDB, Redis, a plain object, anything
|
|
51
|
+
const localdb = {
|
|
52
|
+
'myapp.local': { 'A': '192.168.1.10' },
|
|
53
|
+
};
|
|
54
|
+
const cache = {};
|
|
55
|
+
|
|
56
|
+
server.handle('onmessage', async (msg, send) => {
|
|
57
|
+
|
|
58
|
+
// Step 1 — msg is raw bytes, parse it to see what's inside
|
|
59
|
+
const request = parseRequest(msg);
|
|
60
|
+
const { name, type } = request.questions[0];
|
|
61
|
+
|
|
62
|
+
// Step 2 — check your local database first
|
|
63
|
+
if (localdb[name]?.[type]) {
|
|
64
|
+
const response = createResponseFromRequest(request);
|
|
65
|
+
response.answers.push(localdb[name][type]);
|
|
66
|
+
return send(response);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Step 3 — check your cache
|
|
70
|
+
if (cache[`${name}:${type}`]) {
|
|
71
|
+
const response = createResponseFromRequest(request);
|
|
72
|
+
response.answers.push(cache[`${name}:${type}`]);
|
|
73
|
+
return send(response);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Step 4 — not found anywhere, forward the original raw bytes upstream
|
|
77
|
+
const rawUpstream = await upstreamResponse(msg);
|
|
78
|
+
|
|
79
|
+
// Step 5 — upstream replied with raw bytes, parse them to read the answers
|
|
80
|
+
const parsed = parseResponse(rawUpstream);
|
|
81
|
+
for (const record of parsed.answers) {
|
|
82
|
+
cache[`${name}:${type}`] = record; // store in your cache for next time
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Step 6 — send the raw upstream bytes directly back to the client
|
|
86
|
+
send(rawUpstream);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
server.handle('onerror', (err) => {
|
|
90
|
+
console.error('DNS error:', err.message);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
server.listen(5353);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Test it:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
dig @127.0.0.1 -p5353 myapp.local
|
|
100
|
+
dig @127.0.0.1 -p5353 google.com
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## The request object
|
|
106
|
+
|
|
107
|
+
When `parseRequest(msg)` is called, the raw bytes are parsed into a structured object that reflects the actual DNS packet format:
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
{
|
|
111
|
+
id: 1432, // unique ID — the response must carry this same ID
|
|
112
|
+
flags: {
|
|
113
|
+
raw: 256, // the 2-byte flags field as it arrived on the wire
|
|
114
|
+
qr: 0, // 0 = this is a query, 1 = this is a response
|
|
115
|
+
opcode: 0, // 0 = standard query
|
|
116
|
+
aa: 0, // authoritative answer
|
|
117
|
+
tc: 0, // truncated (message was cut short)
|
|
118
|
+
rd: 1, // recursion desired — client wants full resolution
|
|
119
|
+
ra: 0, // recursion available — server supports recursion
|
|
120
|
+
z: 0, // reserved, always 0
|
|
121
|
+
rcode: 0, // response code — 0 = no error, 3 = NXDOMAIN
|
|
122
|
+
},
|
|
123
|
+
questions: [
|
|
124
|
+
{ name: 'google.com', type: 'A', class: 1 }
|
|
125
|
+
],
|
|
126
|
+
answers: [], // empty in a query, filled in a response
|
|
127
|
+
authority: [], // authoritative nameserver records
|
|
128
|
+
additional: [], // additional records the sender included
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The `id` is the most important field to understand. When a client sends a DNS query, it assigns a random `id` to it. When your server sends a response back, that response **must carry the same `id`** — otherwise the client will discard it. This is exactly why `createResponseFromRequest` exists: it copies the `id` automatically.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## The response object
|
|
137
|
+
|
|
138
|
+
`createResponseFromRequest(request)` returns a response shell that already has the correct `id` and `questions` copied from the request. You only need to fill in the `answers`:
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
{
|
|
142
|
+
id: 1432, // copied from request — do not change this
|
|
143
|
+
flags: {
|
|
144
|
+
qr: 1, // 1 = this is a response
|
|
145
|
+
opcode: 0, // copied from request
|
|
146
|
+
aa: 0, // set to 1 if you are authoritative for this domain
|
|
147
|
+
tc: 0, // set to 1 if the response was truncated
|
|
148
|
+
rd: 1, // copied from request
|
|
149
|
+
ra: 1, // 1 = this server supports recursion
|
|
150
|
+
z: 0,
|
|
151
|
+
rcode: 0, // 0 = success, 3 = NXDOMAIN (domain does not exist)
|
|
152
|
+
},
|
|
153
|
+
questions: [...], // copied from request
|
|
154
|
+
answers: [], // push your answer here
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
You can change any `flags` field before calling `send`. For example to tell the client a domain does not exist:
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
const response = createResponseFromRequest(request);
|
|
162
|
+
response.flags.rcode = 3; // 3 = NXDOMAIN
|
|
163
|
+
send(response);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Pushing answers
|
|
169
|
+
|
|
170
|
+
dns3 accepts two formats when pushing into `response.answers`.
|
|
171
|
+
|
|
172
|
+
**Format 1 — just the data value.** dns3 fills in the rest from the question context:
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
response.answers.push('192.168.1.10'); // A record
|
|
176
|
+
response.answers.push('::1'); // AAAA record
|
|
177
|
+
response.answers.push('mail.myapp.local'); // CNAME or MX exchange
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Format 2 — a full record object.** Use this when pushing a record that came from `parseResponse` (it already has the full shape), or when you want to control `ttl`:
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
response.answers.push({
|
|
184
|
+
name: 'google.com',
|
|
185
|
+
type: 'A',
|
|
186
|
+
class: 1,
|
|
187
|
+
ttl: 300,
|
|
188
|
+
data: '142.250.64.100',
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
When records come back from `parseResponse().answers`, they are already in Format 2 — so you can push them directly into `response.answers` or store them in your cache and push them later. Either way works.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Supported record types
|
|
197
|
+
|
|
198
|
+
| Type | What it maps | Example data value |
|
|
199
|
+
|-------|---------------------------------------|---------------------------------------------------------|
|
|
200
|
+
| A | Domain → IPv4 address | `'192.168.1.10'` |
|
|
201
|
+
| AAAA | Domain → IPv6 address | `'::1'` |
|
|
202
|
+
| CNAME | Domain → another domain (alias) | `'app.myapp.local'` |
|
|
203
|
+
| NS | Domain → nameserver | `'ns1.myapp.local'` |
|
|
204
|
+
| MX | Domain → mail server | `{ priority: 10, exchange: 'mail.myapp.local' }` |
|
|
205
|
+
| TXT | Domain → text string(s) | `'v=spf1 ~all'` or `['string1', 'string2']` |
|
|
206
|
+
| PTR | Reverse IP → domain (reverse lookup) | `'myhost.local'` |
|
|
207
|
+
| SOA | Start of authority record | `{ mname, rname, serial, refresh, retry, expire, minimum }` |
|
|
208
|
+
|
|
209
|
+
The `type` field in `questions[0].type` will always be one of these strings — never a number. So your database keys can be readable strings like `'A'`, `'MX'`, `'TXT'` directly.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Using with MongoDB
|
|
214
|
+
|
|
215
|
+
```js
|
|
216
|
+
server.handle('onmessage', async (msg, send) => {
|
|
217
|
+
const request = parseRequest(msg);
|
|
218
|
+
const { name, type } = request.questions[0];
|
|
219
|
+
|
|
220
|
+
// check MongoDB
|
|
221
|
+
const record = await db.collection('records').findOne({ name, type });
|
|
222
|
+
if (record) {
|
|
223
|
+
const response = createResponseFromRequest(request);
|
|
224
|
+
response.answers.push(record.data);
|
|
225
|
+
return send(response);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// forward upstream and cache in MongoDB
|
|
229
|
+
const rawUpstream = await upstreamResponse(msg);
|
|
230
|
+
const parsed = parseResponse(rawUpstream);
|
|
231
|
+
for (const answer of parsed.answers) {
|
|
232
|
+
await db.collection('cache').insertOne({
|
|
233
|
+
name: answer.name,
|
|
234
|
+
type: answer.type,
|
|
235
|
+
data: answer.data,
|
|
236
|
+
expiresAt: new Date(Date.now() + answer.ttl * 1000),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
send(rawUpstream);
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Using with Redis
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
server.handle('onmessage', async (msg, send) => {
|
|
250
|
+
const request = parseRequest(msg);
|
|
251
|
+
const { name, type } = request.questions[0];
|
|
252
|
+
|
|
253
|
+
// check Redis
|
|
254
|
+
const cached = await redis.get(`${name}:${type}`);
|
|
255
|
+
if (cached) {
|
|
256
|
+
const response = createResponseFromRequest(request);
|
|
257
|
+
response.answers.push(JSON.parse(cached));
|
|
258
|
+
return send(response);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// forward upstream and cache in Redis
|
|
262
|
+
const rawUpstream = await upstreamResponse(msg);
|
|
263
|
+
const parsed = parseResponse(rawUpstream);
|
|
264
|
+
for (const answer of parsed.answers) {
|
|
265
|
+
await redis.set(
|
|
266
|
+
`${answer.name}:${answer.type}`,
|
|
267
|
+
JSON.stringify(answer),
|
|
268
|
+
{ EX: answer.ttl }
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
send(rawUpstream);
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Forwarding upstream
|
|
279
|
+
|
|
280
|
+
`upstreamResponse(msg)` takes the **original raw bytes** from the `onmessage` handler and sends them to an upstream DNS server. It returns raw bytes:
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
// default upstream is 8.8.8.8 (Google)
|
|
284
|
+
const rawUpstream = await upstreamResponse(msg);
|
|
285
|
+
|
|
286
|
+
// use a different upstream
|
|
287
|
+
const rawUpstream = await upstreamResponse(msg, '1.1.1.1');
|
|
288
|
+
|
|
289
|
+
// custom port and timeout
|
|
290
|
+
const rawUpstream = await upstreamResponse(msg, '1.1.1.1', 53, 3000);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The reason you pass `msg` (the raw bytes) and not the parsed request is important: the upstream server expects a DNS packet on the wire, not a JavaScript object. The raw bytes *are* that packet. Parsing and re-encoding would be unnecessary work — just forward the original bytes.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Parsing upstream responses
|
|
298
|
+
|
|
299
|
+
`parseResponse(rawUpstream)` parses what came back from upstream. It gives the same structure as `parseRequest`, but with `answers` filled in:
|
|
300
|
+
|
|
301
|
+
```js
|
|
302
|
+
const parsed = parseResponse(rawUpstream);
|
|
303
|
+
|
|
304
|
+
console.log(parsed.answers);
|
|
305
|
+
// [
|
|
306
|
+
// { name: 'google.com', type: 'A', class: 1, ttl: 300, data: '142.250.64.100' },
|
|
307
|
+
// { name: 'google.com', type: 'A', class: 1, ttl: 300, data: '142.250.64.102' },
|
|
308
|
+
// ]
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
This is how you read what the upstream server found, so you can store it in your cache with the correct TTL before sending the response.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## API reference
|
|
316
|
+
|
|
317
|
+
### `dns3.createServer()`
|
|
318
|
+
|
|
319
|
+
Creates a DNS server. Returns a server object with `handle()` and `listen()`.
|
|
320
|
+
|
|
321
|
+
### `server.handle(event, fn)`
|
|
322
|
+
|
|
323
|
+
Registers a handler for a server event.
|
|
324
|
+
|
|
325
|
+
- `'onmessage'` — fires for every incoming DNS query. Receives `(msg, send)`:
|
|
326
|
+
- `msg` — the raw incoming bytes (`Buffer`)
|
|
327
|
+
- `send(response)` — call this to reply. Accepts either a response object or a raw `Buffer`
|
|
328
|
+
- `'onerror'` — fires when an error occurs. Receives `(err)`
|
|
329
|
+
|
|
330
|
+
### `server.listen(port)`
|
|
331
|
+
|
|
332
|
+
Binds the server to a UDP port and starts listening.
|
|
333
|
+
|
|
334
|
+
### `parseRequest(msg)`
|
|
335
|
+
|
|
336
|
+
Parses a raw incoming DNS query buffer. Returns a structured object with `id`, `flags`, `questions`, `answers`, `authority`, `additional`.
|
|
337
|
+
|
|
338
|
+
### `parseResponse(buffer)`
|
|
339
|
+
|
|
340
|
+
Parses a raw DNS response buffer — either from upstream or your own encoded response. Same structure as `parseRequest` but with `answers` filled in.
|
|
341
|
+
|
|
342
|
+
### `createResponseFromRequest(request)`
|
|
343
|
+
|
|
344
|
+
Creates a response shell from a parsed request. Copies `id`, `flags`, and `questions`. Sets response flags (`qr: 1`, `ra: 1`). Returns an object with an empty `answers` array ready to fill.
|
|
345
|
+
|
|
346
|
+
### `upstreamResponse(msg, upstream, port, timeout)`
|
|
347
|
+
|
|
348
|
+
Forwards raw bytes to an upstream DNS server and returns raw bytes.
|
|
349
|
+
|
|
350
|
+
| Argument | Default | Description |
|
|
351
|
+
|-----------|-------------|--------------------------------|
|
|
352
|
+
| msg | required | Raw query Buffer |
|
|
353
|
+
| upstream | `'8.8.8.8'` | Upstream server IP |
|
|
354
|
+
| port | `53` | Upstream server port |
|
|
355
|
+
| timeout | `2000` | Timeout in milliseconds |
|
|
356
|
+
|
|
357
|
+
### `getRecord(type)`
|
|
358
|
+
|
|
359
|
+
Looks up a record type handler by name (`'A'`, `'MX'`, etc.) or numeric code. Returns `{ typeCode, typeName, encode, decode }` or `null`.
|
|
360
|
+
|
|
361
|
+
### `registerRecord(handler)`
|
|
362
|
+
|
|
363
|
+
Registers a custom record type handler. The handler must have `{ typeCode, typeName, encode, decode }`.
|
|
364
|
+
|
|
365
|
+
```js
|
|
366
|
+
const { registerRecord } = require('@gajanan_107/dns3');
|
|
367
|
+
|
|
368
|
+
registerRecord({
|
|
369
|
+
typeCode: 99,
|
|
370
|
+
typeName: 'MY',
|
|
371
|
+
encode(data) { return Buffer.from(data); },
|
|
372
|
+
decode(buf, offset, rdlength) { return buf.toString('ascii', offset, offset + rdlength); },
|
|
373
|
+
});
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Project structure
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
dns3/
|
|
382
|
+
├── index.js ← single entry point, all exports
|
|
383
|
+
├── packet/
|
|
384
|
+
│ ├── parser.js ← parseRequest, parseResponse
|
|
385
|
+
│ ├── encoder.js ← encodeResponse (used internally by send)
|
|
386
|
+
│ ├── header.js ← 12-byte DNS header read/write
|
|
387
|
+
│ ├── nameCodec.js ← DNS wire-format name encoding/decoding
|
|
388
|
+
│ └── records/
|
|
389
|
+
│ └── index.js ← A, AAAA, CNAME, NS, MX, TXT, PTR, SOA handlers
|
|
390
|
+
├── resolver/
|
|
391
|
+
│ ├── index.js ← createResponseFromRequest
|
|
392
|
+
│ └── forwarder.js ← upstreamResponse
|
|
393
|
+
└── server/
|
|
394
|
+
└── udpServer.js ← createServer, handle, listen
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## DNS flags reference
|
|
400
|
+
|
|
401
|
+
Every DNS packet carries a 16-bit flags field. dns3 breaks it into individual named bits so you can read or change each one:
|
|
402
|
+
|
|
403
|
+
| Flag | Bits | Meaning |
|
|
404
|
+
|----------|------|----------------------------------------------------------------------|
|
|
405
|
+
| `qr` | 1 | `0` = query (request), `1` = response |
|
|
406
|
+
| `opcode` | 4 | `0` = standard query. Other values are rare. |
|
|
407
|
+
| `aa` | 1 | Authoritative Answer — `1` if your server owns this domain |
|
|
408
|
+
| `tc` | 1 | Truncated — `1` if the response was cut short (too large for UDP) |
|
|
409
|
+
| `rd` | 1 | Recursion Desired — set by the client, copied to your response |
|
|
410
|
+
| `ra` | 1 | Recursion Available — set to `1` to tell the client you can recurse |
|
|
411
|
+
| `z` | 1 | Reserved, always `0` |
|
|
412
|
+
| `rcode` | 4 | `0` = success, `3` = NXDOMAIN (domain does not exist) |
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## RFC references
|
|
417
|
+
|
|
418
|
+
This package implements the core DNS wire format as defined in:
|
|
419
|
+
|
|
420
|
+
- [RFC 1034 — Domain Names: Concepts and Facilities](https://tools.ietf.org/html/rfc1034)
|
|
421
|
+
- [RFC 1035 — Domain Names: Implementation and Specification](https://tools.ietf.org/html/rfc1035)
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## License
|
|
426
|
+
|
|
427
|
+
MIT
|