@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.
- package/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +7 -2
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +11 -0
- package/dist_ts/dns/classes.dns-server-runtime.js +81 -1
- package/dist_ts/security/classes.security-policy-manager.d.ts +1 -0
- package/dist_ts/security/classes.security-policy-manager.js +13 -5
- package/dist_ts_migrations/index.js +70 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +8 -1
- package/ts/dns/classes.dns-server-runtime.ts +88 -0
- package/ts/security/classes.security-policy-manager.ts +12 -4
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/dcrouter',
|
|
6
|
-
version: '17.9.
|
|
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.
|
|
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.
|
|
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",
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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 (
|
|
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 (' +
|
|
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
|
|