@serve.zone/dcrouter 17.9.1 → 17.9.3

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.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '17.9.1',
6
+ version: '17.9.3',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfd2ViLzAwX2NvbW1pdGluZm9fZGF0YS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7R0FFRztBQUNILE1BQU0sQ0FBQyxNQUFNLFVBQVUsR0FBRztJQUN4QixJQUFJLEVBQUUsc0JBQXNCO0lBQzVCLE9BQU8sRUFBRSxRQUFRO0lBQ2pCLFdBQVcsRUFBRSwwRUFBMEU7Q0FDeEYsQ0FBQSJ9
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@serve.zone/dcrouter",
3
3
  "private": false,
4
- "version": "17.9.1",
4
+ "version": "17.9.3",
5
5
  "description": "A multifaceted routing service handling mail and SMS delivery functions.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -41,7 +41,7 @@
41
41
  "@push.rocks/smartbucket": "^4.6.1",
42
42
  "@push.rocks/smartchallenge": "^1.1.0",
43
43
  "@push.rocks/smartdata": "^7.4.0",
44
- "@push.rocks/smartdb": "^2.14.0",
44
+ "@push.rocks/smartdb": "^2.14.1",
45
45
  "@push.rocks/smartdns": "^7.12.1",
46
46
  "@push.rocks/smartfs": "^1.6.0",
47
47
  "@push.rocks/smartguard": "^3.1.2",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '17.9.1',
6
+ version: '17.9.3',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -743,6 +743,9 @@ export class DcRouter {
743
743
  } catch (err: unknown) {
744
744
  logger.log('error', `Failed to sync Remote Ingress allowed edges: ${(err as Error).message}`);
745
745
  }
746
+ this.dnsServerRuntime.syncPrivateRouteOverrides(
747
+ routes as IDcRouterRouteConfig[],
748
+ );
746
749
  },
747
750
  (preparedRoutes) => buildHttpRedirectRuntimeRoutes(preparedRoutes || []),
748
751
  (storedRoute: IRoute) => this.emailRouteBuilder.hydrateStoredRouteForRuntime(storedRoute),
@@ -832,10 +835,14 @@ export class DcRouter {
832
835
 
833
836
  // DNS Server: optional, depends on SmartProxy
834
837
  if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0 && this.options.dnsScopes && this.options.dnsScopes.length > 0) {
838
+ const dnsServerDeps = ['SmartProxy'];
839
+ if (this.options.dbConfig?.enabled !== false) {
840
+ dnsServerDeps.push('ConfigManagers', 'EmailServer');
841
+ }
835
842
  this.serviceManager.addService(
836
843
  new plugins.taskbuffer.Service('DnsServer')
837
844
  .optional()
838
- .dependsOn('SmartProxy', ...((this.options.dbConfig?.enabled !== false) ? ['EmailServer'] : []))
845
+ .dependsOn(...dnsServerDeps)
839
846
  .withStart(async () => {
840
847
  await this.dnsServerRuntime.setup();
841
848
  })
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
2
2
  import { logger } from '../logger.js';
3
3
  import { buildEmailDnsRecords } from '../email/index.js';
4
4
  import type { DcRouter } from '../classes.dcrouter.js';
5
+ import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
5
6
 
6
7
  type TDnsRecordSeed = { name: string; type: string; value: string; ttl?: number; useIngressProxy?: boolean };
7
8
 
@@ -18,6 +19,8 @@ export class DnsServerRuntime {
18
19
  private batchCount = 0;
19
20
  private batchTimer: ReturnType<typeof setTimeout> | null = null;
20
21
 
22
+ private privateRouteHostnames = new Set<string>();
23
+ private privateRouteTargetIp?: string;
21
24
  constructor(private dcRouterRef: DcRouter) {}
22
25
 
23
26
  /**
@@ -58,6 +61,10 @@ export class DnsServerRuntime {
58
61
  }
59
62
  }
60
63
 
64
+ this.privateRouteTargetIp = plugins.net.isIP(vmIpAddress)
65
+ && vmIpAddress !== '0.0.0.0' && vmIpAddress !== '::'
66
+ ? vmIpAddress
67
+ : undefined;
61
68
  // Create DNS server instance with manual HTTPS mode
62
69
  const dnsServer = new plugins.smartdns.dnsServerMod.DnsServer({
63
70
  udpPort: 53,
@@ -72,6 +79,7 @@ export class DnsServerRuntime {
72
79
  httpsCert: ''
73
80
  });
74
81
  this.dcRouterRef.dnsServer = dnsServer;
82
+ this.registerPrivateRouteHandler(dnsServer);
75
83
 
76
84
  // Start the DNS server. smartdns owns UDP and DNS-over-TCP; dcrouter
77
85
  // keeps DNS-over-HTTPS routed through SmartProxy manual HTTPS mode.
@@ -172,6 +180,86 @@ export class DnsServerRuntime {
172
180
  this.dcRouterRef.mailDnsSync?.requestSync('DNS server attached');
173
181
  }
174
182
 
183
+ /**
184
+ * Keep an internal-only A-record overlay for exact route hostnames whose
185
+ * compiled source policy is private or whose only ingress is SmartVPN.
186
+ * Public routes compile without clientIp restrictions and are never added.
187
+ */
188
+ public syncPrivateRouteOverrides(routesArg: IDcRouterRouteConfig[]): void {
189
+ const nextHostnames = new Set<string>();
190
+ for (const route of routesArg) {
191
+ if (route.action?.type !== 'forward') continue;
192
+ const ingress = route.ingress;
193
+ if (!ingress?.directHub && !ingress?.smartVpn) continue;
194
+
195
+ const clientIp = route.match?.clientIp;
196
+ const clientIpEntries = Array.isArray(clientIp)
197
+ ? clientIp
198
+ : [];
199
+ const hasRestrictedSourceProfile = clientIpEntries
200
+ .map((entryArg) => entryArg.trim())
201
+ .filter(Boolean)
202
+ .some((entryArg) => entryArg !== '*');
203
+ const isSmartVpnOnly = ingress.smartVpn && !ingress.directHub;
204
+ if (!hasRestrictedSourceProfile && !isSmartVpnOnly) continue;
205
+
206
+ const domains = route.match?.domains;
207
+ const domainEntries = Array.isArray(domains)
208
+ ? domains
209
+ : typeof domains === 'string'
210
+ ? domains.split(',')
211
+ : [];
212
+ for (const domainArg of domainEntries) {
213
+ const hostname = this.normalizePrivateRouteHostname(domainArg);
214
+ if (hostname) nextHostnames.add(hostname);
215
+ }
216
+ }
217
+
218
+ const changed = nextHostnames.size !== this.privateRouteHostnames.size
219
+ || [...nextHostnames].some((hostnameArg) => !this.privateRouteHostnames.has(hostnameArg));
220
+ this.privateRouteHostnames = nextHostnames;
221
+ if (changed) {
222
+ logger.log(
223
+ 'info',
224
+ `DNS private-route overlay now serves ${nextHostnames.size} exact hostname(s)`,
225
+ { zone: 'dns' },
226
+ );
227
+ }
228
+ }
229
+
230
+ private registerPrivateRouteHandler(
231
+ dnsServerArg: plugins.smartdns.dnsServerMod.DnsServer,
232
+ ): void {
233
+ dnsServerArg.registerHandler('*', ['A'], (questionArg) => {
234
+ const hostname = this.normalizePrivateRouteHostname(questionArg.name);
235
+ if (
236
+ !hostname
237
+ || questionArg.type.toUpperCase() !== 'A'
238
+ || !this.privateRouteTargetIp
239
+ || !this.privateRouteHostnames.has(hostname)
240
+ ) {
241
+ return null;
242
+ }
243
+ return {
244
+ name: hostname,
245
+ type: 'A',
246
+ class: 'IN',
247
+ ttl: 60,
248
+ data: this.privateRouteTargetIp,
249
+ };
250
+ });
251
+ }
252
+
253
+ private normalizePrivateRouteHostname(hostnameArg: string): string | undefined {
254
+ const hostname = hostnameArg.trim().toLowerCase().replace(/\.$/, '');
255
+ if (!hostname || hostname.includes('*') || hostname.includes(',')) return undefined;
256
+ if (hostname.length > 253) return undefined;
257
+ const labels = hostname.split('.');
258
+ if (labels.some((labelArg) => !labelArg || labelArg.length > 63
259
+ || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(labelArg))) return undefined;
260
+ return hostname;
261
+ }
262
+
175
263
  /**
176
264
  * Create the DoH socket handler SmartProxy routes hand TLS sockets to.
177
265
  */
@@ -44,6 +44,7 @@ export class SecurityPolicyManager {
44
44
  private retentionSweepTimer?: ReturnType<typeof setInterval>;
45
45
  private retentionSweepPromise?: Promise<void>;
46
46
  private retentionMutationTail: Promise<void> = Promise.resolve();
47
+ private persistedIntelligenceCount?: number;
47
48
  private readonly inFlightObservations = new Map<string, Promise<void>>();
48
49
  private readonly queuedObservations = new Set<string>();
49
50
  private readonly observationQueue: string[] = [];
@@ -221,10 +222,11 @@ export class SecurityPolicyManager {
221
222
  return await this.withRetentionMutation(async () => {
222
223
  if (this.isStopping) return null;
223
224
  let doc = await IpIntelligenceDoc.findByIp(ip);
225
+ let isNewDocument = false;
226
+ let verifiedCount = this.persistedIntelligenceCount;
224
227
  if (!doc) {
225
- let countAfterPrune: number;
226
228
  try {
227
- countAfterPrune = await this.pruneIpIntelligence({ forCapacity: true });
229
+ verifiedCount ??= await this.pruneIpIntelligence({ forCapacity: true });
228
230
  } catch (error: unknown) {
229
231
  logger.log(
230
232
  'warn',
@@ -233,17 +235,18 @@ export class SecurityPolicyManager {
233
235
  );
234
236
  return null;
235
237
  }
236
- if (countAfterPrune >= IP_INTELLIGENCE_RETENTION_HIGH_WATER) {
238
+ if (verifiedCount >= IP_INTELLIGENCE_RETENTION_HIGH_WATER) {
237
239
  logger.log(
238
240
  'warn',
239
241
  'Skipping persistence for new IP ' + ip
240
- + ': cache remains at capacity (' + countAfterPrune + ')',
242
+ + ': cache remains at capacity (' + verifiedCount + ')',
241
243
  );
242
244
  return null;
243
245
  }
244
246
  doc = new IpIntelligenceDoc();
245
247
  doc.ipAddress = ip;
246
248
  doc.firstSeenAt = now;
249
+ isNewDocument = true;
247
250
  }
248
251
 
249
252
  Object.assign(doc, intelligence);
@@ -251,6 +254,9 @@ export class SecurityPolicyManager {
251
254
  doc.updatedAt = now;
252
255
  doc.seenCount = (doc.seenCount || 0) + 1;
253
256
  await doc.save();
257
+ if (isNewDocument) {
258
+ this.persistedIntelligenceCount = verifiedCount! + 1;
259
+ }
254
260
  return doc;
255
261
  });
256
262
  }
@@ -277,6 +283,7 @@ export class SecurityPolicyManager {
277
283
  }
278
284
 
279
285
  private async pruneIpIntelligence(options: { forCapacity: boolean }): Promise<number> {
286
+ this.persistedIntelligenceCount = undefined;
280
287
  let deletedCount = 0;
281
288
  const expiryCutoff = this.now() - IP_INTELLIGENCE_RETENTION_MAX_AGE_MS;
282
289
 
@@ -313,6 +320,7 @@ export class SecurityPolicyManager {
313
320
  if (deletedCount > 0) {
314
321
  logger.log('info', 'IP intelligence retention removed ' + deletedCount + ' cached record(s)');
315
322
  }
323
+ this.persistedIntelligenceCount = currentCount;
316
324
  return currentCount;
317
325
  }
318
326
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '17.9.1',
6
+ version: '17.9.3',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }