@push.rocks/smartdns 7.5.1 → 7.8.0
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/dist_rust/rustdns-client_linux_amd64 +0 -0
- package/dist_rust/rustdns-client_linux_arm64 +0 -0
- package/dist_rust/rustdns_linux_amd64 +0 -0
- package/dist_rust/rustdns_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts_client/classes.dnsclient.d.ts +20 -31
- package/dist_ts_client/classes.dnsclient.js +126 -44
- package/dist_ts_client/classes.rustdnsclientbridge.d.ts +59 -0
- package/dist_ts_client/classes.rustdnsclientbridge.js +110 -0
- package/dist_ts_client/index.d.ts +1 -0
- package/dist_ts_client/index.js +2 -1
- package/dist_ts_client/plugins.d.ts +8 -1
- package/dist_ts_client/plugins.js +8 -2
- package/dist_ts_server/classes.dnsserver.d.ts +42 -54
- package/dist_ts_server/classes.dnsserver.js +290 -637
- package/dist_ts_server/classes.rustdnsbridge.d.ts +119 -0
- package/dist_ts_server/classes.rustdnsbridge.js +136 -0
- package/dist_ts_server/index.d.ts +1 -0
- package/dist_ts_server/index.js +2 -1
- package/dist_ts_server/plugins.d.ts +8 -7
- package/dist_ts_server/plugins.js +7 -8
- package/npmextra.json +18 -6
- package/package.json +10 -9
- package/readme.hints.md +28 -1
- package/readme.md +345 -645
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/readme.md +47 -0
package/readme.md
CHANGED
|
@@ -1,218 +1,253 @@
|
|
|
1
1
|
# @push.rocks/smartdns
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A TypeScript-first DNS toolkit powered by high-performance Rust binaries — covering everything from simple record lookups to running a full authoritative DNS server with DNSSEC, DNS-over-HTTPS, and automatic Let's Encrypt certificates.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Issue Reporting and Security
|
|
6
|
+
|
|
7
|
+
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## Install
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
|
-
pnpm install @push.rocks/smartdns
|
|
12
|
+
pnpm install @push.rocks/smartdns
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
## Architecture at a Glance 🏗️
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
npm install @push.rocks/smartdns --save
|
|
17
|
-
```
|
|
17
|
+
smartdns ships as **three entry points** that you can import independently:
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
| Entry point | What it does |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `@push.rocks/smartdns/client` | DNS resolution & record queries (UDP, DoH, system resolver) |
|
|
22
|
+
| `@push.rocks/smartdns/server` | Full DNS server — UDP, DoH, DNSSEC, ACME |
|
|
23
|
+
| `@push.rocks/smartdns` | Convenience re-export of both modules |
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
Both the **client** and the **server** delegate performance-critical work to compiled **Rust binaries** that ship with the package:
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
- **`rustdns`** — The server binary: network I/O, packet parsing, DNSSEC signing
|
|
28
|
+
- **`rustdns-client`** — The client binary: UDP wire-format queries, RFC 8484 DoH resolution
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
- **Server Module** (`@push.rocks/smartdns/server`): Full DNS server implementation with DNSSEC
|
|
27
|
-
- **Main Module** (`@push.rocks/smartdns`): Convenience exports for both client and server
|
|
30
|
+
TypeScript retains the public API, handler registration, ACME orchestration, and strategy routing. Communication between TypeScript and Rust happens over stdin/stdout JSON IPC via [`@push.rocks/smartrust`](https://code.foss.global/push.rocks/smartrust).
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
```
|
|
33
|
+
┌─────────────────────────┐
|
|
34
|
+
│ Your Application │
|
|
35
|
+
└────────┬────────────────┘
|
|
36
|
+
│
|
|
37
|
+
┌────────────────────┼─────────────────────┐
|
|
38
|
+
▼ ▼
|
|
39
|
+
┌──────────────────┐ ┌──────────────────┐
|
|
40
|
+
│ Smartdns Client │ │ DnsServer │
|
|
41
|
+
│ (TypeScript API) │ │ (TypeScript API) │
|
|
42
|
+
└────────┬─────────┘ └────────┬─────────┘
|
|
43
|
+
│ │
|
|
44
|
+
┌───────┼────────┐ ┌───────────┤
|
|
45
|
+
▼ ▼ ▼ ▼ ▼
|
|
46
|
+
system Rust Rust Rust binary TS Handlers
|
|
47
|
+
(Node) UDP DoH (rustdns) (minimatch)
|
|
48
|
+
│ │ │
|
|
49
|
+
▼ ▼ ▼
|
|
50
|
+
rustdns-client UDP / HTTPS listeners
|
|
51
|
+
(IPC binary) DNSSEC signing
|
|
52
|
+
```
|
|
30
53
|
|
|
31
|
-
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
### Quick Start
|
|
32
57
|
|
|
33
58
|
```typescript
|
|
34
|
-
//
|
|
59
|
+
// DNS client — resolve records
|
|
35
60
|
import { Smartdns } from '@push.rocks/smartdns/client';
|
|
36
61
|
|
|
37
|
-
|
|
38
|
-
|
|
62
|
+
const dns = new Smartdns({});
|
|
63
|
+
const records = await dns.getRecordsA('example.com');
|
|
64
|
+
console.log(records);
|
|
39
65
|
|
|
40
|
-
//
|
|
41
|
-
import {
|
|
42
|
-
const dnsClient = new dnsClientMod.Smartdns({});
|
|
43
|
-
const dnsServer = new dnsServerMod.DnsServer({ /* options */ });
|
|
44
|
-
```
|
|
66
|
+
// DNS server — serve records
|
|
67
|
+
import { DnsServer } from '@push.rocks/smartdns/server';
|
|
45
68
|
|
|
46
|
-
|
|
69
|
+
const server = new DnsServer({
|
|
70
|
+
udpPort: 5333,
|
|
71
|
+
httpsPort: 8443,
|
|
72
|
+
httpsKey: '...pem...',
|
|
73
|
+
httpsCert: '...pem...',
|
|
74
|
+
dnssecZone: 'example.com',
|
|
75
|
+
});
|
|
47
76
|
|
|
48
|
-
|
|
77
|
+
server.registerHandler('*.example.com', ['A'], (question) => ({
|
|
78
|
+
name: question.name,
|
|
79
|
+
type: 'A',
|
|
80
|
+
class: 'IN',
|
|
81
|
+
ttl: 300,
|
|
82
|
+
data: '192.168.1.100',
|
|
83
|
+
}));
|
|
49
84
|
|
|
50
|
-
|
|
85
|
+
await server.start();
|
|
86
|
+
```
|
|
51
87
|
|
|
52
|
-
|
|
88
|
+
Or import from the unified entry point:
|
|
53
89
|
|
|
54
90
|
```typescript
|
|
55
|
-
import {
|
|
91
|
+
import { dnsClientMod, dnsServerMod } from '@push.rocks/smartdns';
|
|
56
92
|
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
console.log(aRecords);
|
|
60
|
-
// Output: [{ name: 'example.com', type: 'A', dnsSecEnabled: false, value: '93.184.215.14' }]
|
|
93
|
+
const client = new dnsClientMod.Smartdns({});
|
|
94
|
+
const server = new dnsServerMod.DnsServer({ /* ... */ });
|
|
61
95
|
```
|
|
62
96
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
For resolving a domain to IPv6 addresses:
|
|
97
|
+
---
|
|
66
98
|
|
|
67
|
-
|
|
68
|
-
const aaaaRecords = await dnsClient.getRecordsAAAA('example.com');
|
|
69
|
-
console.log(aaaaRecords);
|
|
70
|
-
// Output: [{ name: 'example.com', type: 'AAAA', dnsSecEnabled: false, value: '2606:2800:21f:cb07:6820:80da:af6b:8b2c' }]
|
|
71
|
-
```
|
|
99
|
+
## 🔍 DNS Client
|
|
72
100
|
|
|
73
|
-
|
|
101
|
+
The `Smartdns` class resolves DNS records using a configurable strategy that combines the system resolver, raw UDP queries, and DNS-over-HTTPS — all backed by a Rust binary for the wire-format transports.
|
|
74
102
|
|
|
75
|
-
|
|
103
|
+
### Constructor Options
|
|
76
104
|
|
|
77
105
|
```typescript
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
106
|
+
interface ISmartDnsConstructorOptions {
|
|
107
|
+
strategy?: 'doh' | 'udp' | 'system' | 'prefer-system' | 'prefer-udp'; // default: 'prefer-system'
|
|
108
|
+
allowDohFallback?: boolean; // fallback to DoH when system fails (default: true)
|
|
109
|
+
timeoutMs?: number; // per-query timeout in milliseconds
|
|
110
|
+
}
|
|
81
111
|
```
|
|
82
112
|
|
|
83
|
-
|
|
113
|
+
### Resolution Strategies
|
|
114
|
+
|
|
115
|
+
| Strategy | Behavior |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `prefer-system` | 🏠 Try the OS resolver first, fall back to Rust DoH. Honors `/etc/hosts`. |
|
|
118
|
+
| `system` | 🏠 Use only the Node.js system resolver. No Rust binary needed. |
|
|
119
|
+
| `doh` | 🌐 Use only DNS-over-HTTPS (RFC 8484 wire format via Cloudflare). Rust-powered. |
|
|
120
|
+
| `udp` | ⚡ Use only raw UDP queries to upstream resolver (Cloudflare 1.1.1.1). Rust-powered. |
|
|
121
|
+
| `prefer-udp` | ⚡ Try Rust UDP first, fall back to Rust DoH if UDP fails. |
|
|
84
122
|
|
|
85
|
-
The client
|
|
123
|
+
The Rust binary (`rustdns-client`) is spawned **lazily** — only on the first query that needs it. This means `system`-only usage incurs zero Rust overhead.
|
|
124
|
+
|
|
125
|
+
### Querying Records
|
|
86
126
|
|
|
87
127
|
```typescript
|
|
88
|
-
|
|
89
|
-
|
|
128
|
+
const dns = new Smartdns({ strategy: 'prefer-udp' });
|
|
129
|
+
|
|
130
|
+
// Type-specific helpers
|
|
131
|
+
const aRecords = await dns.getRecordsA('example.com');
|
|
132
|
+
const aaaaRecords = await dns.getRecordsAAAA('example.com');
|
|
133
|
+
const txtRecords = await dns.getRecordsTxt('example.com');
|
|
90
134
|
|
|
91
|
-
// NS
|
|
92
|
-
const
|
|
135
|
+
// Generic query — supports A, AAAA, CNAME, MX, TXT, NS, SOA, PTR, SRV
|
|
136
|
+
const mxRecords = await dns.getRecords('example.com', 'MX');
|
|
93
137
|
|
|
94
|
-
//
|
|
95
|
-
const
|
|
138
|
+
// Nameserver lookup
|
|
139
|
+
const nameservers = await dns.getNameServers('example.com');
|
|
96
140
|
```
|
|
97
141
|
|
|
98
|
-
|
|
142
|
+
Every query returns an array of `IDnsRecord`:
|
|
99
143
|
|
|
100
|
-
|
|
144
|
+
```typescript
|
|
145
|
+
interface IDnsRecord {
|
|
146
|
+
name: string;
|
|
147
|
+
type: string; // 'A', 'AAAA', 'TXT', 'MX', etc.
|
|
148
|
+
dnsSecEnabled: boolean; // true if upstream AD flag was set
|
|
149
|
+
value: string;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
101
152
|
|
|
102
|
-
|
|
153
|
+
### DNSSEC Detection 🔐
|
|
154
|
+
|
|
155
|
+
When using `doh`, `udp`, or `prefer-udp` strategies, the Rust binary sends queries with the EDNS0 DO (DNSSEC OK) bit set and reports the AD (Authenticated Data) flag from the upstream response:
|
|
103
156
|
|
|
104
157
|
```typescript
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
158
|
+
const dns = new Smartdns({ strategy: 'udp' });
|
|
159
|
+
const records = await dns.getRecordsA('cloudflare.com');
|
|
160
|
+
console.log(records[0].dnsSecEnabled); // true — upstream validated DNSSEC
|
|
161
|
+
```
|
|
108
162
|
|
|
109
|
-
|
|
163
|
+
### Checking DNS Propagation
|
|
164
|
+
|
|
165
|
+
Wait for a specific record to appear — essential after making DNS changes:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
const propagated = await dns.checkUntilAvailable(
|
|
110
169
|
'example.com',
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
50,
|
|
114
|
-
500
|
|
170
|
+
'TXT',
|
|
171
|
+
'verification=abc123',
|
|
172
|
+
50, // max check cycles (default: 50)
|
|
173
|
+
500 // interval in ms (default: 500)
|
|
115
174
|
);
|
|
116
175
|
|
|
117
|
-
if (
|
|
118
|
-
console.log('
|
|
119
|
-
} else {
|
|
120
|
-
console.log('DNS propagation timeout - record not found.');
|
|
176
|
+
if (propagated) {
|
|
177
|
+
console.log('Record is live!');
|
|
121
178
|
}
|
|
122
179
|
```
|
|
123
180
|
|
|
124
|
-
|
|
181
|
+
The method alternates between system resolver and the configured strategy on each cycle for maximum coverage.
|
|
182
|
+
|
|
183
|
+
### Configuring the System DNS Provider
|
|
125
184
|
|
|
126
|
-
|
|
185
|
+
Override the global Node.js DNS resolver for all subsequent lookups:
|
|
127
186
|
|
|
128
187
|
```typescript
|
|
129
|
-
// Import the standalone function
|
|
130
188
|
import { makeNodeProcessUseDnsProvider } from '@push.rocks/smartdns/client';
|
|
131
189
|
|
|
132
|
-
//
|
|
133
|
-
makeNodeProcessUseDnsProvider('
|
|
134
|
-
|
|
135
|
-
// Or use Google DNS
|
|
136
|
-
makeNodeProcessUseDnsProvider('google');
|
|
190
|
+
makeNodeProcessUseDnsProvider('cloudflare'); // 1.1.1.1 / 1.0.0.1
|
|
191
|
+
makeNodeProcessUseDnsProvider('google'); // 8.8.8.8 / 8.8.4.4
|
|
137
192
|
```
|
|
138
193
|
|
|
139
|
-
###
|
|
140
|
-
|
|
141
|
-
#### DNS-Based Feature Flagging
|
|
194
|
+
### Cleanup
|
|
142
195
|
|
|
143
|
-
|
|
196
|
+
When you're done with a `Smartdns` instance (especially one using Rust strategies), call `destroy()` to kill the Rust child process:
|
|
144
197
|
|
|
145
198
|
```typescript
|
|
146
|
-
const
|
|
147
|
-
|
|
199
|
+
const dns = new Smartdns({ strategy: 'udp' });
|
|
200
|
+
// ... do queries ...
|
|
201
|
+
dns.destroy(); // kills rustdns-client process
|
|
202
|
+
```
|
|
148
203
|
|
|
149
|
-
|
|
150
|
-
// Parse TXT records like "feature-dark-mode=true"
|
|
151
|
-
const [feature, enabled] = record.value.split('=');
|
|
152
|
-
featureFlags[feature] = enabled === 'true';
|
|
153
|
-
});
|
|
204
|
+
---
|
|
154
205
|
|
|
155
|
-
|
|
156
|
-
console.log('Dark mode is enabled!');
|
|
157
|
-
}
|
|
158
|
-
```
|
|
206
|
+
## 🖥️ DNS Server
|
|
159
207
|
|
|
160
|
-
|
|
208
|
+
The `DnsServer` class runs a production-capable authoritative DNS server backed by a Rust binary. It supports standard UDP DNS (port 53), DNS-over-HTTPS, DNSSEC signing, and automated Let's Encrypt certificates.
|
|
161
209
|
|
|
162
|
-
|
|
210
|
+
### Server Options
|
|
163
211
|
|
|
164
212
|
```typescript
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
213
|
+
interface IDnsServerOptions {
|
|
214
|
+
udpPort: number; // Port for UDP DNS queries
|
|
215
|
+
httpsPort: number; // Port for DNS-over-HTTPS
|
|
216
|
+
httpsKey: string; // PEM private key (path or content)
|
|
217
|
+
httpsCert: string; // PEM certificate (path or content)
|
|
218
|
+
dnssecZone: string; // Zone for DNSSEC signing
|
|
219
|
+
primaryNameserver?: string; // SOA mname field (default: 'ns1.{dnssecZone}')
|
|
220
|
+
udpBindInterface?: string; // IP to bind UDP (default: '0.0.0.0')
|
|
221
|
+
httpsBindInterface?: string; // IP to bind HTTPS (default: '0.0.0.0')
|
|
222
|
+
manualUdpMode?: boolean; // Don't auto-bind UDP socket
|
|
223
|
+
manualHttpsMode?: boolean; // Don't auto-bind HTTPS server
|
|
224
|
+
enableLocalhostHandling?: boolean; // RFC 6761 localhost (default: true)
|
|
225
|
+
}
|
|
172
226
|
```
|
|
173
227
|
|
|
174
|
-
###
|
|
175
|
-
|
|
176
|
-
The `DnsServer` class provides a full-featured DNS server with support for UDP, DNS-over-HTTPS (DoH), DNSSEC, and automatic SSL certificate management via Let's Encrypt.
|
|
177
|
-
|
|
178
|
-
#### Basic DNS Server Setup
|
|
179
|
-
|
|
180
|
-
Create a simple DNS server that responds to queries:
|
|
228
|
+
### Basic Server
|
|
181
229
|
|
|
182
230
|
```typescript
|
|
183
231
|
import { DnsServer } from '@push.rocks/smartdns/server';
|
|
184
232
|
|
|
185
|
-
const
|
|
186
|
-
udpPort: 5333,
|
|
187
|
-
httpsPort: 8443,
|
|
188
|
-
httpsKey: '
|
|
189
|
-
httpsCert: '
|
|
190
|
-
dnssecZone: 'example.com' // Optional: enable DNSSEC for this zone
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// For enhanced security, bind to specific interfaces
|
|
194
|
-
const secureServer = new DnsServer({
|
|
195
|
-
udpPort: 53,
|
|
196
|
-
httpsPort: 443,
|
|
197
|
-
httpsKey: 'path/to/key.pem',
|
|
198
|
-
httpsCert: 'path/to/cert.pem',
|
|
233
|
+
const server = new DnsServer({
|
|
234
|
+
udpPort: 5333,
|
|
235
|
+
httpsPort: 8443,
|
|
236
|
+
httpsKey: '...pem...',
|
|
237
|
+
httpsCert: '...pem...',
|
|
199
238
|
dnssecZone: 'example.com',
|
|
200
|
-
udpBindInterface: '127.0.0.1', // Bind UDP to localhost only
|
|
201
|
-
httpsBindInterface: '127.0.0.1', // Bind HTTPS to localhost only
|
|
202
|
-
primaryNameserver: 'ns1.example.com' // Optional: primary nameserver for SOA records (defaults to ns1.{dnssecZone})
|
|
203
239
|
});
|
|
204
240
|
|
|
205
|
-
// Register
|
|
206
|
-
|
|
241
|
+
// Register handlers
|
|
242
|
+
server.registerHandler('example.com', ['A'], (question) => ({
|
|
207
243
|
name: question.name,
|
|
208
244
|
type: 'A',
|
|
209
245
|
class: 'IN',
|
|
210
246
|
ttl: 300,
|
|
211
|
-
data: '
|
|
247
|
+
data: '93.184.215.14',
|
|
212
248
|
}));
|
|
213
249
|
|
|
214
|
-
|
|
215
|
-
dnsServer.registerHandler('example.com', ['TXT'], (question) => ({
|
|
250
|
+
server.registerHandler('example.com', ['TXT'], (question) => ({
|
|
216
251
|
name: question.name,
|
|
217
252
|
type: 'TXT',
|
|
218
253
|
class: 'IN',
|
|
@@ -220,643 +255,308 @@ dnsServer.registerHandler('example.com', ['TXT'], (question) => ({
|
|
|
220
255
|
data: 'v=spf1 include:_spf.example.com ~all',
|
|
221
256
|
}));
|
|
222
257
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
console.log('DNS Server started!');
|
|
258
|
+
await server.start();
|
|
259
|
+
// DNS Server started (UDP: 0.0.0.0:5333, HTTPS: 0.0.0.0:8443)
|
|
226
260
|
```
|
|
227
261
|
|
|
228
|
-
###
|
|
262
|
+
### Handler System 🎯
|
|
229
263
|
|
|
230
|
-
|
|
264
|
+
Handlers use **glob patterns** (via `minimatch`) to match incoming query names. Multiple handlers can contribute records to the same response.
|
|
231
265
|
|
|
232
266
|
```typescript
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
httpsPort: 443,
|
|
236
|
-
httpsKey: 'path/to/key.pem',
|
|
237
|
-
httpsCert: 'path/to/cert.pem',
|
|
238
|
-
dnssecZone: 'example.com',
|
|
239
|
-
primaryNameserver: 'ns1.example.com' // Specify your actual primary nameserver
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// Without primaryNameserver, the SOA mname defaults to 'ns1.{dnssecZone}'
|
|
243
|
-
// In this case, it would be 'ns1.example.com'
|
|
244
|
-
|
|
245
|
-
// The automatic SOA record includes:
|
|
246
|
-
// - mname: Primary nameserver (from primaryNameserver option)
|
|
247
|
-
// - rname: Responsible person email (hostmaster.{dnssecZone})
|
|
248
|
-
// - serial: Unix timestamp
|
|
249
|
-
// - refresh: 3600 (1 hour)
|
|
250
|
-
// - retry: 600 (10 minutes)
|
|
251
|
-
// - expire: 604800 (7 days)
|
|
252
|
-
// - minimum: 86400 (1 day)
|
|
253
|
-
```
|
|
267
|
+
// Exact domain
|
|
268
|
+
server.registerHandler('example.com', ['A'], handler);
|
|
254
269
|
|
|
255
|
-
|
|
270
|
+
// All subdomains
|
|
271
|
+
server.registerHandler('*.example.com', ['A'], handler);
|
|
256
272
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
dnssecZone: 'secure.example.com', // Enable DNSSEC for this zone
|
|
273
|
+
// Specific pattern
|
|
274
|
+
server.registerHandler('db-*.internal.example.com', ['A'], (question) => {
|
|
275
|
+
const id = question.name.match(/db-(\d+)/)?.[1];
|
|
276
|
+
return {
|
|
277
|
+
name: question.name,
|
|
278
|
+
type: 'A',
|
|
279
|
+
class: 'IN',
|
|
280
|
+
ttl: 60,
|
|
281
|
+
data: `10.0.1.${id}`,
|
|
282
|
+
};
|
|
268
283
|
});
|
|
269
284
|
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
// 2. Creates DS records for parent zone delegation
|
|
273
|
-
// 3. Signs all responses with RRSIG records
|
|
274
|
-
// 4. Provides NSEC records for authenticated denial of existence
|
|
275
|
-
|
|
276
|
-
// Register your handlers as normal - DNSSEC signing is automatic
|
|
277
|
-
dnsServer.registerHandler('secure.example.com', ['A'], (question) => ({
|
|
285
|
+
// Catch-all
|
|
286
|
+
server.registerHandler('*', ['A'], (question) => ({
|
|
278
287
|
name: question.name,
|
|
279
288
|
type: 'A',
|
|
280
289
|
class: 'IN',
|
|
281
290
|
ttl: 300,
|
|
282
|
-
data: '
|
|
291
|
+
data: '127.0.0.1',
|
|
283
292
|
}));
|
|
284
293
|
|
|
285
|
-
|
|
294
|
+
// Multiple record types
|
|
295
|
+
server.registerHandler('example.com', ['MX'], (question) => ({
|
|
296
|
+
name: question.name,
|
|
297
|
+
type: 'MX',
|
|
298
|
+
class: 'IN',
|
|
299
|
+
ttl: 300,
|
|
300
|
+
data: { preference: 10, exchange: 'mail.example.com' },
|
|
301
|
+
}));
|
|
286
302
|
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
const client = new Smartdns({});
|
|
290
|
-
const dnskeyRecords = await client.getRecords('secure.example.com', 'DNSKEY');
|
|
291
|
-
const dsRecords = await client.getRecords('secure.example.com', 'DS');
|
|
303
|
+
// Unregister a handler
|
|
304
|
+
server.unregisterHandler('example.com', ['A']);
|
|
292
305
|
```
|
|
293
306
|
|
|
294
|
-
|
|
307
|
+
When no handler matches, the server automatically returns an **SOA record** for the zone.
|
|
295
308
|
|
|
296
|
-
|
|
297
|
-
- **ECDSAP256SHA256** (Algorithm 13) - Default, using P-256 curve
|
|
298
|
-
- **ED25519** (Algorithm 15) - Modern elliptic curve algorithm
|
|
299
|
-
- **RSASHA256** (Algorithm 8) - RSA-based signatures
|
|
309
|
+
### DNSSEC ✅
|
|
300
310
|
|
|
301
|
-
|
|
311
|
+
DNSSEC is enabled automatically when you set the `dnssecZone` option. The Rust backend handles:
|
|
302
312
|
|
|
303
|
-
|
|
313
|
+
- **Key generation** — ECDSA P-256 (algorithm 13) by default
|
|
314
|
+
- **DNSKEY / DS record** generation
|
|
315
|
+
- **RRSIG signing** for all responses
|
|
316
|
+
- **NSEC records** for authenticated denial of existence
|
|
304
317
|
|
|
305
318
|
```typescript
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const dnsServer = new DnsServer({
|
|
319
|
+
const server = new DnsServer({
|
|
309
320
|
udpPort: 53,
|
|
310
321
|
httpsPort: 443,
|
|
311
|
-
httpsKey: '
|
|
312
|
-
httpsCert: '
|
|
322
|
+
httpsKey: '...',
|
|
323
|
+
httpsCert: '...',
|
|
324
|
+
dnssecZone: 'secure.example.com',
|
|
313
325
|
});
|
|
314
326
|
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
if (result.success) {
|
|
326
|
-
console.log('Certificate retrieved successfully!');
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// The server automatically:
|
|
330
|
-
// 1. Handles ACME DNS-01 challenges
|
|
331
|
-
// 2. Creates temporary TXT records for domain validation
|
|
332
|
-
// 3. Retrieves and installs the certificate
|
|
333
|
-
// 4. Restarts the HTTPS server with the new certificate
|
|
327
|
+
// Just register handlers as usual — signing is automatic
|
|
328
|
+
server.registerHandler('secure.example.com', ['A'], (q) => ({
|
|
329
|
+
name: q.name,
|
|
330
|
+
type: 'A',
|
|
331
|
+
class: 'IN',
|
|
332
|
+
ttl: 300,
|
|
333
|
+
data: '10.0.0.1',
|
|
334
|
+
}));
|
|
334
335
|
|
|
335
|
-
await
|
|
336
|
-
console.log('DNS Server with Let\'s Encrypt SSL started!');
|
|
336
|
+
await server.start();
|
|
337
337
|
```
|
|
338
338
|
|
|
339
|
-
|
|
339
|
+
Supported algorithms: **ECDSAP256SHA256** (13), **ED25519** (15), **RSASHA256** (8).
|
|
340
340
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
#### Configuration Options
|
|
344
|
-
|
|
345
|
-
```typescript
|
|
346
|
-
export interface IDnsServerOptions {
|
|
347
|
-
httpsKey: string; // Path or content of HTTPS private key
|
|
348
|
-
httpsCert: string; // Path or content of HTTPS certificate
|
|
349
|
-
httpsPort: number; // Port for DNS-over-HTTPS
|
|
350
|
-
udpPort: number; // Port for standard UDP DNS
|
|
351
|
-
dnssecZone: string; // Zone name for DNSSEC signing
|
|
352
|
-
udpBindInterface?: string; // IP address to bind UDP socket (default: '0.0.0.0')
|
|
353
|
-
httpsBindInterface?: string; // IP address to bind HTTPS server (default: '0.0.0.0')
|
|
354
|
-
manualUdpMode?: boolean; // Handle UDP sockets manually
|
|
355
|
-
manualHttpsMode?: boolean; // Handle HTTPS sockets manually
|
|
356
|
-
primaryNameserver?: string; // Primary nameserver for SOA records (default: 'ns1.{dnssecZone}')
|
|
357
|
-
}
|
|
358
|
-
```
|
|
341
|
+
### SOA Records
|
|
359
342
|
|
|
360
|
-
|
|
343
|
+
The server auto-generates SOA records for zones when no specific handler matches. Customize the primary nameserver:
|
|
361
344
|
|
|
362
345
|
```typescript
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
import * as net from 'net';
|
|
366
|
-
|
|
367
|
-
// Create server with manual UDP mode
|
|
368
|
-
const dnsServer = new DnsServer({
|
|
369
|
-
httpsKey: '...',
|
|
370
|
-
httpsCert: '...',
|
|
371
|
-
httpsPort: 853,
|
|
372
|
-
udpPort: 53,
|
|
346
|
+
const server = new DnsServer({
|
|
347
|
+
// ...
|
|
373
348
|
dnssecZone: 'example.com',
|
|
374
|
-
|
|
349
|
+
primaryNameserver: 'ns1.example.com', // defaults to 'ns1.{dnssecZone}'
|
|
375
350
|
});
|
|
376
351
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
// Handle incoming UDP messages
|
|
383
|
-
udpSocket.on('message', (msg, rinfo) => {
|
|
384
|
-
dnsServer.handleUdpMessage(msg, rinfo, (response, responseRinfo) => {
|
|
385
|
-
// Send response using your socket
|
|
386
|
-
udpSocket.send(response, responseRinfo.port, responseRinfo.address);
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
// Bind to custom port or multiple interfaces
|
|
391
|
-
udpSocket.bind(5353, '0.0.0.0');
|
|
352
|
+
// Generated SOA includes:
|
|
353
|
+
// mname: ns1.example.com
|
|
354
|
+
// rname: hostmaster.example.com
|
|
355
|
+
// serial: Unix timestamp
|
|
356
|
+
// refresh: 3600, retry: 600, expire: 604800, minimum: 86400
|
|
392
357
|
```
|
|
393
358
|
|
|
394
|
-
|
|
359
|
+
### Let's Encrypt Integration 🔒
|
|
360
|
+
|
|
361
|
+
Built-in ACME DNS-01 challenge support for automatic SSL certificates:
|
|
395
362
|
|
|
396
363
|
```typescript
|
|
397
|
-
|
|
398
|
-
const dnsServer = new DnsServer({
|
|
399
|
-
httpsKey: '...',
|
|
400
|
-
httpsCert: '...',
|
|
401
|
-
httpsPort: 853,
|
|
364
|
+
const server = new DnsServer({
|
|
402
365
|
udpPort: 53,
|
|
366
|
+
httpsPort: 443,
|
|
367
|
+
httpsKey: '/path/to/key.pem',
|
|
368
|
+
httpsCert: '/path/to/cert.pem',
|
|
403
369
|
dnssecZone: 'example.com',
|
|
404
|
-
manualHttpsMode: true // HTTPS manual, UDP automatic
|
|
405
370
|
});
|
|
406
371
|
|
|
407
|
-
await
|
|
372
|
+
await server.start();
|
|
408
373
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
374
|
+
const result = await server.retrieveSslCertificate(
|
|
375
|
+
['example.com', 'www.example.com'],
|
|
376
|
+
{
|
|
377
|
+
email: 'admin@example.com',
|
|
378
|
+
staging: false,
|
|
379
|
+
certDir: './certs',
|
|
380
|
+
}
|
|
381
|
+
);
|
|
414
382
|
|
|
415
|
-
|
|
383
|
+
if (result.success) {
|
|
384
|
+
console.log('Certificate installed!');
|
|
385
|
+
// The server automatically:
|
|
386
|
+
// 1. Registers temporary _acme-challenge TXT handlers
|
|
387
|
+
// 2. Completes DNS-01 validation
|
|
388
|
+
// 3. Updates the HTTPS server with the new cert
|
|
389
|
+
// 4. Cleans up challenge handlers
|
|
390
|
+
}
|
|
416
391
|
```
|
|
417
392
|
|
|
418
|
-
|
|
393
|
+
### Interface Binding
|
|
419
394
|
|
|
420
|
-
|
|
395
|
+
Restrict the server to specific network interfaces:
|
|
421
396
|
|
|
422
397
|
```typescript
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
dnssecZone: 'example.com',
|
|
429
|
-
manualUdpMode: true,
|
|
430
|
-
manualHttpsMode: true
|
|
398
|
+
// Localhost only — great for development
|
|
399
|
+
const server = new DnsServer({
|
|
400
|
+
// ...
|
|
401
|
+
udpBindInterface: '127.0.0.1',
|
|
402
|
+
httpsBindInterface: '127.0.0.1',
|
|
431
403
|
});
|
|
432
404
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
//
|
|
436
|
-
|
|
405
|
+
// Different interfaces per protocol
|
|
406
|
+
const server = new DnsServer({
|
|
407
|
+
// ...
|
|
408
|
+
udpBindInterface: '192.168.1.100',
|
|
409
|
+
httpsBindInterface: '10.0.0.50',
|
|
410
|
+
});
|
|
437
411
|
```
|
|
438
412
|
|
|
439
|
-
|
|
413
|
+
### Manual Socket Handling 🔧
|
|
440
414
|
|
|
441
|
-
|
|
415
|
+
For clustering, load balancing, or custom transports, take control of socket management:
|
|
442
416
|
|
|
443
417
|
```typescript
|
|
444
|
-
// Create multiple UDP sockets for different CPU cores
|
|
445
|
-
const sockets = [];
|
|
446
|
-
const numCPUs = require('os').cpus().length;
|
|
447
|
-
|
|
448
|
-
for (let i = 0; i < numCPUs; i++) {
|
|
449
|
-
const socket = dgram.createSocket({
|
|
450
|
-
type: 'udp4',
|
|
451
|
-
reuseAddr: true // Allow multiple sockets on same port
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
socket.on('message', (msg, rinfo) => {
|
|
455
|
-
dnsServer.handleUdpMessage(msg, rinfo, (response, rinfo) => {
|
|
456
|
-
socket.send(response, rinfo.port, rinfo.address);
|
|
457
|
-
});
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
socket.bind(53);
|
|
461
|
-
sockets.push(socket);
|
|
462
|
-
}
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
##### Clustering with Worker Processes
|
|
466
|
-
|
|
467
|
-
```typescript
|
|
468
|
-
import cluster from 'cluster';
|
|
469
418
|
import { DnsServer } from '@push.rocks/smartdns/server';
|
|
419
|
+
import * as dgram from 'dgram';
|
|
470
420
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
// Distribute connections to workers
|
|
476
|
-
server.on('connection', (socket) => {
|
|
477
|
-
const worker = getNextWorker(); // Round-robin or custom logic
|
|
478
|
-
worker.send('socket', socket);
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
server.listen(853);
|
|
482
|
-
} else {
|
|
483
|
-
// Worker process handles DNS
|
|
484
|
-
const dnsServer = new DnsServer({
|
|
485
|
-
httpsKey: '...',
|
|
486
|
-
httpsCert: '...',
|
|
487
|
-
httpsPort: 853,
|
|
488
|
-
udpPort: 53,
|
|
489
|
-
dnssecZone: 'example.com',
|
|
490
|
-
manualHttpsMode: true
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
process.on('message', (msg, socket) => {
|
|
494
|
-
if (msg === 'socket') {
|
|
495
|
-
dnsServer.handleHttpsSocket(socket);
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
await dnsServer.start();
|
|
500
|
-
}
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
##### Custom Transport Protocol
|
|
504
|
-
|
|
505
|
-
```typescript
|
|
506
|
-
// Use DNS server with custom transport (e.g., WebSocket)
|
|
507
|
-
import WebSocket from 'ws';
|
|
508
|
-
|
|
509
|
-
const wss = new WebSocket.Server({ port: 8080 });
|
|
510
|
-
const dnsServer = new DnsServer({
|
|
511
|
-
httpsKey: '...',
|
|
512
|
-
httpsCert: '...',
|
|
513
|
-
httpsPort: 853,
|
|
514
|
-
udpPort: 53,
|
|
515
|
-
dnssecZone: 'example.com',
|
|
421
|
+
// Manual UDP mode — you control the socket
|
|
422
|
+
const server = new DnsServer({
|
|
423
|
+
// ...
|
|
516
424
|
manualUdpMode: true,
|
|
517
|
-
manualHttpsMode: true
|
|
518
425
|
});
|
|
519
426
|
|
|
520
|
-
await
|
|
427
|
+
await server.start(); // HTTPS auto-binds, UDP does not
|
|
521
428
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
ws.send(response);
|
|
429
|
+
const socket = dgram.createSocket('udp4');
|
|
430
|
+
socket.on('message', (msg, rinfo) => {
|
|
431
|
+
server.handleUdpMessage(msg, rinfo, (response, responseRinfo) => {
|
|
432
|
+
socket.send(response, responseRinfo.port, responseRinfo.address);
|
|
527
433
|
});
|
|
528
434
|
});
|
|
435
|
+
socket.bind(5353);
|
|
529
436
|
```
|
|
530
437
|
|
|
531
|
-
|
|
438
|
+
Full manual mode (both protocols):
|
|
532
439
|
|
|
533
440
|
```typescript
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
{ address: '::1', type: 'udp6' }
|
|
539
|
-
];
|
|
540
|
-
|
|
541
|
-
interfaces.forEach(({ address, type }) => {
|
|
542
|
-
const socket = dgram.createSocket(type);
|
|
543
|
-
|
|
544
|
-
socket.on('message', (msg, rinfo) => {
|
|
545
|
-
console.log(`Query received on ${address}`);
|
|
546
|
-
dnsServer.handleUdpMessage(msg, rinfo, (response, rinfo) => {
|
|
547
|
-
socket.send(response, rinfo.port, rinfo.address);
|
|
548
|
-
});
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
socket.bind(53, address);
|
|
441
|
+
const server = new DnsServer({
|
|
442
|
+
// ...
|
|
443
|
+
manualUdpMode: true,
|
|
444
|
+
manualHttpsMode: true,
|
|
552
445
|
});
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
### Handling Different Protocols
|
|
556
446
|
|
|
557
|
-
|
|
447
|
+
await server.start(); // Neither protocol binds automatically
|
|
448
|
+
```
|
|
558
449
|
|
|
559
|
-
|
|
450
|
+
Process individual DNS packets directly:
|
|
560
451
|
|
|
561
452
|
```typescript
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const dnsServer = new DnsServer({
|
|
566
|
-
udpPort: 5353, // Using alternate port for testing
|
|
567
|
-
httpsPort: 8443,
|
|
568
|
-
httpsKey: fs.readFileSync('/path/to/key.pem', 'utf8'),
|
|
569
|
-
httpsCert: fs.readFileSync('/path/to/cert.pem', 'utf8'),
|
|
570
|
-
dnssecZone: 'test.local' // Optional
|
|
571
|
-
});
|
|
453
|
+
// Synchronous (TypeScript fallback)
|
|
454
|
+
const response = server.processRawDnsPacket(packetBuffer);
|
|
572
455
|
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
name: question.name,
|
|
576
|
-
type: 'A',
|
|
577
|
-
class: 'IN',
|
|
578
|
-
ttl: 60,
|
|
579
|
-
data: '127.0.0.1',
|
|
580
|
-
}));
|
|
581
|
-
|
|
582
|
-
await dnsServer.start();
|
|
583
|
-
|
|
584
|
-
// Test with dig or nslookup:
|
|
585
|
-
// dig @localhost -p 5353 test.local
|
|
456
|
+
// Asynchronous (via Rust bridge — includes DNSSEC signing)
|
|
457
|
+
const response = await server.processRawDnsPacketAsync(packetBuffer);
|
|
586
458
|
```
|
|
587
459
|
|
|
588
|
-
####
|
|
589
|
-
|
|
590
|
-
Provide encrypted DNS queries over HTTPS:
|
|
460
|
+
#### Load Balancing Example
|
|
591
461
|
|
|
592
462
|
```typescript
|
|
593
|
-
import
|
|
594
|
-
import * as
|
|
595
|
-
|
|
596
|
-
const dnsServer = new DnsServer({
|
|
597
|
-
httpsPort: 8443,
|
|
598
|
-
httpsKey: fs.readFileSync('/path/to/key.pem', 'utf8'),
|
|
599
|
-
httpsCert: fs.readFileSync('/path/to/cert.pem', 'utf8'),
|
|
600
|
-
});
|
|
463
|
+
import * as dgram from 'dgram';
|
|
464
|
+
import * as os from 'os';
|
|
601
465
|
|
|
602
|
-
|
|
603
|
-
// - DNS wire format in POST body
|
|
604
|
-
// - Proper Content-Type headers (application/dns-message)
|
|
605
|
-
// - Base64url encoding for GET requests
|
|
466
|
+
const numCPUs = os.cpus().length;
|
|
606
467
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
type: 'A',
|
|
610
|
-
class: 'IN',
|
|
611
|
-
ttl: 300,
|
|
612
|
-
data: '10.0.0.1',
|
|
613
|
-
}));
|
|
468
|
+
for (let i = 0; i < numCPUs; i++) {
|
|
469
|
+
const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
614
470
|
|
|
615
|
-
|
|
471
|
+
socket.on('message', (msg, rinfo) => {
|
|
472
|
+
server.handleUdpMessage(msg, rinfo, (response, rinfo) => {
|
|
473
|
+
socket.send(response, rinfo.port, rinfo.address);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
616
476
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
// --data-binary @query.bin \
|
|
620
|
-
// https://localhost:8443/dns-query
|
|
477
|
+
socket.bind(53);
|
|
478
|
+
}
|
|
621
479
|
```
|
|
622
480
|
|
|
623
|
-
###
|
|
624
|
-
|
|
625
|
-
For enhanced security and network isolation, you can bind the DNS server to specific network interfaces instead of all available interfaces.
|
|
626
|
-
|
|
627
|
-
#### Localhost-Only Binding
|
|
628
|
-
|
|
629
|
-
Bind to localhost for development or local-only DNS services:
|
|
481
|
+
### Stopping the Server
|
|
630
482
|
|
|
631
483
|
```typescript
|
|
632
|
-
|
|
633
|
-
udpPort: 5353,
|
|
634
|
-
httpsPort: 8443,
|
|
635
|
-
httpsKey: cert.key,
|
|
636
|
-
httpsCert: cert.cert,
|
|
637
|
-
dnssecZone: 'local.test',
|
|
638
|
-
udpBindInterface: '127.0.0.1', // IPv4 localhost
|
|
639
|
-
httpsBindInterface: '127.0.0.1'
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
// Or use IPv6 localhost
|
|
643
|
-
const ipv6LocalServer = new DnsServer({
|
|
644
|
-
// ... other options
|
|
645
|
-
udpBindInterface: '::1', // IPv6 localhost
|
|
646
|
-
httpsBindInterface: '::1'
|
|
647
|
-
});
|
|
484
|
+
await server.stop();
|
|
648
485
|
```
|
|
649
486
|
|
|
650
|
-
|
|
487
|
+
This gracefully shuts down the Rust process and releases all bound sockets.
|
|
651
488
|
|
|
652
|
-
|
|
489
|
+
---
|
|
653
490
|
|
|
654
|
-
|
|
655
|
-
const interfaceServer = new DnsServer({
|
|
656
|
-
udpPort: 53,
|
|
657
|
-
httpsPort: 443,
|
|
658
|
-
httpsKey: cert.key,
|
|
659
|
-
httpsCert: cert.cert,
|
|
660
|
-
dnssecZone: 'example.com',
|
|
661
|
-
udpBindInterface: '192.168.1.100', // Specific internal interface
|
|
662
|
-
httpsBindInterface: '10.0.0.50' // Different interface for HTTPS
|
|
663
|
-
});
|
|
664
|
-
```
|
|
491
|
+
## 🦀 Rust Crate Structure
|
|
665
492
|
|
|
666
|
-
|
|
493
|
+
The Rust workspace (`rust/crates/`) contains five crates:
|
|
667
494
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
-
|
|
495
|
+
| Crate | Purpose |
|
|
496
|
+
|---|---|
|
|
497
|
+
| `rustdns` | Server binary — IPC management loop, handler callback routing |
|
|
498
|
+
| `rustdns-client` | Client binary — stateless UDP/DoH query proxy |
|
|
499
|
+
| `rustdns-protocol` | DNS wire format parsing, encoding, and RDATA decode/encode |
|
|
500
|
+
| `rustdns-server` | Async UDP + HTTPS servers (tokio, hyper, rustls) |
|
|
501
|
+
| `rustdns-dnssec` | ECDSA/ED25519 key generation and RRset signing |
|
|
672
502
|
|
|
673
|
-
|
|
503
|
+
Pre-compiled binaries for `linux_amd64` and `linux_arm64` are included in `dist_rust/`. Cross-compilation is handled by [`@git.zone/tsrust`](https://code.foss.global/git.zone/tsrust).
|
|
674
504
|
|
|
675
|
-
|
|
505
|
+
---
|
|
676
506
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
```typescript
|
|
680
|
-
// Match all subdomains
|
|
681
|
-
dnsServer.registerHandler('*.example.com', ['A'], (question) => {
|
|
682
|
-
// Extract subdomain
|
|
683
|
-
const subdomain = question.name.replace('.example.com', '');
|
|
684
|
-
|
|
685
|
-
// Dynamic response based on subdomain
|
|
686
|
-
return {
|
|
687
|
-
name: question.name,
|
|
688
|
-
type: 'A',
|
|
689
|
-
class: 'IN',
|
|
690
|
-
ttl: 300,
|
|
691
|
-
data: subdomain === 'api' ? '10.0.0.10' : '10.0.0.1',
|
|
692
|
-
};
|
|
693
|
-
});
|
|
507
|
+
## 🧪 Testing
|
|
694
508
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
return {
|
|
699
|
-
name: question.name,
|
|
700
|
-
type: 'A',
|
|
701
|
-
class: 'IN',
|
|
702
|
-
ttl: 60,
|
|
703
|
-
data: `10.0.1.${instanceId}`,
|
|
704
|
-
};
|
|
705
|
-
});
|
|
509
|
+
```bash
|
|
510
|
+
# Run all tests
|
|
511
|
+
pnpm test
|
|
706
512
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
type: 'A',
|
|
711
|
-
class: 'IN',
|
|
712
|
-
ttl: 300,
|
|
713
|
-
data: '127.0.0.1',
|
|
714
|
-
}));
|
|
513
|
+
# Run specific test file
|
|
514
|
+
tstest test/test.client.ts --verbose
|
|
515
|
+
tstest test/test.server.ts --verbose
|
|
715
516
|
```
|
|
716
517
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
The library uses `@git.zone/tstest` for testing. Here's an example of comprehensive tests:
|
|
518
|
+
Example test:
|
|
720
519
|
|
|
721
520
|
```typescript
|
|
722
521
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
723
522
|
import { Smartdns } from '@push.rocks/smartdns/client';
|
|
724
|
-
import { DnsServer } from '@push.rocks/smartdns/server';
|
|
725
|
-
|
|
726
|
-
// Test DNS Client
|
|
727
|
-
tap.test('DNS Client - Query Records', async () => {
|
|
728
|
-
const dnsClient = new Smartdns({});
|
|
729
|
-
|
|
730
|
-
// Test A record query
|
|
731
|
-
const aRecords = await dnsClient.getRecordsA('google.com');
|
|
732
|
-
expect(aRecords).toBeArray();
|
|
733
|
-
expect(aRecords[0]).toHaveProperty('type', 'A');
|
|
734
|
-
expect(aRecords[0].data).toMatch(/^\d+\.\d+\.\d+\.\d+$/);
|
|
735
|
-
|
|
736
|
-
// Test TXT record query
|
|
737
|
-
const txtRecords = await dnsClient.getRecordsTxt('google.com');
|
|
738
|
-
expect(txtRecords).toBeArray();
|
|
739
|
-
expect(txtRecords[0]).toHaveProperty('type', 'TXT');
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
// Test DNS Server
|
|
743
|
-
let dnsServer: DnsServer;
|
|
744
|
-
|
|
745
|
-
tap.test('DNS Server - Setup and Start', async () => {
|
|
746
|
-
dnsServer = new DnsServer({
|
|
747
|
-
udpPort: 5353,
|
|
748
|
-
httpsPort: 8443,
|
|
749
|
-
httpsKey: 'test-key', // Use test certificates
|
|
750
|
-
httpsCert: 'test-cert',
|
|
751
|
-
dnssecZone: 'test.local'
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
expect(dnsServer).toBeInstanceOf(DnsServer);
|
|
755
|
-
await dnsServer.start();
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
tap.test('DNS Server - Register Handlers', async () => {
|
|
759
|
-
// Register multiple handlers
|
|
760
|
-
dnsServer.registerHandler('test.local', ['A'], () => ({
|
|
761
|
-
name: 'test.local',
|
|
762
|
-
type: 'A',
|
|
763
|
-
class: 'IN',
|
|
764
|
-
ttl: 300,
|
|
765
|
-
data: '127.0.0.1',
|
|
766
|
-
}));
|
|
767
|
-
|
|
768
|
-
dnsServer.registerHandler('*.test.local', ['A'], (question) => ({
|
|
769
|
-
name: question.name,
|
|
770
|
-
type: 'A',
|
|
771
|
-
class: 'IN',
|
|
772
|
-
ttl: 60,
|
|
773
|
-
data: '127.0.0.2',
|
|
774
|
-
}));
|
|
775
|
-
});
|
|
776
523
|
|
|
777
|
-
tap.test('
|
|
778
|
-
const
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
questions: [{
|
|
785
|
-
type: 'A',
|
|
786
|
-
class: 'IN',
|
|
787
|
-
name: 'test.local',
|
|
788
|
-
}],
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
const client = dgram.createSocket('udp4');
|
|
792
|
-
const done = tools.defer();
|
|
793
|
-
|
|
794
|
-
client.on('message', (msg) => {
|
|
795
|
-
const response = dnsPacket.decode(msg);
|
|
796
|
-
expect(response.answers[0].data).toEqual('127.0.0.1');
|
|
797
|
-
client.close();
|
|
798
|
-
done.resolve();
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
client.send(query, 5353, 'localhost'); // Use the port specified during server creation
|
|
802
|
-
await done.promise;
|
|
524
|
+
tap.test('resolve A records via UDP', async () => {
|
|
525
|
+
const dns = new Smartdns({ strategy: 'udp' });
|
|
526
|
+
const records = await dns.getRecordsA('google.com');
|
|
527
|
+
expect(records).toBeArray();
|
|
528
|
+
expect(records[0]).toHaveProperty('type', 'A');
|
|
529
|
+
expect(records[0]).toHaveProperty('value');
|
|
530
|
+
dns.destroy();
|
|
803
531
|
});
|
|
804
532
|
|
|
805
|
-
tap.test('
|
|
806
|
-
|
|
533
|
+
tap.test('detect DNSSEC via DoH', async () => {
|
|
534
|
+
const dns = new Smartdns({ strategy: 'doh' });
|
|
535
|
+
const records = await dns.getRecordsA('cloudflare.com');
|
|
536
|
+
expect(records[0].dnsSecEnabled).toBeTrue();
|
|
537
|
+
dns.destroy();
|
|
807
538
|
});
|
|
808
539
|
|
|
809
|
-
|
|
810
|
-
await tap.start();
|
|
540
|
+
export default tap.start();
|
|
811
541
|
```
|
|
812
542
|
|
|
813
|
-
### Best Practices
|
|
814
|
-
|
|
815
|
-
1. **Port Selection**: Use non-privileged ports (>1024) during development
|
|
816
|
-
2. **Handler Organization**: Group related handlers together
|
|
817
|
-
3. **Error Handling**: Always handle DNS query errors gracefully
|
|
818
|
-
4. **DNSSEC**: Enable DNSSEC for production deployments
|
|
819
|
-
5. **Monitoring**: Log DNS queries for debugging and analytics
|
|
820
|
-
6. **Rate Limiting**: Implement rate limiting for public DNS servers
|
|
821
|
-
7. **Caching**: Respect TTL values and implement proper caching
|
|
822
|
-
8. **Manual Sockets**: Use manual socket handling for clustering and load balancing
|
|
823
|
-
|
|
824
|
-
### Performance Considerations
|
|
825
|
-
|
|
826
|
-
- The DNS client uses HTTP keep-alive for connection reuse
|
|
827
|
-
- The DNS server handles concurrent UDP and HTTPS requests efficiently
|
|
828
|
-
- DNSSEC signatures are generated on-demand to reduce memory usage
|
|
829
|
-
- Pattern matching uses caching for improved performance
|
|
830
|
-
- Manual socket handling enables horizontal scaling across CPU cores
|
|
831
|
-
|
|
832
|
-
### Security Considerations
|
|
833
|
-
|
|
834
|
-
- Always use DNSSEC for authenticated responses
|
|
835
|
-
- Enable DoH for encrypted DNS queries
|
|
836
|
-
- Validate and sanitize all DNS inputs
|
|
837
|
-
- Implement access controls for DNS server handlers
|
|
838
|
-
- Use Let's Encrypt for automatic SSL certificate management
|
|
839
|
-
- Never expose internal network information through DNS
|
|
840
|
-
- Bind to specific interfaces in production environments
|
|
841
|
-
- Use manual socket handling for custom security layers
|
|
842
|
-
|
|
843
|
-
This comprehensive library provides everything needed for both DNS client operations and running production-grade DNS servers with modern security features in TypeScript.
|
|
844
|
-
|
|
845
543
|
## License and Legal Information
|
|
846
544
|
|
|
847
|
-
This repository contains open-source code
|
|
545
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
|
848
546
|
|
|
849
547
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
850
548
|
|
|
851
549
|
### Trademarks
|
|
852
550
|
|
|
853
|
-
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein.
|
|
551
|
+
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
|
552
|
+
|
|
553
|
+
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
|
854
554
|
|
|
855
555
|
### Company Information
|
|
856
556
|
|
|
857
|
-
Task Venture Capital GmbH
|
|
858
|
-
Registered at District
|
|
557
|
+
Task Venture Capital GmbH
|
|
558
|
+
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
859
559
|
|
|
860
|
-
For any legal inquiries or
|
|
560
|
+
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
861
561
|
|
|
862
|
-
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
|
562
|
+
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|