@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.
Files changed (2) hide show
  1. package/README.md +427 -0
  2. 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gajanan_107/dns3",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {