@serve.zone/dcrouter 17.1.3 → 17.1.5
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 +8 -2
- package/dist_ts/db/documents/classes.ip-intelligence.doc.d.ts +13 -1
- package/dist_ts/db/documents/classes.ip-intelligence.doc.js +81 -1
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +1 -0
- package/dist_ts/dns/classes.dns-server-runtime.js +8 -4
- package/dist_ts/email/classes.email-domain.manager.js +27 -15
- package/dist_ts/email/classes.mail-dns-sync.d.ts +1 -2
- package/dist_ts/email/classes.mail-dns-sync.js +58 -31
- package/dist_ts/email/index.d.ts +1 -0
- package/dist_ts/email/index.js +2 -1
- package/dist_ts/email/mail-dns-status.d.ts +5 -0
- package/dist_ts/email/mail-dns-status.js +31 -0
- package/dist_ts/security/classes.security-policy-manager.d.ts +19 -0
- package/dist_ts/security/classes.security-policy-manager.js +220 -28
- package/dist_ts_migrations/index.js +270 -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/db/documents/classes.ip-intelligence.doc.ts +103 -1
- package/ts/dns/classes.dns-server-runtime.ts +9 -3
- package/ts/email/classes.email-domain.manager.ts +23 -16
- package/ts/email/classes.mail-dns-sync.ts +67 -37
- package/ts/email/index.ts +1 -0
- package/ts/email/mail-dns-status.ts +35 -0
- package/ts/security/classes.security-policy-manager.ts +253 -28
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
|
|
13
13
|
export interface ISecurityPolicyManagerOptions {
|
|
14
14
|
intelligenceRefreshMs?: number;
|
|
15
|
+
retentionSweepIntervalMs?: number;
|
|
16
|
+
now?: () => number;
|
|
15
17
|
onPolicyChanged?: () => void | Promise<void>;
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -24,12 +26,24 @@ const OBSERVED_IP_BATCH_LIMIT = 20;
|
|
|
24
26
|
const OBSERVED_IP_QUEUE_CONCURRENCY = 2;
|
|
25
27
|
const OBSERVED_IP_REQUEUE_THROTTLE_MS = 60_000;
|
|
26
28
|
|
|
29
|
+
export const IP_INTELLIGENCE_LIST_LIMIT = 500;
|
|
30
|
+
export const IP_INTELLIGENCE_RETENTION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
31
|
+
export const IP_INTELLIGENCE_RETENTION_HIGH_WATER = 10_000;
|
|
32
|
+
export const IP_INTELLIGENCE_RETENTION_LOW_WATER = 9_000;
|
|
33
|
+
export const IP_INTELLIGENCE_RETENTION_BATCH_SIZE = 500;
|
|
34
|
+
export const IP_INTELLIGENCE_RETENTION_SWEEP_INTERVAL_MS = 60 * 60 * 1000;
|
|
35
|
+
|
|
27
36
|
export class SecurityPolicyManager {
|
|
28
37
|
private readonly smartNetwork = new plugins.smartnetwork.SmartNetwork({
|
|
29
38
|
cacheTtl: 24 * 60 * 60 * 1000,
|
|
30
39
|
ipIntelligenceTimeout: 5_000,
|
|
31
40
|
});
|
|
32
41
|
private readonly intelligenceRefreshMs: number;
|
|
42
|
+
private readonly retentionSweepIntervalMs: number;
|
|
43
|
+
private readonly now: () => number;
|
|
44
|
+
private retentionSweepTimer?: ReturnType<typeof setInterval>;
|
|
45
|
+
private retentionSweepPromise?: Promise<void>;
|
|
46
|
+
private retentionMutationTail: Promise<void> = Promise.resolve();
|
|
33
47
|
private readonly inFlightObservations = new Map<string, Promise<void>>();
|
|
34
48
|
private readonly queuedObservations = new Set<string>();
|
|
35
49
|
private readonly observationQueue: string[] = [];
|
|
@@ -41,18 +55,34 @@ export class SecurityPolicyManager {
|
|
|
41
55
|
|
|
42
56
|
constructor(options: ISecurityPolicyManagerOptions = {}) {
|
|
43
57
|
this.intelligenceRefreshMs = options.intelligenceRefreshMs ?? 24 * 60 * 60 * 1000;
|
|
58
|
+
this.retentionSweepIntervalMs = options.retentionSweepIntervalMs
|
|
59
|
+
?? IP_INTELLIGENCE_RETENTION_SWEEP_INTERVAL_MS;
|
|
60
|
+
this.now = options.now ?? Date.now;
|
|
44
61
|
this.onPolicyChanged = options.onPolicyChanged;
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
public async start(): Promise<void> {
|
|
65
|
+
this.isStopping = false;
|
|
66
|
+
await this.requestRetentionSweep();
|
|
67
|
+
this.retentionSweepTimer = setInterval(() => {
|
|
68
|
+
void this.requestRetentionSweep();
|
|
69
|
+
}, this.retentionSweepIntervalMs);
|
|
70
|
+
this.retentionSweepTimer.unref?.();
|
|
48
71
|
logger.log('info', 'SecurityPolicyManager started');
|
|
49
72
|
}
|
|
50
73
|
|
|
51
74
|
public async stop(): Promise<void> {
|
|
52
75
|
this.isStopping = true;
|
|
76
|
+
if (this.retentionSweepTimer) {
|
|
77
|
+
clearInterval(this.retentionSweepTimer);
|
|
78
|
+
this.retentionSweepTimer = undefined;
|
|
79
|
+
}
|
|
53
80
|
this.observationQueue.length = 0;
|
|
54
81
|
this.queuedObservations.clear();
|
|
55
82
|
await this.smartNetwork.stop();
|
|
83
|
+
await Promise.allSettled([...this.inFlightObservations.values()]);
|
|
84
|
+
await this.retentionSweepPromise;
|
|
85
|
+
await this.retentionMutationTail;
|
|
56
86
|
}
|
|
57
87
|
|
|
58
88
|
public async observeIps(ips: string[]): Promise<void> {
|
|
@@ -88,6 +118,7 @@ export class SecurityPolicyManager {
|
|
|
88
118
|
}
|
|
89
119
|
|
|
90
120
|
public async observeIp(ipAddress: string, options: { force?: boolean } = {}): Promise<void> {
|
|
121
|
+
if (this.isStopping) return;
|
|
91
122
|
const ip = this.normalizeIp(ipAddress);
|
|
92
123
|
if (!ip || !this.isPublicIp(ip)) {
|
|
93
124
|
return;
|
|
@@ -110,7 +141,7 @@ export class SecurityPolicyManager {
|
|
|
110
141
|
|
|
111
142
|
private async performObserveIp(ip: string, options: { force?: boolean } = {}): Promise<void> {
|
|
112
143
|
try {
|
|
113
|
-
const now =
|
|
144
|
+
const now = this.now();
|
|
114
145
|
let doc = await IpIntelligenceDoc.findByIp(ip);
|
|
115
146
|
if (doc && !options.force && now - doc.updatedAt < this.intelligenceRefreshMs) {
|
|
116
147
|
if (now - doc.lastSeenAt > 60_000) {
|
|
@@ -122,16 +153,18 @@ export class SecurityPolicyManager {
|
|
|
122
153
|
}
|
|
123
154
|
|
|
124
155
|
const intelligence = await this.smartNetwork.getIpIntelligence(ip);
|
|
125
|
-
if (
|
|
126
|
-
doc
|
|
127
|
-
doc.
|
|
128
|
-
doc.
|
|
156
|
+
if (doc) {
|
|
157
|
+
Object.assign(doc, intelligence);
|
|
158
|
+
doc.lastSeenAt = now;
|
|
159
|
+
doc.updatedAt = now;
|
|
160
|
+
doc.seenCount = (doc.seenCount || 0) + 1;
|
|
161
|
+
await doc.save();
|
|
162
|
+
} else {
|
|
163
|
+
doc = await this.persistNewIntelligence(ip, intelligence, now);
|
|
164
|
+
if (!doc) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
129
167
|
}
|
|
130
|
-
Object.assign(doc, intelligence);
|
|
131
|
-
doc.lastSeenAt = now;
|
|
132
|
-
doc.updatedAt = now;
|
|
133
|
-
doc.seenCount = (doc.seenCount || 0) + 1;
|
|
134
|
-
await doc.save();
|
|
135
168
|
|
|
136
169
|
if (await this.matchesAnyReactiveRule(doc)) {
|
|
137
170
|
await this.notifyPolicyChanged();
|
|
@@ -147,20 +180,154 @@ export class SecurityPolicyManager {
|
|
|
147
180
|
|
|
148
181
|
public async listIpIntelligence(options: { ipAddresses?: string[]; limit?: number } = {}): Promise<IIpIntelligenceRecord[]> {
|
|
149
182
|
const limit = Number.isInteger(options.limit) && options.limit! > 0
|
|
150
|
-
? Math.min(options.limit!,
|
|
151
|
-
:
|
|
152
|
-
|
|
153
|
-
let
|
|
154
|
-
if (options.ipAddresses
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
? Math.min(options.limit!, IP_INTELLIGENCE_LIST_LIMIT)
|
|
184
|
+
: IP_INTELLIGENCE_LIST_LIMIT;
|
|
185
|
+
|
|
186
|
+
let ipAddresses: string[] | undefined;
|
|
187
|
+
if (options.ipAddresses !== undefined) {
|
|
188
|
+
if (!Array.isArray(options.ipAddresses)) {
|
|
189
|
+
throw new Error('ipAddresses must be an array');
|
|
190
|
+
}
|
|
191
|
+
const normalizedIps = options.ipAddresses.map((ipAddress, index) => {
|
|
192
|
+
if (typeof ipAddress !== 'string') {
|
|
193
|
+
throw new Error('ipAddresses[' + index + '] must be a string');
|
|
194
|
+
}
|
|
195
|
+
const normalizedIp = this.normalizeIp(ipAddress);
|
|
196
|
+
if (!normalizedIp) {
|
|
197
|
+
throw new Error('ipAddresses[' + index + '] is not a valid IP address');
|
|
198
|
+
}
|
|
199
|
+
return normalizedIp;
|
|
200
|
+
});
|
|
201
|
+
ipAddresses = [...new Set(normalizedIps)];
|
|
202
|
+
if (ipAddresses.length > IP_INTELLIGENCE_LIST_LIMIT) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
'ipAddresses may contain at most ' + IP_INTELLIGENCE_LIST_LIMIT + ' unique addresses',
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (ipAddresses.length === 0) {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
160
210
|
}
|
|
161
211
|
|
|
162
|
-
const
|
|
163
|
-
return
|
|
212
|
+
const docs = await IpIntelligenceDoc.findRecent({ ipAddresses, limit });
|
|
213
|
+
return docs.map((doc) => this.intelligenceFromDoc(doc));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async persistNewIntelligence(
|
|
217
|
+
ip: string,
|
|
218
|
+
intelligence: Awaited<ReturnType<plugins.smartnetwork.SmartNetwork['getIpIntelligence']>>,
|
|
219
|
+
now: number,
|
|
220
|
+
): Promise<IpIntelligenceDoc | null> {
|
|
221
|
+
return await this.withRetentionMutation(async () => {
|
|
222
|
+
if (this.isStopping) return null;
|
|
223
|
+
let doc = await IpIntelligenceDoc.findByIp(ip);
|
|
224
|
+
if (!doc) {
|
|
225
|
+
let countAfterPrune: number;
|
|
226
|
+
try {
|
|
227
|
+
countAfterPrune = await this.pruneIpIntelligence({ forCapacity: true });
|
|
228
|
+
} catch (error: unknown) {
|
|
229
|
+
logger.log(
|
|
230
|
+
'warn',
|
|
231
|
+
'Skipping persistence for new IP ' + ip
|
|
232
|
+
+ ': cache capacity could not be verified: ' + (error as Error).message,
|
|
233
|
+
);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
if (countAfterPrune >= IP_INTELLIGENCE_RETENTION_HIGH_WATER) {
|
|
237
|
+
logger.log(
|
|
238
|
+
'warn',
|
|
239
|
+
'Skipping persistence for new IP ' + ip
|
|
240
|
+
+ ': cache remains at capacity (' + countAfterPrune + ')',
|
|
241
|
+
);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
doc = new IpIntelligenceDoc();
|
|
245
|
+
doc.ipAddress = ip;
|
|
246
|
+
doc.firstSeenAt = now;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
Object.assign(doc, intelligence);
|
|
250
|
+
doc.lastSeenAt = now;
|
|
251
|
+
doc.updatedAt = now;
|
|
252
|
+
doc.seenCount = (doc.seenCount || 0) + 1;
|
|
253
|
+
await doc.save();
|
|
254
|
+
return doc;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private async requestRetentionSweep(): Promise<void> {
|
|
259
|
+
if (this.retentionSweepPromise) {
|
|
260
|
+
return await this.retentionSweepPromise;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const sweepPromise = this.withRetentionMutation(async () => {
|
|
264
|
+
await this.pruneIpIntelligence({ forCapacity: false });
|
|
265
|
+
}).catch((error: unknown) => {
|
|
266
|
+
logger.log(
|
|
267
|
+
'warn',
|
|
268
|
+
'IP intelligence retention sweep failed: ' + (error as Error).message,
|
|
269
|
+
);
|
|
270
|
+
}).finally(() => {
|
|
271
|
+
if (this.retentionSweepPromise === sweepPromise) {
|
|
272
|
+
this.retentionSweepPromise = undefined;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
this.retentionSweepPromise = sweepPromise;
|
|
276
|
+
return await sweepPromise;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async pruneIpIntelligence(options: { forCapacity: boolean }): Promise<number> {
|
|
280
|
+
let deletedCount = 0;
|
|
281
|
+
const expiryCutoff = this.now() - IP_INTELLIGENCE_RETENTION_MAX_AGE_MS;
|
|
282
|
+
|
|
283
|
+
while (!this.isStopping) {
|
|
284
|
+
const expiredIds = await IpIntelligenceDoc.findOldestIds({
|
|
285
|
+
lastSeenBefore: expiryCutoff,
|
|
286
|
+
limit: IP_INTELLIGENCE_RETENTION_BATCH_SIZE,
|
|
287
|
+
});
|
|
288
|
+
if (expiredIds.length === 0) break;
|
|
289
|
+
deletedCount += await IpIntelligenceDoc.deleteByDocumentIds(expiredIds);
|
|
290
|
+
if (expiredIds.length < IP_INTELLIGENCE_RETENTION_BATCH_SIZE) break;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let currentCount = await IpIntelligenceDoc.countAll();
|
|
294
|
+
const shouldPruneExcess = currentCount > IP_INTELLIGENCE_RETENTION_HIGH_WATER
|
|
295
|
+
|| (options.forCapacity && currentCount >= IP_INTELLIGENCE_RETENTION_HIGH_WATER);
|
|
296
|
+
if (shouldPruneExcess) {
|
|
297
|
+
let remainingToDelete = currentCount - IP_INTELLIGENCE_RETENTION_LOW_WATER;
|
|
298
|
+
while (remainingToDelete > 0 && !this.isStopping) {
|
|
299
|
+
const batchLimit = Math.min(
|
|
300
|
+
remainingToDelete,
|
|
301
|
+
IP_INTELLIGENCE_RETENTION_BATCH_SIZE,
|
|
302
|
+
);
|
|
303
|
+
const oldestIds = await IpIntelligenceDoc.findOldestIds({ limit: batchLimit });
|
|
304
|
+
if (oldestIds.length === 0) break;
|
|
305
|
+
const deletedThisBatch = await IpIntelligenceDoc.deleteByDocumentIds(oldestIds);
|
|
306
|
+
deletedCount += deletedThisBatch;
|
|
307
|
+
currentCount -= deletedThisBatch;
|
|
308
|
+
remainingToDelete -= deletedThisBatch;
|
|
309
|
+
if (deletedThisBatch === 0 || oldestIds.length < batchLimit) break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (deletedCount > 0) {
|
|
314
|
+
logger.log('info', 'IP intelligence retention removed ' + deletedCount + ' cached record(s)');
|
|
315
|
+
}
|
|
316
|
+
return currentCount;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private async withRetentionMutation<T>(operation: () => Promise<T>): Promise<T> {
|
|
320
|
+
const previous = this.retentionMutationTail;
|
|
321
|
+
let release!: () => void;
|
|
322
|
+
this.retentionMutationTail = new Promise<void>((resolve) => {
|
|
323
|
+
release = resolve;
|
|
324
|
+
});
|
|
325
|
+
await previous;
|
|
326
|
+
try {
|
|
327
|
+
return await operation();
|
|
328
|
+
} finally {
|
|
329
|
+
release();
|
|
330
|
+
}
|
|
164
331
|
}
|
|
165
332
|
|
|
166
333
|
public async refreshIpIntelligence(ipAddress: string): Promise<IIpIntelligenceRecord | null> {
|
|
@@ -299,9 +466,9 @@ export class SecurityPolicyManager {
|
|
|
299
466
|
|
|
300
467
|
public async compilePolicy(): Promise<ISecurityCompiledPolicy> {
|
|
301
468
|
const rules = await SecurityBlockRuleDoc.findEnabled();
|
|
302
|
-
const intelligenceDocs = await IpIntelligenceDoc.findAll();
|
|
303
469
|
const blockedIps = new Set<string>();
|
|
304
470
|
const blockedCidrs = new Set<string>();
|
|
471
|
+
const reactiveRules: SecurityBlockRuleDoc[] = [];
|
|
305
472
|
|
|
306
473
|
for (const rule of rules) {
|
|
307
474
|
const normalizedValue = rule.value.trim();
|
|
@@ -320,7 +487,32 @@ export class SecurityPolicyManager {
|
|
|
320
487
|
continue;
|
|
321
488
|
}
|
|
322
489
|
|
|
323
|
-
|
|
490
|
+
if (this.getReactiveRuleQuery(rule)) {
|
|
491
|
+
reactiveRules.push(rule);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (reactiveRules.length === 0) {
|
|
496
|
+
return this.compiledPolicyFromSets(blockedIps, blockedCidrs);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const candidateDocs = new Map<string, IpIntelligenceDoc>();
|
|
500
|
+
for (const rule of reactiveRules) {
|
|
501
|
+
const query = this.getReactiveRuleQuery(rule)!;
|
|
502
|
+
const docs = query.type === 'asn'
|
|
503
|
+
? await IpIntelligenceDoc.findByAsn(query.asn)
|
|
504
|
+
: await IpIntelligenceDoc.findByOrganization(query.organization, query.matchMode);
|
|
505
|
+
for (const doc of docs) {
|
|
506
|
+
const documentId = (doc as IpIntelligenceDoc & { _id?: unknown })._id;
|
|
507
|
+
const candidateKey = documentId === undefined
|
|
508
|
+
? `ip:${doc.ipAddress}`
|
|
509
|
+
: `id:${String(documentId)}`;
|
|
510
|
+
candidateDocs.set(candidateKey, doc);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
for (const rule of reactiveRules) {
|
|
515
|
+
for (const doc of candidateDocs.values()) {
|
|
324
516
|
if (!this.ruleMatchesIntelligence(rule, doc)) continue;
|
|
325
517
|
const networkEntries = this.normalizeNetworkEntryList([
|
|
326
518
|
...(doc.networkCidrs || []),
|
|
@@ -336,12 +528,45 @@ export class SecurityPolicyManager {
|
|
|
336
528
|
}
|
|
337
529
|
}
|
|
338
530
|
|
|
531
|
+
return this.compiledPolicyFromSets(blockedIps, blockedCidrs);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private compiledPolicyFromSets(
|
|
535
|
+
blockedIps: Set<string>,
|
|
536
|
+
blockedCidrs: Set<string>,
|
|
537
|
+
): ISecurityCompiledPolicy {
|
|
339
538
|
return {
|
|
340
539
|
blockedIps: [...blockedIps].sort(),
|
|
341
540
|
blockedCidrs: [...blockedCidrs].sort(),
|
|
342
541
|
};
|
|
343
542
|
}
|
|
344
543
|
|
|
544
|
+
private getReactiveRuleQuery(rule: SecurityBlockRuleDoc):
|
|
545
|
+
| { type: 'asn'; asn: number }
|
|
546
|
+
| { type: 'organization'; organization: string; matchMode: TSecurityBlockRuleMatchMode }
|
|
547
|
+
| undefined {
|
|
548
|
+
const value = rule.value.trim();
|
|
549
|
+
if (!value) return undefined;
|
|
550
|
+
|
|
551
|
+
if (rule.type === 'asn') {
|
|
552
|
+
const normalizedAsn = value.replace(/^as/i, '');
|
|
553
|
+
if (!/^\d+$/.test(normalizedAsn)) return undefined;
|
|
554
|
+
const asn = Number(normalizedAsn);
|
|
555
|
+
if (!Number.isSafeInteger(asn) || String(asn) !== normalizedAsn) return undefined;
|
|
556
|
+
return { type: 'asn', asn };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (rule.type === 'organization') {
|
|
560
|
+
return {
|
|
561
|
+
type: 'organization',
|
|
562
|
+
organization: value,
|
|
563
|
+
matchMode: rule.matchMode === 'exact' ? 'exact' : 'contains',
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
|
|
345
570
|
public async compileSmartProxyPolicy(): Promise<ISecurityCompiledPolicy> {
|
|
346
571
|
return await this.compilePolicy();
|
|
347
572
|
}
|
|
@@ -385,10 +610,10 @@ export class SecurityPolicyManager {
|
|
|
385
610
|
|
|
386
611
|
private normalizeIp(ipAddress: string): string | undefined {
|
|
387
612
|
const ip = ipAddress.trim();
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
return plugins.net.isIP(
|
|
613
|
+
const normalizedIp = ip.startsWith('::ffff:')
|
|
614
|
+
? ip.slice('::ffff:'.length)
|
|
615
|
+
: ip;
|
|
616
|
+
return plugins.net.isIP(normalizedIp) ? normalizedIp : undefined;
|
|
392
617
|
}
|
|
393
618
|
|
|
394
619
|
private normalizeCidr(value: string): string | undefined {
|