@serve.zone/dcrouter 15.0.1 → 15.0.2
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/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +25 -0
- package/dist_ts/acme/classes.smartacme-lifecycle.js +144 -0
- package/dist_ts/acme/index.d.ts +1 -0
- package/dist_ts/acme/index.js +2 -1
- package/dist_ts/classes.dcrouter.d.ts +21 -139
- package/dist_ts/classes.dcrouter.js +71 -1585
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +37 -0
- package/dist_ts/dns/classes.dns-server-runtime.js +449 -0
- package/dist_ts/dns/index.d.ts +1 -0
- package/dist_ts/dns/index.js +2 -1
- package/dist_ts/email/classes.accepted-email-spool.d.ts +55 -0
- package/dist_ts/email/classes.accepted-email-spool.js +345 -0
- package/dist_ts/email/classes.email-route-builder.d.ts +28 -0
- package/dist_ts/email/classes.email-route-builder.js +260 -0
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +10 -8
- package/dist_ts/remoteingress/classes.hub-lifecycle.d.ts +27 -0
- package/dist_ts/remoteingress/classes.hub-lifecycle.js +241 -0
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +1 -2
- package/dist_ts/remoteingress/index.d.ts +1 -0
- package/dist_ts/remoteingress/index.js +2 -1
- package/dist_ts/security/classes.route-policy-augmenter.d.ts +22 -0
- package/dist_ts/security/classes.route-policy-augmenter.js +120 -0
- package/dist_ts/security/index.d.ts +1 -0
- package/dist_ts/security/index.js +2 -1
- package/dist_ts/vpn/classes.vpn-access-resolver.d.ts +34 -0
- package/dist_ts/vpn/classes.vpn-access-resolver.js +101 -0
- package/dist_ts/vpn/index.d.ts +1 -0
- package/dist_ts/vpn/index.js +2 -1
- package/dist_ts_migrations/index.js +92 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/acme/classes.smartacme-lifecycle.ts +155 -0
- package/ts/acme/index.ts +1 -0
- package/ts/classes.dcrouter.ts +118 -1919
- package/ts/dns/classes.dns-server-runtime.ts +525 -0
- package/ts/dns/index.ts +1 -0
- package/ts/email/classes.accepted-email-spool.ts +434 -0
- package/ts/email/classes.email-route-builder.ts +312 -0
- package/ts/email/index.ts +2 -0
- package/ts/opsserver/handlers/gatewayclient.handler.ts +9 -7
- package/ts/remoteingress/classes.hub-lifecycle.ts +278 -0
- package/ts/remoteingress/classes.remoteingress-manager.ts +1 -1
- package/ts/remoteingress/index.ts +1 -0
- package/ts/security/classes.route-policy-augmenter.ts +140 -0
- package/ts/security/index.ts +1 -0
- package/ts/vpn/classes.vpn-access-resolver.ts +126 -0
- package/ts/vpn/index.ts +1 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import * as paths from '../paths.js';
|
|
3
|
+
import { logger } from '../logger.js';
|
|
4
|
+
import { buildEmailDnsRecords } from '../email/index.js';
|
|
5
|
+
import type { DcRouter } from '../classes.dcrouter.js';
|
|
6
|
+
|
|
7
|
+
type TDnsRecordSeed = { name: string; type: string; value: string; ttl?: number; useIngressProxy?: boolean };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sets up and feeds the embedded authoritative smartdns server: validates the
|
|
11
|
+
* DNS configuration, generates authoritative/email/DKIM records, applies
|
|
12
|
+
* proxy-IP replacement, registers record handlers, wires rate-limited query
|
|
13
|
+
* logging/metrics, and provides the DoH socket handler for SmartProxy routes.
|
|
14
|
+
*/
|
|
15
|
+
export class DnsServerRuntime {
|
|
16
|
+
// Adaptive query-log rate limiting state
|
|
17
|
+
private logWindowSecond = 0; // epoch second of current window
|
|
18
|
+
private logWindowCount = 0; // queries logged this second
|
|
19
|
+
private batchCount = 0;
|
|
20
|
+
private batchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(private dcRouterRef: DcRouter) {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create the DNS server, start it on UDP, wire metrics/logging, and
|
|
26
|
+
* register all generated records.
|
|
27
|
+
*/
|
|
28
|
+
public async setup(): Promise<void> {
|
|
29
|
+
const options = this.dcRouterRef.options;
|
|
30
|
+
if (!options.dnsNsDomains || options.dnsNsDomains.length === 0) {
|
|
31
|
+
throw new Error('dnsNsDomains is required for DNS server setup');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!options.dnsScopes || options.dnsScopes.length === 0) {
|
|
35
|
+
throw new Error('dnsScopes is required for DNS server setup');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const primaryNameserver = options.dnsNsDomains[0];
|
|
39
|
+
logger.log('info', `Setting up DNS server with primary nameserver: ${primaryNameserver}`);
|
|
40
|
+
|
|
41
|
+
// Get VM IP address for UDP binding
|
|
42
|
+
const networkInterfaces = plugins.os.networkInterfaces() as Record<
|
|
43
|
+
string,
|
|
44
|
+
Array<{ internal: boolean; family: string; address: string }> | undefined
|
|
45
|
+
>;
|
|
46
|
+
let vmIpAddress = options.dnsBindInterface || '0.0.0.0'; // Default to all interfaces
|
|
47
|
+
|
|
48
|
+
// Try to find the VM's internal IP address when no explicit bind address is configured.
|
|
49
|
+
if (!options.dnsBindInterface) {
|
|
50
|
+
interfaceLoop: for (const [_name, interfaces] of Object.entries(networkInterfaces)) {
|
|
51
|
+
if (interfaces) {
|
|
52
|
+
for (const iface of interfaces) {
|
|
53
|
+
if (!iface.internal && iface.family === 'IPv4') {
|
|
54
|
+
vmIpAddress = iface.address;
|
|
55
|
+
break interfaceLoop;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Create DNS server instance with manual HTTPS mode
|
|
63
|
+
const dnsServer = new plugins.smartdns.dnsServerMod.DnsServer({
|
|
64
|
+
udpPort: 53,
|
|
65
|
+
udpBindInterface: vmIpAddress,
|
|
66
|
+
httpsPort: 443, // Required but won't bind due to manual mode
|
|
67
|
+
manualHttpsMode: true, // Enable manual HTTPS socket handling
|
|
68
|
+
dnssecZone: primaryNameserver,
|
|
69
|
+
primaryNameserver: primaryNameserver, // Automatically generates correct SOA records
|
|
70
|
+
// For now, use self-signed cert until we integrate with Let's Encrypt
|
|
71
|
+
httpsKey: '',
|
|
72
|
+
httpsCert: ''
|
|
73
|
+
});
|
|
74
|
+
this.dcRouterRef.dnsServer = dnsServer;
|
|
75
|
+
|
|
76
|
+
// Start the DNS server (UDP only)
|
|
77
|
+
await dnsServer.start();
|
|
78
|
+
logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`);
|
|
79
|
+
|
|
80
|
+
// Wire DNS query events to MetricsManager and logger with adaptive rate limiting
|
|
81
|
+
if (this.dcRouterRef.metricsManager) {
|
|
82
|
+
const flushDnsBatch = () => {
|
|
83
|
+
if (this.batchCount > 0) {
|
|
84
|
+
logger.log('info', `DNS: ${this.batchCount} queries processed (rate limited)`, { zone: 'dns' });
|
|
85
|
+
this.batchCount = 0;
|
|
86
|
+
}
|
|
87
|
+
this.batchTimer = null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
dnsServer.on('query', (event: plugins.smartdns.dnsServerMod.IDnsQueryCompletedEvent) => {
|
|
91
|
+
// Metrics tracking
|
|
92
|
+
for (const question of event.questions) {
|
|
93
|
+
this.dcRouterRef.metricsManager?.trackDnsQuery(
|
|
94
|
+
question.type,
|
|
95
|
+
question.name,
|
|
96
|
+
false,
|
|
97
|
+
event.responseTimeMs,
|
|
98
|
+
event.answered,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Adaptive logging: individual logs up to 2/sec, then batch
|
|
103
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
104
|
+
if (nowSec !== this.logWindowSecond) {
|
|
105
|
+
this.logWindowSecond = nowSec;
|
|
106
|
+
this.logWindowCount = 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (this.logWindowCount < 2) {
|
|
110
|
+
this.logWindowCount++;
|
|
111
|
+
const summary = event.questions.map(q => `${q.type} ${q.name}`).join(', ');
|
|
112
|
+
logger.log('info', `DNS query: ${summary} (${event.responseTimeMs}ms, ${event.answered ? 'answered' : 'unanswered'})`, { zone: 'dns' });
|
|
113
|
+
} else {
|
|
114
|
+
this.batchCount++;
|
|
115
|
+
if (!this.batchTimer) {
|
|
116
|
+
this.batchTimer = setTimeout(flushDnsBatch, 5000);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Validate DNS configuration
|
|
123
|
+
await this.validateConfiguration();
|
|
124
|
+
|
|
125
|
+
// Generate and register authoritative records
|
|
126
|
+
const authoritativeRecords = await this.generateAuthoritativeRecords();
|
|
127
|
+
|
|
128
|
+
// Generate email DNS records
|
|
129
|
+
const emailDnsRecords = await this.generateEmailDnsRecords();
|
|
130
|
+
|
|
131
|
+
// Ensure DKIM keys exist for internal-dns domains before generating records.
|
|
132
|
+
await this.initializeDkimForEmailDomains();
|
|
133
|
+
|
|
134
|
+
// Generate DKIM records directly from smartmta.
|
|
135
|
+
const dkimRecords = await this.loadDkimRecords();
|
|
136
|
+
|
|
137
|
+
// Combine all records: authoritative, email, DKIM, and user-defined
|
|
138
|
+
const allRecords: TDnsRecordSeed[] = [...authoritativeRecords, ...emailDnsRecords, ...dkimRecords];
|
|
139
|
+
if (options.dnsRecords && options.dnsRecords.length > 0) {
|
|
140
|
+
allRecords.push(...options.dnsRecords);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Apply proxy IP replacement if configured
|
|
144
|
+
await this.applyProxyIpReplacement(allRecords);
|
|
145
|
+
|
|
146
|
+
// Register all DNS records
|
|
147
|
+
if (allRecords.length > 0) {
|
|
148
|
+
this.registerRecords(allRecords);
|
|
149
|
+
logger.log('info', `Registered ${allRecords.length} DNS records (${authoritativeRecords.length} authoritative, ${emailDnsRecords.length} email, ${dkimRecords.length} DKIM, ${options.dnsRecords?.length || 0} user-defined)`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Hand the DnsServer to DnsManager so DB-backed local records on
|
|
153
|
+
// dcrouter-hosted domains get registered too.
|
|
154
|
+
if (this.dcRouterRef.dnsManager) {
|
|
155
|
+
await this.dcRouterRef.dnsManager.attachDnsServer(dnsServer);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create the DoH socket handler SmartProxy routes hand TLS sockets to.
|
|
161
|
+
*/
|
|
162
|
+
public createSocketHandler(): (socket: plugins.net.Socket) => Promise<void> {
|
|
163
|
+
return async (socket: plugins.net.Socket) => {
|
|
164
|
+
if (!this.dcRouterRef.dnsServer) {
|
|
165
|
+
logger.log('error', 'DNS socket handler called but DNS server not initialized');
|
|
166
|
+
socket.end();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Prevent uncaught exception from socket 'error' events
|
|
171
|
+
socket.on('error', (err) => {
|
|
172
|
+
logger.log('error', `DNS socket error: ${err.message}`);
|
|
173
|
+
if (!socket.destroyed) {
|
|
174
|
+
socket.destroy();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
logger.log('debug', 'DNS socket handler: passing socket to DnsServer');
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// Use the built-in socket handler from smartdns
|
|
182
|
+
// This handles HTTP/2, DoH protocol, etc.
|
|
183
|
+
await (this.dcRouterRef.dnsServer as any).handleHttpsSocket(socket);
|
|
184
|
+
} catch (error: unknown) {
|
|
185
|
+
logger.log('error', `DNS socket handler error: ${(error as Error).message}`);
|
|
186
|
+
if (!socket.destroyed) {
|
|
187
|
+
socket.destroy();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Flush the pending rate-limited query-log batch and reset logging state. */
|
|
194
|
+
public flushQueryLogBatch(): void {
|
|
195
|
+
if (this.batchTimer) {
|
|
196
|
+
clearTimeout(this.batchTimer);
|
|
197
|
+
if (this.batchCount > 0) {
|
|
198
|
+
logger.log('info', `DNS: ${this.batchCount} queries processed (final flush)`, { zone: 'dns' });
|
|
199
|
+
}
|
|
200
|
+
this.batchTimer = null;
|
|
201
|
+
this.batchCount = 0;
|
|
202
|
+
this.logWindowSecond = 0;
|
|
203
|
+
this.logWindowCount = 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private registerRecords(records: TDnsRecordSeed[]): void {
|
|
208
|
+
const dnsServer = this.dcRouterRef.dnsServer;
|
|
209
|
+
if (!dnsServer) return;
|
|
210
|
+
|
|
211
|
+
// Register a separate handler for each record
|
|
212
|
+
// This ensures multiple records of the same type (like NS records) are all served
|
|
213
|
+
for (const record of records) {
|
|
214
|
+
// Register handler for this specific record
|
|
215
|
+
dnsServer.registerHandler(record.name, [record.type], (question) => {
|
|
216
|
+
// Check if this handler matches the question
|
|
217
|
+
if (question.name === record.name && question.type === record.type) {
|
|
218
|
+
return {
|
|
219
|
+
name: record.name,
|
|
220
|
+
type: record.type,
|
|
221
|
+
class: 'IN',
|
|
222
|
+
ttl: record.ttl || 300,
|
|
223
|
+
data: this.parseRecordData(record.type, record.value)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return null;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
logger.log('info', `Registered ${records.length} DNS handlers (one per record)`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private parseRecordData(type: string, value: string): any {
|
|
235
|
+
switch (type) {
|
|
236
|
+
case 'A':
|
|
237
|
+
return value; // IP address as string
|
|
238
|
+
case 'MX':
|
|
239
|
+
const [priority, exchange] = value.split(' ');
|
|
240
|
+
return { priority: parseInt(priority), exchange };
|
|
241
|
+
case 'TXT':
|
|
242
|
+
return value;
|
|
243
|
+
case 'NS':
|
|
244
|
+
return value;
|
|
245
|
+
case 'SOA':
|
|
246
|
+
// SOA format: primary-ns admin-email serial refresh retry expire minimum
|
|
247
|
+
const parts = value.split(' ');
|
|
248
|
+
return {
|
|
249
|
+
mname: parts[0],
|
|
250
|
+
rname: parts[1],
|
|
251
|
+
serial: parseInt(parts[2]),
|
|
252
|
+
refresh: parseInt(parts[3]),
|
|
253
|
+
retry: parseInt(parts[4]),
|
|
254
|
+
expire: parseInt(parts[5]),
|
|
255
|
+
minimum: parseInt(parts[6])
|
|
256
|
+
};
|
|
257
|
+
default:
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private async validateConfiguration(): Promise<void> {
|
|
263
|
+
const options = this.dcRouterRef.options;
|
|
264
|
+
if (!options.dnsNsDomains || !options.dnsScopes) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
logger.log('info', 'Validating DNS configuration...');
|
|
269
|
+
|
|
270
|
+
// Check if email domains with internal-dns are in dnsScopes
|
|
271
|
+
if (options.emailConfig?.domains) {
|
|
272
|
+
for (const domainConfig of options.emailConfig.domains) {
|
|
273
|
+
if (domainConfig.dnsMode === 'internal-dns' &&
|
|
274
|
+
!options.dnsScopes.includes(domainConfig.domain)) {
|
|
275
|
+
logger.log('warn', `Email domain '${domainConfig.domain}' with internal-dns mode is not in dnsScopes. It should be added to dnsScopes.`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Validate user-provided DNS records are within scopes
|
|
281
|
+
if (options.dnsRecords) {
|
|
282
|
+
for (const record of options.dnsRecords) {
|
|
283
|
+
const recordDomain = this.extractDomain(record.name);
|
|
284
|
+
const isInScope = options.dnsScopes.some(scope =>
|
|
285
|
+
recordDomain === scope || recordDomain.endsWith(`.${scope}`)
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
if (!isInScope) {
|
|
289
|
+
logger.log('warn', `DNS record for '${record.name}' is outside defined scopes [${options.dnsScopes.join(', ')}]`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async generateEmailDnsRecords(): Promise<TDnsRecordSeed[]> {
|
|
296
|
+
const options = this.dcRouterRef.options;
|
|
297
|
+
const records: TDnsRecordSeed[] = [];
|
|
298
|
+
|
|
299
|
+
if (!options.emailConfig?.domains) {
|
|
300
|
+
return records;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Filter domains with internal-dns mode
|
|
304
|
+
const internalDnsDomains = options.emailConfig.domains.filter(
|
|
305
|
+
domain => domain.dnsMode === 'internal-dns'
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
for (const domainConfig of internalDnsDomains) {
|
|
309
|
+
const domain = domainConfig.domain;
|
|
310
|
+
const ttl = domainConfig.dns?.internal?.ttl || 3600;
|
|
311
|
+
const requiredRecords = buildEmailDnsRecords({
|
|
312
|
+
domain,
|
|
313
|
+
hostname: options.emailConfig.hostname,
|
|
314
|
+
mxPriority: domainConfig.dns?.internal?.mxPriority,
|
|
315
|
+
}).filter((record) => !record.name.includes('._domainkey.'));
|
|
316
|
+
|
|
317
|
+
for (const record of requiredRecords) {
|
|
318
|
+
records.push({
|
|
319
|
+
name: record.name,
|
|
320
|
+
type: record.type,
|
|
321
|
+
value: record.value,
|
|
322
|
+
ttl,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
logger.log('info', `Generated ${records.length} email DNS records for ${internalDnsDomains.length} internal-dns domains`);
|
|
328
|
+
return records;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private async loadDkimRecords(): Promise<TDnsRecordSeed[]> {
|
|
332
|
+
const options = this.dcRouterRef.options;
|
|
333
|
+
const records: TDnsRecordSeed[] = [];
|
|
334
|
+
if (!options.emailConfig?.domains || !this.dcRouterRef.emailServer?.dkimCreator) {
|
|
335
|
+
return records;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const domainConfig of options.emailConfig.domains) {
|
|
339
|
+
if (domainConfig.dnsMode !== 'internal-dns') {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const selector = domainConfig.dkim?.selector || 'default';
|
|
343
|
+
try {
|
|
344
|
+
const dkimRecord = await this.dcRouterRef.emailServer.dkimCreator.getDNSRecordForDomain(domainConfig.domain, selector);
|
|
345
|
+
records.push({
|
|
346
|
+
name: dkimRecord.name,
|
|
347
|
+
type: 'TXT',
|
|
348
|
+
value: dkimRecord.value,
|
|
349
|
+
ttl: domainConfig.dns?.internal?.ttl || 3600,
|
|
350
|
+
});
|
|
351
|
+
} catch (error: unknown) {
|
|
352
|
+
logger.log('error', `Failed to generate DKIM record for ${domainConfig.domain}: ${(error as Error).message}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return records;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private async initializeDkimForEmailDomains(): Promise<void> {
|
|
360
|
+
const options = this.dcRouterRef.options;
|
|
361
|
+
if (!options.emailConfig?.domains || !this.dcRouterRef.emailServer) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
logger.log('info', 'Initializing DKIM keys for email domains...');
|
|
366
|
+
|
|
367
|
+
// Get DKIMCreator instance from email server (public in smartmta)
|
|
368
|
+
const dkimCreator = this.dcRouterRef.emailServer.dkimCreator;
|
|
369
|
+
if (!dkimCreator) {
|
|
370
|
+
logger.log('warn', 'DKIMCreator not available, skipping DKIM initialization');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Ensure necessary directories exist
|
|
375
|
+
paths.ensureDataDirectories(this.dcRouterRef.resolvedPaths);
|
|
376
|
+
|
|
377
|
+
// Generate DKIM keys for each internal-dns email domain using the configured selector.
|
|
378
|
+
for (const domainConfig of options.emailConfig.domains) {
|
|
379
|
+
if (domainConfig.dnsMode !== 'internal-dns') {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
await dkimCreator.handleDKIMKeysForSelector(
|
|
384
|
+
domainConfig.domain,
|
|
385
|
+
domainConfig.dkim?.selector || 'default',
|
|
386
|
+
domainConfig.dkim?.keySize || 2048,
|
|
387
|
+
);
|
|
388
|
+
logger.log('info', `DKIM keys initialized for ${domainConfig.domain}`);
|
|
389
|
+
} catch (error: unknown) {
|
|
390
|
+
logger.log('error', `Failed to initialize DKIM for ${domainConfig.domain}: ${(error as Error).message}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
logger.log('info', 'DKIM initialization complete');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private async generateAuthoritativeRecords(): Promise<TDnsRecordSeed[]> {
|
|
398
|
+
const options = this.dcRouterRef.options;
|
|
399
|
+
const records: TDnsRecordSeed[] = [];
|
|
400
|
+
|
|
401
|
+
if (!options.dnsNsDomains || !options.dnsScopes) {
|
|
402
|
+
return records;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Determine the public IP for nameserver A records
|
|
406
|
+
let publicIp: string | null = null;
|
|
407
|
+
|
|
408
|
+
// Use proxy IPs if configured (these should be public IPs)
|
|
409
|
+
if (options.proxyIps && options.proxyIps.length > 0) {
|
|
410
|
+
publicIp = options.proxyIps[0]; // Use first proxy IP
|
|
411
|
+
logger.log('info', `Using proxy IP for nameserver A records: ${publicIp}`);
|
|
412
|
+
} else if (options.publicIp) {
|
|
413
|
+
// Use explicitly configured public IP
|
|
414
|
+
publicIp = options.publicIp;
|
|
415
|
+
this.dcRouterRef.detectedPublicIp = publicIp;
|
|
416
|
+
logger.log('info', `Using configured public IP for nameserver A records: ${publicIp}`);
|
|
417
|
+
} else {
|
|
418
|
+
// Auto-discover public IP using smartnetwork
|
|
419
|
+
try {
|
|
420
|
+
logger.log('info', 'Auto-discovering public IP address...');
|
|
421
|
+
const smartNetwork = new plugins.smartnetwork.SmartNetwork();
|
|
422
|
+
const publicIps = await smartNetwork.getPublicIps();
|
|
423
|
+
|
|
424
|
+
if (publicIps.v4) {
|
|
425
|
+
publicIp = publicIps.v4;
|
|
426
|
+
this.dcRouterRef.detectedPublicIp = publicIp;
|
|
427
|
+
logger.log('info', `Auto-discovered public IPv4: ${publicIp}`);
|
|
428
|
+
} else {
|
|
429
|
+
logger.log('warn', 'Could not auto-discover public IPv4 address');
|
|
430
|
+
}
|
|
431
|
+
} catch (error: unknown) {
|
|
432
|
+
logger.log('error', `Failed to auto-discover public IP: ${(error as Error).message}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!publicIp) {
|
|
436
|
+
logger.log('warn', 'No public IP available. Nameserver A records require either proxyIps, publicIp, or successful auto-discovery.');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Generate A records for nameservers if we have a public IP
|
|
441
|
+
if (publicIp) {
|
|
442
|
+
for (const nsDomain of options.dnsNsDomains) {
|
|
443
|
+
records.push({
|
|
444
|
+
name: nsDomain,
|
|
445
|
+
type: 'A',
|
|
446
|
+
value: publicIp,
|
|
447
|
+
ttl: 3600
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
logger.log('info', `Generated A records for ${options.dnsNsDomains.length} nameservers`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Generate NS records for each domain in scopes
|
|
454
|
+
for (const domain of options.dnsScopes) {
|
|
455
|
+
// Add NS records for all nameservers
|
|
456
|
+
for (const nsDomain of options.dnsNsDomains) {
|
|
457
|
+
records.push({
|
|
458
|
+
name: domain,
|
|
459
|
+
type: 'NS',
|
|
460
|
+
value: nsDomain,
|
|
461
|
+
ttl: 3600
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// SOA records are now automatically generated by smartdns DnsServer
|
|
466
|
+
// with the primaryNameserver configuration option
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
logger.log('info', `Generated ${records.length} total records (A + NS) for ${options.dnsScopes.length} domains`);
|
|
470
|
+
return records;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private extractDomain(recordName: string): string {
|
|
474
|
+
// Handle wildcards
|
|
475
|
+
if (recordName.startsWith('*.')) {
|
|
476
|
+
recordName = recordName.substring(2);
|
|
477
|
+
}
|
|
478
|
+
return recordName;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private async applyProxyIpReplacement(records: TDnsRecordSeed[]): Promise<void> {
|
|
482
|
+
const options = this.dcRouterRef.options;
|
|
483
|
+
if (!options.proxyIps || options.proxyIps.length === 0) {
|
|
484
|
+
return; // No proxy IPs configured, skip replacement
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Get server's public IP
|
|
488
|
+
const serverIp = await this.detectServerPublicIp();
|
|
489
|
+
if (!serverIp) {
|
|
490
|
+
logger.log('warn', 'Could not detect server public IP, skipping proxy IP replacement');
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
logger.log('info', `Applying proxy IP replacement. Server IP: ${serverIp}, Proxy IPs: ${options.proxyIps.join(', ')}`);
|
|
495
|
+
|
|
496
|
+
let proxyIndex = 0;
|
|
497
|
+
for (const record of records) {
|
|
498
|
+
if (record.type === 'A' &&
|
|
499
|
+
record.value === serverIp &&
|
|
500
|
+
record.useIngressProxy !== false) {
|
|
501
|
+
// Round-robin through proxy IPs
|
|
502
|
+
const proxyIp = options.proxyIps[proxyIndex % options.proxyIps.length];
|
|
503
|
+
logger.log('info', `Replacing A record for ${record.name}: ${record.value} → ${proxyIp}`);
|
|
504
|
+
record.value = proxyIp;
|
|
505
|
+
proxyIndex++;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private async detectServerPublicIp(): Promise<string | null> {
|
|
511
|
+
try {
|
|
512
|
+
const smartNetwork = new plugins.smartnetwork.SmartNetwork();
|
|
513
|
+
const publicIps = await smartNetwork.getPublicIps();
|
|
514
|
+
|
|
515
|
+
if (publicIps.v4) {
|
|
516
|
+
return publicIps.v4;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return null;
|
|
520
|
+
} catch (error: unknown) {
|
|
521
|
+
logger.log('warn', `Failed to detect public IP: ${(error as Error).message}`);
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
package/ts/dns/index.ts
CHANGED