@serve.zone/dcrouter 13.21.1 → 13.23.0
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/dist_serve/bundle.js +26 -4
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +6 -0
- package/dist_ts/classes.dcrouter.js +83 -5
- package/dist_ts/config/classes.route-config-manager.d.ts +1 -1
- package/dist_ts/config/classes.route-config-manager.js +2 -2
- package/dist_ts/db/documents/classes.ip-intelligence.doc.d.ts +25 -0
- package/dist_ts/db/documents/classes.ip-intelligence.doc.js +175 -0
- package/dist_ts/db/documents/classes.security-block-rule.doc.d.ts +17 -0
- package/dist_ts/db/documents/classes.security-block-rule.doc.js +124 -0
- package/dist_ts/db/documents/classes.security-policy-audit.doc.d.ts +11 -0
- package/dist_ts/db/documents/classes.security-policy-audit.doc.js +95 -0
- package/dist_ts/db/documents/index.d.ts +3 -0
- package/dist_ts/db/documents/index.js +4 -1
- package/dist_ts/monitoring/classes.metricsmanager.js +2 -1
- package/dist_ts/opsserver/handlers/config.handler.js +2 -1
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +3 -1
- package/dist_ts/opsserver/handlers/security.handler.js +42 -1
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +10 -0
- package/dist_ts/remoteingress/classes.remoteingress-manager.js +9 -1
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +3 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.js +23 -5
- package/dist_ts/security/classes.security-policy-manager.d.ts +41 -0
- package/dist_ts/security/classes.security-policy-manager.js +283 -0
- package/dist_ts/security/index.d.ts +1 -0
- package/dist_ts/security/index.js +2 -1
- package/dist_ts_apiclient/classes.remoteingress.d.ts +2 -0
- package/dist_ts_apiclient/classes.remoteingress.js +7 -1
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/remoteingress.d.ts +51 -0
- package/dist_ts_interfaces/data/security-policy.d.ts +32 -0
- package/dist_ts_interfaces/data/security-policy.js +2 -0
- package/dist_ts_interfaces/requests/config.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/security-policy.d.ts +64 -0
- package/dist_ts_interfaces/requests/security-policy.js +2 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/network/ops-view-remoteingress.d.ts +5 -0
- package/dist_ts_web/elements/network/ops-view-remoteingress.js +69 -1
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +106 -6
- package/ts/config/classes.route-config-manager.ts +2 -2
- package/ts/db/documents/classes.ip-intelligence.doc.ts +75 -0
- package/ts/db/documents/classes.security-block-rule.doc.ts +52 -0
- package/ts/db/documents/classes.security-policy-audit.doc.ts +33 -0
- package/ts/db/documents/index.ts +3 -0
- package/ts/monitoring/classes.metricsmanager.ts +2 -0
- package/ts/opsserver/handlers/config.handler.ts +1 -0
- package/ts/opsserver/handlers/remoteingress.handler.ts +2 -0
- package/ts/opsserver/handlers/security.handler.ts +69 -0
- package/ts/remoteingress/classes.remoteingress-manager.ts +15 -2
- package/ts/remoteingress/classes.tunnel-manager.ts +25 -5
- package/ts/security/classes.security-policy-manager.ts +315 -0
- package/ts/security/index.ts +7 -1
- package/ts_apiclient/classes.remoteingress.ts +6 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/network/ops-view-remoteingress.ts +68 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
import { IpIntelligenceDoc, SecurityBlockRuleDoc, SecurityPolicyAuditDoc } from '../db/index.js';
|
|
4
|
+
import type {
|
|
5
|
+
IIpIntelligenceRecord,
|
|
6
|
+
ISecurityBlockRule,
|
|
7
|
+
ISecurityCompiledPolicy,
|
|
8
|
+
TSecurityBlockRuleMatchMode,
|
|
9
|
+
TSecurityBlockRuleType,
|
|
10
|
+
} from '../../ts_interfaces/data/security-policy.js';
|
|
11
|
+
|
|
12
|
+
export interface ISecurityPolicyManagerOptions {
|
|
13
|
+
intelligenceRefreshMs?: number;
|
|
14
|
+
onPolicyChanged?: () => void | Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface IRemoteIngressFirewallSnapshot {
|
|
18
|
+
blockedIps?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class SecurityPolicyManager {
|
|
22
|
+
private readonly smartNetwork = new plugins.smartnetwork.SmartNetwork({
|
|
23
|
+
cacheTtl: 24 * 60 * 60 * 1000,
|
|
24
|
+
});
|
|
25
|
+
private readonly intelligenceRefreshMs: number;
|
|
26
|
+
private readonly inFlightObservations = new Set<string>();
|
|
27
|
+
private readonly onPolicyChanged?: () => void | Promise<void>;
|
|
28
|
+
|
|
29
|
+
constructor(options: ISecurityPolicyManagerOptions = {}) {
|
|
30
|
+
this.intelligenceRefreshMs = options.intelligenceRefreshMs ?? 24 * 60 * 60 * 1000;
|
|
31
|
+
this.onPolicyChanged = options.onPolicyChanged;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public async start(): Promise<void> {
|
|
35
|
+
logger.log('info', 'SecurityPolicyManager started');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public async stop(): Promise<void> {
|
|
39
|
+
await this.smartNetwork.stop();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async observeIps(ips: string[]): Promise<void> {
|
|
43
|
+
const uniqueIps = [...new Set(ips.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])];
|
|
44
|
+
await Promise.allSettled(uniqueIps.map((ip) => this.observeIp(ip)));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async observeIp(ipAddress: string): Promise<void> {
|
|
48
|
+
const ip = this.normalizeIp(ipAddress);
|
|
49
|
+
if (!ip || !this.isPublicIp(ip) || this.inFlightObservations.has(ip)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.inFlightObservations.add(ip);
|
|
54
|
+
try {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
let doc = await IpIntelligenceDoc.findByIp(ip);
|
|
57
|
+
if (doc && now - doc.updatedAt < this.intelligenceRefreshMs) {
|
|
58
|
+
if (now - doc.lastSeenAt > 60_000) {
|
|
59
|
+
doc.lastSeenAt = now;
|
|
60
|
+
doc.seenCount = (doc.seenCount || 0) + 1;
|
|
61
|
+
await doc.save();
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const intelligence = await this.smartNetwork.getIpIntelligence(ip);
|
|
67
|
+
if (!doc) {
|
|
68
|
+
doc = new IpIntelligenceDoc();
|
|
69
|
+
doc.ipAddress = ip;
|
|
70
|
+
doc.firstSeenAt = now;
|
|
71
|
+
}
|
|
72
|
+
Object.assign(doc, intelligence);
|
|
73
|
+
doc.lastSeenAt = now;
|
|
74
|
+
doc.updatedAt = now;
|
|
75
|
+
doc.seenCount = (doc.seenCount || 0) + 1;
|
|
76
|
+
await doc.save();
|
|
77
|
+
|
|
78
|
+
if (await this.matchesAnyReactiveRule(doc)) {
|
|
79
|
+
await this.notifyPolicyChanged();
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
logger.log('warn', `Failed to enrich IP ${ip}: ${(err as Error).message}`);
|
|
83
|
+
} finally {
|
|
84
|
+
this.inFlightObservations.delete(ip);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public async listBlockRules(): Promise<ISecurityBlockRule[]> {
|
|
89
|
+
return (await SecurityBlockRuleDoc.findAll()).map((doc) => this.ruleFromDoc(doc));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public async listIpIntelligence(): Promise<IIpIntelligenceRecord[]> {
|
|
93
|
+
return (await IpIntelligenceDoc.findAll()).map((doc) => ({
|
|
94
|
+
ipAddress: doc.ipAddress,
|
|
95
|
+
asn: doc.asn,
|
|
96
|
+
asnOrg: doc.asnOrg,
|
|
97
|
+
registrantOrg: doc.registrantOrg,
|
|
98
|
+
registrantCountry: doc.registrantCountry,
|
|
99
|
+
networkRange: doc.networkRange,
|
|
100
|
+
abuseContact: doc.abuseContact,
|
|
101
|
+
country: doc.country,
|
|
102
|
+
countryCode: doc.countryCode,
|
|
103
|
+
city: doc.city,
|
|
104
|
+
latitude: doc.latitude,
|
|
105
|
+
longitude: doc.longitude,
|
|
106
|
+
accuracyRadius: doc.accuracyRadius,
|
|
107
|
+
timezone: doc.timezone,
|
|
108
|
+
firstSeenAt: doc.firstSeenAt,
|
|
109
|
+
lastSeenAt: doc.lastSeenAt,
|
|
110
|
+
updatedAt: doc.updatedAt,
|
|
111
|
+
seenCount: doc.seenCount,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public async createBlockRule(input: {
|
|
116
|
+
type: TSecurityBlockRuleType;
|
|
117
|
+
value: string;
|
|
118
|
+
matchMode?: TSecurityBlockRuleMatchMode;
|
|
119
|
+
reason?: string;
|
|
120
|
+
enabled?: boolean;
|
|
121
|
+
}, actor = 'system'): Promise<ISecurityBlockRule> {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
const doc = new SecurityBlockRuleDoc();
|
|
124
|
+
doc.id = plugins.uuid.v4();
|
|
125
|
+
doc.type = input.type;
|
|
126
|
+
doc.value = input.value.trim();
|
|
127
|
+
doc.matchMode = input.matchMode;
|
|
128
|
+
doc.reason = input.reason;
|
|
129
|
+
doc.enabled = input.enabled ?? true;
|
|
130
|
+
doc.createdAt = now;
|
|
131
|
+
doc.updatedAt = now;
|
|
132
|
+
doc.createdBy = actor;
|
|
133
|
+
await doc.save();
|
|
134
|
+
await this.writeAudit('createBlockRule', actor, { rule: this.ruleFromDoc(doc) });
|
|
135
|
+
await this.notifyPolicyChanged();
|
|
136
|
+
return this.ruleFromDoc(doc);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public async updateBlockRule(id: string, patch: Partial<Pick<ISecurityBlockRule, 'value' | 'matchMode' | 'reason' | 'enabled'>>, actor = 'system'): Promise<ISecurityBlockRule | null> {
|
|
140
|
+
const doc = await SecurityBlockRuleDoc.findById(id);
|
|
141
|
+
if (!doc) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (patch.value !== undefined) doc.value = patch.value.trim();
|
|
145
|
+
if (patch.matchMode !== undefined) doc.matchMode = patch.matchMode;
|
|
146
|
+
if (patch.reason !== undefined) doc.reason = patch.reason;
|
|
147
|
+
if (patch.enabled !== undefined) doc.enabled = patch.enabled;
|
|
148
|
+
doc.updatedAt = Date.now();
|
|
149
|
+
await doc.save();
|
|
150
|
+
await this.writeAudit('updateBlockRule', actor, { id, patch });
|
|
151
|
+
await this.notifyPolicyChanged();
|
|
152
|
+
return this.ruleFromDoc(doc);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public async deleteBlockRule(id: string, actor = 'system'): Promise<boolean> {
|
|
156
|
+
const doc = await SecurityBlockRuleDoc.findById(id);
|
|
157
|
+
if (!doc) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
await doc.delete();
|
|
161
|
+
await this.writeAudit('deleteBlockRule', actor, { id });
|
|
162
|
+
await this.notifyPolicyChanged();
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public async compilePolicy(): Promise<ISecurityCompiledPolicy> {
|
|
167
|
+
const rules = await SecurityBlockRuleDoc.findEnabled();
|
|
168
|
+
const intelligenceDocs = await IpIntelligenceDoc.findAll();
|
|
169
|
+
const blockedIps = new Set<string>();
|
|
170
|
+
const blockedCidrs = new Set<string>();
|
|
171
|
+
|
|
172
|
+
for (const rule of rules) {
|
|
173
|
+
const normalizedValue = rule.value.trim();
|
|
174
|
+
if (!normalizedValue) continue;
|
|
175
|
+
|
|
176
|
+
if (rule.type === 'ip') {
|
|
177
|
+
const ip = this.normalizeIp(normalizedValue);
|
|
178
|
+
if (ip && plugins.net.isIP(ip)) blockedIps.add(ip);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (rule.type === 'cidr') {
|
|
183
|
+
const cidr = this.normalizeCidr(normalizedValue);
|
|
184
|
+
if (cidr) blockedCidrs.add(cidr);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const doc of intelligenceDocs) {
|
|
189
|
+
if (!this.ruleMatchesIntelligence(rule, doc)) continue;
|
|
190
|
+
const cidr = this.normalizeCidr(doc.networkRange || '');
|
|
191
|
+
if (cidr) {
|
|
192
|
+
blockedCidrs.add(cidr);
|
|
193
|
+
} else if (this.normalizeIp(doc.ipAddress)) {
|
|
194
|
+
blockedIps.add(this.normalizeIp(doc.ipAddress)!);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
blockedIps: [...blockedIps].sort(),
|
|
201
|
+
blockedCidrs: [...blockedCidrs].sort(),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public async compileSmartProxyPolicy(): Promise<ISecurityCompiledPolicy> {
|
|
206
|
+
return await this.compilePolicy();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public async compileRemoteIngressFirewall(): Promise<IRemoteIngressFirewallSnapshot | undefined> {
|
|
210
|
+
const policy = await this.compilePolicy();
|
|
211
|
+
const blockedIps = [
|
|
212
|
+
...policy.blockedIps.filter((ip) => plugins.net.isIP(ip) === 4),
|
|
213
|
+
...policy.blockedCidrs.filter((cidr) => plugins.net.isIP(cidr.split('/')[0]) === 4),
|
|
214
|
+
];
|
|
215
|
+
return blockedIps.length > 0 ? { blockedIps } : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private async matchesAnyReactiveRule(doc: IpIntelligenceDoc): Promise<boolean> {
|
|
219
|
+
const rules = await SecurityBlockRuleDoc.findEnabled();
|
|
220
|
+
return rules.some((rule) => rule.type === 'asn' || rule.type === 'organization'
|
|
221
|
+
? this.ruleMatchesIntelligence(rule, doc)
|
|
222
|
+
: false);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private ruleMatchesIntelligence(rule: SecurityBlockRuleDoc, doc: IpIntelligenceDoc): boolean {
|
|
226
|
+
const value = rule.value.trim().toLowerCase();
|
|
227
|
+
if (!value) return false;
|
|
228
|
+
|
|
229
|
+
if (rule.type === 'asn') {
|
|
230
|
+
return String(doc.asn ?? '') === value.replace(/^as/i, '');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (rule.type === 'organization') {
|
|
234
|
+
const candidates = [doc.asnOrg, doc.registrantOrg]
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
.map((candidate) => candidate!.toLowerCase());
|
|
237
|
+
if (rule.matchMode === 'exact') {
|
|
238
|
+
return candidates.some((candidate) => candidate === value);
|
|
239
|
+
}
|
|
240
|
+
return candidates.some((candidate) => candidate.includes(value));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private normalizeIp(ipAddress: string): string | undefined {
|
|
247
|
+
const ip = ipAddress.trim();
|
|
248
|
+
if (ip.startsWith('::ffff:')) {
|
|
249
|
+
return ip.slice('::ffff:'.length);
|
|
250
|
+
}
|
|
251
|
+
return plugins.net.isIP(ip) ? ip : undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private normalizeCidr(value: string): string | undefined {
|
|
255
|
+
const [rawIp, rawPrefix] = value.trim().split('/');
|
|
256
|
+
if (!rawIp || !rawPrefix) return undefined;
|
|
257
|
+
const ip = this.normalizeIp(rawIp);
|
|
258
|
+
if (!ip) return undefined;
|
|
259
|
+
const prefix = Number(rawPrefix);
|
|
260
|
+
const maxPrefix = plugins.net.isIP(ip) === 4 ? 32 : 128;
|
|
261
|
+
if (!Number.isInteger(prefix) || prefix < 0 || prefix > maxPrefix) return undefined;
|
|
262
|
+
return `${ip}/${prefix}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private isPublicIp(ip: string): boolean {
|
|
266
|
+
const family = plugins.net.isIP(ip);
|
|
267
|
+
if (family === 4) {
|
|
268
|
+
const parts = ip.split('.').map((part) => Number(part));
|
|
269
|
+
const [a, b] = parts;
|
|
270
|
+
if (a === 10 || a === 127 || a === 0 || a >= 224) return false;
|
|
271
|
+
if (a === 100 && b >= 64 && b <= 127) return false;
|
|
272
|
+
if (a === 169 && b === 254) return false;
|
|
273
|
+
if (a === 172 && b >= 16 && b <= 31) return false;
|
|
274
|
+
if (a === 192 && b === 168) return false;
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
if (family === 6) {
|
|
278
|
+
const lower = ip.toLowerCase();
|
|
279
|
+
if (lower === '::1' || lower === '::') return false;
|
|
280
|
+
if (lower.startsWith('fe80:') || lower.startsWith('fc') || lower.startsWith('fd')) return false;
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private ruleFromDoc(doc: SecurityBlockRuleDoc): ISecurityBlockRule {
|
|
287
|
+
return {
|
|
288
|
+
id: doc.id,
|
|
289
|
+
type: doc.type,
|
|
290
|
+
value: doc.value,
|
|
291
|
+
matchMode: doc.matchMode,
|
|
292
|
+
enabled: doc.enabled,
|
|
293
|
+
reason: doc.reason,
|
|
294
|
+
createdAt: doc.createdAt,
|
|
295
|
+
updatedAt: doc.updatedAt,
|
|
296
|
+
createdBy: doc.createdBy,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private async writeAudit(action: string, actor: string, details: Record<string, unknown>): Promise<void> {
|
|
301
|
+
const doc = new SecurityPolicyAuditDoc();
|
|
302
|
+
doc.id = plugins.uuid.v4();
|
|
303
|
+
doc.action = action;
|
|
304
|
+
doc.actor = actor;
|
|
305
|
+
doc.details = details;
|
|
306
|
+
doc.createdAt = Date.now();
|
|
307
|
+
await doc.save();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private async notifyPolicyChanged(): Promise<void> {
|
|
311
|
+
if (this.onPolicyChanged) {
|
|
312
|
+
await this.onPolicyChanged();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
package/ts/security/index.ts
CHANGED
|
@@ -18,4 +18,10 @@ export {
|
|
|
18
18
|
ThreatCategory,
|
|
19
19
|
type IScanResult,
|
|
20
20
|
type IContentScannerOptions
|
|
21
|
-
} from './classes.contentscanner.js';
|
|
21
|
+
} from './classes.contentscanner.js';
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
SecurityPolicyManager,
|
|
25
|
+
type ISecurityPolicyManagerOptions,
|
|
26
|
+
type IRemoteIngressFirewallSnapshot,
|
|
27
|
+
} from './classes.security-policy-manager.js';
|
|
@@ -9,12 +9,14 @@ export class RemoteIngress {
|
|
|
9
9
|
public name: string;
|
|
10
10
|
public secret: string;
|
|
11
11
|
public listenPorts: number[];
|
|
12
|
+
public listenPortsUdp?: number[];
|
|
12
13
|
public enabled: boolean;
|
|
13
14
|
public autoDerivePorts: boolean;
|
|
14
15
|
public tags?: string[];
|
|
15
16
|
public createdAt: number;
|
|
16
17
|
public updatedAt: number;
|
|
17
18
|
public effectiveListenPorts?: number[];
|
|
19
|
+
public effectiveListenPortsUdp?: number[];
|
|
18
20
|
public manualPorts?: number[];
|
|
19
21
|
public derivedPorts?: number[];
|
|
20
22
|
|
|
@@ -24,12 +26,14 @@ export class RemoteIngress {
|
|
|
24
26
|
this.name = data.name;
|
|
25
27
|
this.secret = data.secret;
|
|
26
28
|
this.listenPorts = data.listenPorts;
|
|
29
|
+
this.listenPortsUdp = data.listenPortsUdp;
|
|
27
30
|
this.enabled = data.enabled;
|
|
28
31
|
this.autoDerivePorts = data.autoDerivePorts;
|
|
29
32
|
this.tags = data.tags;
|
|
30
33
|
this.createdAt = data.createdAt;
|
|
31
34
|
this.updatedAt = data.updatedAt;
|
|
32
35
|
this.effectiveListenPorts = data.effectiveListenPorts;
|
|
36
|
+
this.effectiveListenPortsUdp = data.effectiveListenPortsUdp;
|
|
33
37
|
this.manualPorts = data.manualPorts;
|
|
34
38
|
this.derivedPorts = data.derivedPorts;
|
|
35
39
|
}
|
|
@@ -52,11 +56,13 @@ export class RemoteIngress {
|
|
|
52
56
|
const edge = response.edge;
|
|
53
57
|
this.name = edge.name;
|
|
54
58
|
this.listenPorts = edge.listenPorts;
|
|
59
|
+
this.listenPortsUdp = edge.listenPortsUdp;
|
|
55
60
|
this.enabled = edge.enabled;
|
|
56
61
|
this.autoDerivePorts = edge.autoDerivePorts;
|
|
57
62
|
this.tags = edge.tags;
|
|
58
63
|
this.updatedAt = edge.updatedAt;
|
|
59
64
|
this.effectiveListenPorts = edge.effectiveListenPorts;
|
|
65
|
+
this.effectiveListenPortsUdp = edge.effectiveListenPortsUdp;
|
|
60
66
|
this.manualPorts = edge.manualPorts;
|
|
61
67
|
this.derivedPorts = edge.derivedPorts;
|
|
62
68
|
}
|
|
@@ -125,6 +125,18 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
125
125
|
color: ${cssManager.bdTheme('#047857', '#34d399')};
|
|
126
126
|
border: 1px dashed ${cssManager.bdTheme('#6ee7b7', '#065f46')};
|
|
127
127
|
}
|
|
128
|
+
|
|
129
|
+
.metricStack {
|
|
130
|
+
display: flex;
|
|
131
|
+
flex-direction: column;
|
|
132
|
+
gap: 2px;
|
|
133
|
+
font-size: 12px;
|
|
134
|
+
line-height: 1.35;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.metricMuted {
|
|
138
|
+
color: var(--text-muted, #6b7280);
|
|
139
|
+
}
|
|
128
140
|
`,
|
|
129
141
|
];
|
|
130
142
|
|
|
@@ -226,9 +238,13 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
226
238
|
.displayFunction=${(edge: interfaces.data.IRemoteIngress) => ({
|
|
227
239
|
name: edge.name,
|
|
228
240
|
status: this.getEdgeStatusHtml(edge),
|
|
241
|
+
transport: this.getTransportHtml(edge.id),
|
|
229
242
|
publicIp: this.getEdgePublicIp(edge.id),
|
|
230
243
|
ports: this.getPortsHtml(edge),
|
|
231
244
|
tunnels: this.getEdgeTunnelCount(edge.id),
|
|
245
|
+
window: this.getWindowHtml(edge.id),
|
|
246
|
+
queues: this.getQueuesHtml(edge.id),
|
|
247
|
+
traffic: this.getTrafficHtml(edge.id),
|
|
232
248
|
lastHeartbeat: this.getLastHeartbeat(edge.id),
|
|
233
249
|
})}
|
|
234
250
|
.dataActions=${[
|
|
@@ -459,6 +475,46 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
459
475
|
return status?.activeTunnels || 0;
|
|
460
476
|
}
|
|
461
477
|
|
|
478
|
+
private getTransportHtml(edgeId: string): TemplateResult | string {
|
|
479
|
+
const status = this.getEdgeStatus(edgeId);
|
|
480
|
+
if (!status?.connected) return '-';
|
|
481
|
+
const mode = status.transportMode || 'unknown';
|
|
482
|
+
const label = mode === 'quic' ? 'QUIC' : mode === 'tcpTls' ? 'TCP/TLS' : mode;
|
|
483
|
+
return html`<div class="metricStack"><strong>${label}</strong><span class="metricMuted">${status.fallbackUsed ? 'fallback' : status.performance?.profile || 'default'}</span></div>`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private getWindowHtml(edgeId: string): TemplateResult | string {
|
|
487
|
+
const status = this.getEdgeStatus(edgeId);
|
|
488
|
+
if (!status?.connected || !status.flowControl) return '-';
|
|
489
|
+
if (!status.flowControl.applies) {
|
|
490
|
+
return html`<div class="metricStack"><span>native QUIC</span><span class="metricMuted">max ${status.performance?.maxStreamsPerEdge || '-'} streams</span></div>`;
|
|
491
|
+
}
|
|
492
|
+
return html`
|
|
493
|
+
<div class="metricStack">
|
|
494
|
+
<span>${this.formatBytes(status.flowControl.currentWindowBytes)} window</span>
|
|
495
|
+
<span class="metricMuted">${this.formatBytes(status.flowControl.estimatedInFlightBytes)} est. in-flight</span>
|
|
496
|
+
</div>
|
|
497
|
+
`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private getQueuesHtml(edgeId: string): TemplateResult | string {
|
|
501
|
+
const status = this.getEdgeStatus(edgeId);
|
|
502
|
+
if (!status?.connected || !status.queues) return '-';
|
|
503
|
+
return html`<div class="metricStack"><span>C ${status.queues.ctrlQueueDepth} / D ${status.queues.dataQueueDepth}</span><span class="metricMuted">S ${status.queues.sustainedQueueDepth}</span></div>`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private getTrafficHtml(edgeId: string): TemplateResult | string {
|
|
507
|
+
const status = this.getEdgeStatus(edgeId);
|
|
508
|
+
if (!status?.connected || !status.traffic) return '-';
|
|
509
|
+
const drops = (status.traffic.rejectedStreams || 0) + (status.udp?.droppedDatagrams || 0);
|
|
510
|
+
return html`
|
|
511
|
+
<div class="metricStack">
|
|
512
|
+
<span>${this.formatBytes(status.traffic.bytesIn)} in / ${this.formatBytes(status.traffic.bytesOut)} out</span>
|
|
513
|
+
<span class="metricMuted">${drops} rejected/dropped</span>
|
|
514
|
+
</div>
|
|
515
|
+
`;
|
|
516
|
+
}
|
|
517
|
+
|
|
462
518
|
private getLastHeartbeat(edgeId: string): string {
|
|
463
519
|
const status = this.getEdgeStatus(edgeId);
|
|
464
520
|
if (!status?.lastHeartbeat) return '-';
|
|
@@ -467,4 +523,16 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
467
523
|
if (ago < 3600000) return `${Math.floor(ago / 60000)}m ago`;
|
|
468
524
|
return `${Math.floor(ago / 3600000)}h ago`;
|
|
469
525
|
}
|
|
526
|
+
|
|
527
|
+
private formatBytes(bytes: number): string {
|
|
528
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
|
|
529
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
530
|
+
let value = bytes;
|
|
531
|
+
let unitIndex = 0;
|
|
532
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
533
|
+
value = value / 1024;
|
|
534
|
+
unitIndex++;
|
|
535
|
+
}
|
|
536
|
+
return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`;
|
|
537
|
+
}
|
|
470
538
|
}
|