@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.
Files changed (53) hide show
  1. package/deno.json +1 -1
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +25 -0
  4. package/dist_ts/acme/classes.smartacme-lifecycle.js +144 -0
  5. package/dist_ts/acme/index.d.ts +1 -0
  6. package/dist_ts/acme/index.js +2 -1
  7. package/dist_ts/classes.dcrouter.d.ts +21 -139
  8. package/dist_ts/classes.dcrouter.js +71 -1585
  9. package/dist_ts/dns/classes.dns-server-runtime.d.ts +37 -0
  10. package/dist_ts/dns/classes.dns-server-runtime.js +449 -0
  11. package/dist_ts/dns/index.d.ts +1 -0
  12. package/dist_ts/dns/index.js +2 -1
  13. package/dist_ts/email/classes.accepted-email-spool.d.ts +55 -0
  14. package/dist_ts/email/classes.accepted-email-spool.js +345 -0
  15. package/dist_ts/email/classes.email-route-builder.d.ts +28 -0
  16. package/dist_ts/email/classes.email-route-builder.js +260 -0
  17. package/dist_ts/email/index.d.ts +2 -0
  18. package/dist_ts/email/index.js +3 -1
  19. package/dist_ts/opsserver/handlers/gatewayclient.handler.js +10 -8
  20. package/dist_ts/remoteingress/classes.hub-lifecycle.d.ts +27 -0
  21. package/dist_ts/remoteingress/classes.hub-lifecycle.js +241 -0
  22. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +1 -2
  23. package/dist_ts/remoteingress/index.d.ts +1 -0
  24. package/dist_ts/remoteingress/index.js +2 -1
  25. package/dist_ts/security/classes.route-policy-augmenter.d.ts +22 -0
  26. package/dist_ts/security/classes.route-policy-augmenter.js +120 -0
  27. package/dist_ts/security/index.d.ts +1 -0
  28. package/dist_ts/security/index.js +2 -1
  29. package/dist_ts/vpn/classes.vpn-access-resolver.d.ts +34 -0
  30. package/dist_ts/vpn/classes.vpn-access-resolver.js +101 -0
  31. package/dist_ts/vpn/index.d.ts +1 -0
  32. package/dist_ts/vpn/index.js +2 -1
  33. package/dist_ts_migrations/index.js +92 -9
  34. package/dist_ts_web/00_commitinfo_data.js +1 -1
  35. package/package.json +1 -1
  36. package/ts/00_commitinfo_data.ts +1 -1
  37. package/ts/acme/classes.smartacme-lifecycle.ts +155 -0
  38. package/ts/acme/index.ts +1 -0
  39. package/ts/classes.dcrouter.ts +118 -1919
  40. package/ts/dns/classes.dns-server-runtime.ts +525 -0
  41. package/ts/dns/index.ts +1 -0
  42. package/ts/email/classes.accepted-email-spool.ts +434 -0
  43. package/ts/email/classes.email-route-builder.ts +312 -0
  44. package/ts/email/index.ts +2 -0
  45. package/ts/opsserver/handlers/gatewayclient.handler.ts +9 -7
  46. package/ts/remoteingress/classes.hub-lifecycle.ts +278 -0
  47. package/ts/remoteingress/classes.remoteingress-manager.ts +1 -1
  48. package/ts/remoteingress/index.ts +1 -0
  49. package/ts/security/classes.route-policy-augmenter.ts +140 -0
  50. package/ts/security/index.ts +1 -0
  51. package/ts/vpn/classes.vpn-access-resolver.ts +126 -0
  52. package/ts/vpn/index.ts +1 -0
  53. 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
@@ -1,2 +1,3 @@
1
1
  export * from './manager.dns.js';
2
2
  export * from './providers/index.js';
3
+ export * from './classes.dns-server-runtime.js';