@serve.zone/dcrouter 13.12.0 → 13.14.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 +809 -779
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +6 -5
- package/dist_ts/dns/manager.dns.d.ts +46 -8
- package/dist_ts/dns/manager.dns.js +189 -36
- package/dist_ts/monitoring/classes.metricsmanager.d.ts +26 -0
- package/dist_ts/monitoring/classes.metricsmanager.js +72 -2
- package/dist_ts/opsserver/handlers/config.handler.js +2 -2
- package/dist_ts/opsserver/handlers/domain.handler.js +14 -1
- package/dist_ts/opsserver/handlers/security.handler.js +27 -23
- package/dist_ts/opsserver/handlers/stats.handler.js +22 -3
- package/dist_ts_interfaces/data/stats.d.ts +17 -1
- package/dist_ts_interfaces/requests/domains.d.ts +24 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +13 -0
- package/dist_ts_web/appstate.js +62 -54
- package/dist_ts_web/elements/domains/ops-view-domains.d.ts +1 -0
- package/dist_ts_web/elements/domains/ops-view-domains.js +95 -1
- package/dist_ts_web/elements/network/ops-view-network-activity.d.ts +2 -20
- package/dist_ts_web/elements/network/ops-view-network-activity.js +65 -115
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +5 -4
- package/ts/dns/manager.dns.ts +219 -35
- package/ts/monitoring/classes.metricsmanager.ts +77 -1
- package/ts/opsserver/handlers/config.handler.ts +1 -1
- package/ts/opsserver/handlers/domain.handler.ts +18 -0
- package/ts/opsserver/handlers/security.handler.ts +27 -23
- package/ts/opsserver/handlers/stats.handler.ts +22 -2
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +74 -57
- package/ts_web/elements/domains/ops-view-domains.ts +97 -0
- package/ts_web/elements/network/ops-view-network-activity.ts +67 -132
package/ts/dns/manager.dns.ts
CHANGED
|
@@ -296,70 +296,99 @@ export class DnsManager {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
299
|
+
* Find the DomainDoc that covers a given FQDN, regardless of source
|
|
300
|
+
* (dcrouter-hosted or provider-managed). Uses longest-suffix match.
|
|
301
301
|
*/
|
|
302
|
-
public async
|
|
303
|
-
const
|
|
304
|
-
|
|
302
|
+
public async findDomainForFqdn(fqdn: string): Promise<DomainDoc | null> {
|
|
303
|
+
const lower = fqdn.toLowerCase().replace(/^\*\./, '').replace(/\.$/, '');
|
|
304
|
+
const allDomains = await DomainDoc.findAll();
|
|
305
|
+
// Sort by name length descending for longest-match-wins
|
|
306
|
+
allDomains.sort((a, b) => b.name.length - a.name.length);
|
|
307
|
+
for (const domain of allDomains) {
|
|
308
|
+
if (lower === domain.name || lower.endsWith(`.${domain.name}`)) {
|
|
309
|
+
return domain;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Delete all DNS records matching a name and type under a domain.
|
|
317
|
+
* Used for ACME challenge cleanup (may have multiple TXT records at the same name).
|
|
318
|
+
*/
|
|
319
|
+
public async deleteRecordsByNameAndType(
|
|
320
|
+
domainId: string,
|
|
321
|
+
name: string,
|
|
322
|
+
type: TDnsRecordType,
|
|
323
|
+
): Promise<void> {
|
|
324
|
+
const records = await DnsRecordDoc.findByDomainId(domainId);
|
|
325
|
+
for (const rec of records) {
|
|
326
|
+
if (rec.name.toLowerCase() === name.toLowerCase() && rec.type === type) {
|
|
327
|
+
await this.deleteRecord(rec.id);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* True if any domain is under management (dcrouter-hosted or provider-managed).
|
|
334
|
+
* Used by setupSmartProxy() to decide whether to wire SmartAcme with a DNS-01 handler.
|
|
335
|
+
*/
|
|
336
|
+
public async hasAnyManagedDomain(): Promise<boolean> {
|
|
337
|
+
const domains = await DomainDoc.findAll();
|
|
338
|
+
return domains.length > 0;
|
|
305
339
|
}
|
|
306
340
|
|
|
307
341
|
/**
|
|
308
|
-
* Build an IConvenientDnsProvider that
|
|
309
|
-
* the
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
342
|
+
* Build an IConvenientDnsProvider that routes ACME DNS-01 challenges through
|
|
343
|
+
* the DnsManager abstraction. Challenges are dispatched via createRecord() /
|
|
344
|
+
* deleteRecord(), which transparently handle both dcrouter-hosted zones
|
|
345
|
+
* (embedded DnsServer) and provider-managed zones (e.g. Cloudflare API).
|
|
346
|
+
*
|
|
347
|
+
* Only domains under management (with a DomainDoc in DB) are supported —
|
|
348
|
+
* this acts as the management gate for certificate issuance.
|
|
313
349
|
*/
|
|
314
350
|
public buildAcmeConvenientDnsProvider(): plugins.tsclass.network.IConvenientDnsProvider {
|
|
315
351
|
const self = this;
|
|
316
352
|
const adapter = {
|
|
317
353
|
async acmeSetDnsChallenge(dnsChallenge: { hostName: string; challenge: string }) {
|
|
318
|
-
const
|
|
319
|
-
if (!
|
|
354
|
+
const domainDoc = await self.findDomainForFqdn(dnsChallenge.hostName);
|
|
355
|
+
if (!domainDoc) {
|
|
320
356
|
throw new Error(
|
|
321
|
-
`DnsManager: no
|
|
322
|
-
'Add
|
|
357
|
+
`DnsManager: no managed domain found for ${dnsChallenge.hostName}. ` +
|
|
358
|
+
'Add the domain in Domains before issuing certificates.',
|
|
323
359
|
);
|
|
324
360
|
}
|
|
325
|
-
// Clean
|
|
361
|
+
// Clean leftover challenge records first to avoid duplicates.
|
|
326
362
|
try {
|
|
327
|
-
|
|
328
|
-
for (const r of existing) {
|
|
329
|
-
if (r.type === 'TXT' && r.name === dnsChallenge.hostName) {
|
|
330
|
-
await client.deleteRecord(dnsChallenge.hostName, r.providerRecordId).catch(() => {});
|
|
331
|
-
}
|
|
332
|
-
}
|
|
363
|
+
await self.deleteRecordsByNameAndType(domainDoc.id, dnsChallenge.hostName, 'TXT');
|
|
333
364
|
} catch (err: unknown) {
|
|
334
365
|
logger.log('warn', `DnsManager: failed to clean existing TXT for ${dnsChallenge.hostName}: ${(err as Error).message}`);
|
|
335
366
|
}
|
|
336
|
-
|
|
367
|
+
// Create the challenge TXT record via the unified path
|
|
368
|
+
await self.createRecord({
|
|
369
|
+
domainId: domainDoc.id,
|
|
337
370
|
name: dnsChallenge.hostName,
|
|
338
371
|
type: 'TXT',
|
|
339
372
|
value: dnsChallenge.challenge,
|
|
340
373
|
ttl: 120,
|
|
374
|
+
createdBy: 'acme-dns01',
|
|
341
375
|
});
|
|
342
376
|
},
|
|
343
377
|
async acmeRemoveDnsChallenge(dnsChallenge: { hostName: string; challenge: string }) {
|
|
344
|
-
const
|
|
345
|
-
if (!
|
|
378
|
+
const domainDoc = await self.findDomainForFqdn(dnsChallenge.hostName);
|
|
379
|
+
if (!domainDoc) {
|
|
346
380
|
// The domain may have been removed; nothing to clean up.
|
|
347
381
|
return;
|
|
348
382
|
}
|
|
349
383
|
try {
|
|
350
|
-
|
|
351
|
-
for (const r of existing) {
|
|
352
|
-
if (r.type === 'TXT' && r.name === dnsChallenge.hostName) {
|
|
353
|
-
await client.deleteRecord(dnsChallenge.hostName, r.providerRecordId);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
384
|
+
await self.deleteRecordsByNameAndType(domainDoc.id, dnsChallenge.hostName, 'TXT');
|
|
356
385
|
} catch (err: unknown) {
|
|
357
386
|
logger.log('warn', `DnsManager: failed to remove TXT for ${dnsChallenge.hostName}: ${(err as Error).message}`);
|
|
358
387
|
}
|
|
359
388
|
},
|
|
360
389
|
async isDomainSupported(domain: string): Promise<boolean> {
|
|
361
|
-
const
|
|
362
|
-
return !!
|
|
390
|
+
const domainDoc = await self.findDomainForFqdn(domain);
|
|
391
|
+
return !!domainDoc;
|
|
363
392
|
},
|
|
364
393
|
};
|
|
365
394
|
return { convenience: adapter } as plugins.tsclass.network.IConvenientDnsProvider;
|
|
@@ -642,6 +671,151 @@ export class DnsManager {
|
|
|
642
671
|
return await DnsRecordDoc.findById(id);
|
|
643
672
|
}
|
|
644
673
|
|
|
674
|
+
// ==========================================================================
|
|
675
|
+
// Domain migration
|
|
676
|
+
// ==========================================================================
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Migrate a domain between dcrouter-hosted and provider-managed.
|
|
680
|
+
* Transfers all records to the target and updates domain metadata.
|
|
681
|
+
*/
|
|
682
|
+
public async migrateDomain(args: {
|
|
683
|
+
id: string;
|
|
684
|
+
targetSource: 'dcrouter' | 'provider';
|
|
685
|
+
targetProviderId?: string;
|
|
686
|
+
deleteExistingProviderRecords?: boolean;
|
|
687
|
+
}): Promise<{ success: boolean; recordsMigrated?: number; message?: string }> {
|
|
688
|
+
const domain = await DomainDoc.findById(args.id);
|
|
689
|
+
if (!domain) return { success: false, message: 'Domain not found' };
|
|
690
|
+
|
|
691
|
+
if (domain.source === args.targetSource && domain.providerId === args.targetProviderId) {
|
|
692
|
+
return { success: false, message: 'Domain is already in the target configuration' };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const records = await DnsRecordDoc.findByDomainId(domain.id);
|
|
696
|
+
|
|
697
|
+
if (args.targetSource === 'provider') {
|
|
698
|
+
return this.migrateToDnsProvider(domain, records, args.targetProviderId!, args.deleteExistingProviderRecords ?? false);
|
|
699
|
+
} else {
|
|
700
|
+
return this.migrateToDcrouter(domain, records);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Migrate domain from dcrouter-hosted (or another provider) to an external DNS provider.
|
|
706
|
+
*/
|
|
707
|
+
private async migrateToDnsProvider(
|
|
708
|
+
domain: DomainDoc,
|
|
709
|
+
records: DnsRecordDoc[],
|
|
710
|
+
targetProviderId: string,
|
|
711
|
+
deleteExistingProviderRecords: boolean,
|
|
712
|
+
): Promise<{ success: boolean; recordsMigrated?: number; message?: string }> {
|
|
713
|
+
// Validate the target provider exists
|
|
714
|
+
const client = await this.getProviderClientById(targetProviderId);
|
|
715
|
+
if (!client) {
|
|
716
|
+
return { success: false, message: 'Target DNS provider not found' };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Find the zone at the provider
|
|
720
|
+
const providerDomains = await client.listDomains();
|
|
721
|
+
const zone = providerDomains.find(
|
|
722
|
+
(z) => z.name.toLowerCase() === domain.name.toLowerCase(),
|
|
723
|
+
);
|
|
724
|
+
if (!zone) {
|
|
725
|
+
return { success: false, message: `Zone "${domain.name}" not found at the target provider` };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Optionally delete existing records at the provider
|
|
729
|
+
if (deleteExistingProviderRecords) {
|
|
730
|
+
try {
|
|
731
|
+
const existingProviderRecords = await client.listRecords(domain.name);
|
|
732
|
+
for (const pr of existingProviderRecords) {
|
|
733
|
+
await client.deleteRecord(domain.name, pr.providerRecordId).catch(() => {});
|
|
734
|
+
}
|
|
735
|
+
logger.log('info', `Deleted ${existingProviderRecords.length} existing records at provider for ${domain.name}`);
|
|
736
|
+
} catch (err: unknown) {
|
|
737
|
+
logger.log('warn', `Failed to clean existing provider records for ${domain.name}: ${(err as Error).message}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Push each local record to the provider
|
|
742
|
+
let migrated = 0;
|
|
743
|
+
for (const rec of records) {
|
|
744
|
+
try {
|
|
745
|
+
const providerRecord = await client.createRecord(domain.name, {
|
|
746
|
+
name: rec.name,
|
|
747
|
+
type: rec.type as any,
|
|
748
|
+
value: rec.value,
|
|
749
|
+
ttl: rec.ttl,
|
|
750
|
+
});
|
|
751
|
+
// Unregister from embedded DnsServer if it was dcrouter-hosted
|
|
752
|
+
if (domain.source === 'dcrouter') {
|
|
753
|
+
this.unregisterRecordFromDnsServer(rec);
|
|
754
|
+
}
|
|
755
|
+
// Update the record doc to synced
|
|
756
|
+
rec.source = 'synced' as TDnsRecordSource;
|
|
757
|
+
rec.providerRecordId = providerRecord.providerRecordId;
|
|
758
|
+
await rec.save();
|
|
759
|
+
migrated++;
|
|
760
|
+
} catch (err: unknown) {
|
|
761
|
+
logger.log('warn', `Failed to migrate record ${rec.name} ${rec.type} to provider: ${(err as Error).message}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Update domain metadata
|
|
766
|
+
domain.source = 'provider';
|
|
767
|
+
domain.authoritative = false;
|
|
768
|
+
domain.providerId = targetProviderId;
|
|
769
|
+
domain.externalZoneId = zone.externalId;
|
|
770
|
+
domain.nameservers = zone.nameservers;
|
|
771
|
+
domain.lastSyncedAt = Date.now();
|
|
772
|
+
domain.updatedAt = Date.now();
|
|
773
|
+
await domain.save();
|
|
774
|
+
|
|
775
|
+
logger.log('info', `Domain ${domain.name} migrated to provider (${migrated} records)`);
|
|
776
|
+
return { success: true, recordsMigrated: migrated };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Migrate domain from provider-managed to dcrouter-hosted (authoritative).
|
|
781
|
+
*/
|
|
782
|
+
private async migrateToDcrouter(
|
|
783
|
+
domain: DomainDoc,
|
|
784
|
+
records: DnsRecordDoc[],
|
|
785
|
+
): Promise<{ success: boolean; recordsMigrated?: number; message?: string }> {
|
|
786
|
+
// Register each record with the embedded DnsServer
|
|
787
|
+
let migrated = 0;
|
|
788
|
+
for (const rec of records) {
|
|
789
|
+
try {
|
|
790
|
+
this.registerRecordWithDnsServer(rec);
|
|
791
|
+
// Update the record doc to local
|
|
792
|
+
rec.source = 'local' as TDnsRecordSource;
|
|
793
|
+
rec.providerRecordId = undefined;
|
|
794
|
+
await rec.save();
|
|
795
|
+
migrated++;
|
|
796
|
+
} catch (err: unknown) {
|
|
797
|
+
logger.log('warn', `Failed to register record ${rec.name} ${rec.type} with DnsServer: ${(err as Error).message}`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Update domain metadata
|
|
802
|
+
domain.source = 'dcrouter';
|
|
803
|
+
domain.authoritative = true;
|
|
804
|
+
domain.providerId = undefined;
|
|
805
|
+
domain.externalZoneId = undefined;
|
|
806
|
+
domain.nameservers = undefined;
|
|
807
|
+
domain.lastSyncedAt = undefined;
|
|
808
|
+
domain.updatedAt = Date.now();
|
|
809
|
+
await domain.save();
|
|
810
|
+
|
|
811
|
+
logger.log('info', `Domain ${domain.name} migrated to dcrouter (${migrated} records)`);
|
|
812
|
+
return { success: true, recordsMigrated: migrated };
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ==========================================================================
|
|
816
|
+
// Record CRUD
|
|
817
|
+
// ==========================================================================
|
|
818
|
+
|
|
645
819
|
public async createRecord(args: {
|
|
646
820
|
domainId: string;
|
|
647
821
|
name: string;
|
|
@@ -759,14 +933,24 @@ export class DnsManager {
|
|
|
759
933
|
}
|
|
760
934
|
}
|
|
761
935
|
}
|
|
762
|
-
// For
|
|
763
|
-
// so the record
|
|
764
|
-
|
|
936
|
+
// For dcrouter-hosted records: unregister the handler from the embedded DnsServer
|
|
937
|
+
// so the record stops being served immediately (not just after restart).
|
|
938
|
+
if (domain.source === 'dcrouter' && this.dnsServer) {
|
|
939
|
+
this.unregisterRecordFromDnsServer(doc);
|
|
940
|
+
}
|
|
765
941
|
|
|
766
942
|
await doc.delete();
|
|
767
943
|
return { success: true };
|
|
768
944
|
}
|
|
769
945
|
|
|
946
|
+
/**
|
|
947
|
+
* Unregister a record's handler from the embedded DnsServer.
|
|
948
|
+
*/
|
|
949
|
+
public unregisterRecordFromDnsServer(rec: DnsRecordDoc): void {
|
|
950
|
+
if (!this.dnsServer) return;
|
|
951
|
+
this.dnsServer.unregisterHandler(rec.name, [rec.type]);
|
|
952
|
+
}
|
|
953
|
+
|
|
770
954
|
// ==========================================================================
|
|
771
955
|
// Internal helpers
|
|
772
956
|
// ==========================================================================
|
|
@@ -553,12 +553,14 @@ export class MetricsManager {
|
|
|
553
553
|
connectionsByIP: new Map<string, number>(),
|
|
554
554
|
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
|
555
555
|
topIPs: [] as Array<{ ip: string; count: number }>,
|
|
556
|
+
topIPsByBandwidth: [] as Array<{ ip: string; count: number; bwIn: number; bwOut: number }>,
|
|
556
557
|
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
|
557
558
|
throughputHistory: [] as Array<{ timestamp: number; in: number; out: number }>,
|
|
558
559
|
throughputByIP: new Map<string, { in: number; out: number }>(),
|
|
559
560
|
requestsPerSecond: 0,
|
|
560
561
|
requestsTotal: 0,
|
|
561
562
|
backends: [] as Array<any>,
|
|
563
|
+
domainActivity: [] as Array<{ domain: string; bytesInPerSecond: number; bytesOutPerSecond: number; activeConnections: number; routeCount: number }>,
|
|
562
564
|
};
|
|
563
565
|
}
|
|
564
566
|
|
|
@@ -572,7 +574,7 @@ export class MetricsManager {
|
|
|
572
574
|
bytesOutPerSecond: instantThroughput.out
|
|
573
575
|
};
|
|
574
576
|
|
|
575
|
-
// Get top IPs
|
|
577
|
+
// Get top IPs by connection count
|
|
576
578
|
const topIPs = proxyMetrics.connections.topIPs(10);
|
|
577
579
|
|
|
578
580
|
// Get total data transferred
|
|
@@ -699,10 +701,83 @@ export class MetricsManager {
|
|
|
699
701
|
}
|
|
700
702
|
}
|
|
701
703
|
|
|
704
|
+
// Build top 10 IPs by bandwidth (sorted by total throughput desc)
|
|
705
|
+
const allIPData = new Map<string, { count: number; bwIn: number; bwOut: number }>();
|
|
706
|
+
for (const [ip, count] of connectionsByIP) {
|
|
707
|
+
allIPData.set(ip, { count, bwIn: 0, bwOut: 0 });
|
|
708
|
+
}
|
|
709
|
+
for (const [ip, tp] of throughputByIP) {
|
|
710
|
+
const existing = allIPData.get(ip);
|
|
711
|
+
if (existing) {
|
|
712
|
+
existing.bwIn = tp.in;
|
|
713
|
+
existing.bwOut = tp.out;
|
|
714
|
+
} else {
|
|
715
|
+
allIPData.set(ip, { count: 0, bwIn: tp.in, bwOut: tp.out });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const topIPsByBandwidth = Array.from(allIPData.entries())
|
|
719
|
+
.sort((a, b) => (b[1].bwIn + b[1].bwOut) - (a[1].bwIn + a[1].bwOut))
|
|
720
|
+
.slice(0, 10)
|
|
721
|
+
.map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut }));
|
|
722
|
+
|
|
723
|
+
// Build domain activity from per-route metrics
|
|
724
|
+
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
|
725
|
+
const throughputByRoute = proxyMetrics.throughput.byRoute();
|
|
726
|
+
|
|
727
|
+
// Map route name → primary domain using dcrouter's route configs
|
|
728
|
+
const routeToDomain = new Map<string, string>();
|
|
729
|
+
if (this.dcRouter.smartProxy) {
|
|
730
|
+
for (const route of this.dcRouter.smartProxy.routeManager.getRoutes()) {
|
|
731
|
+
if (!route.name || !route.match.domains) continue;
|
|
732
|
+
const domains = Array.isArray(route.match.domains)
|
|
733
|
+
? route.match.domains
|
|
734
|
+
: [route.match.domains];
|
|
735
|
+
if (domains.length > 0) {
|
|
736
|
+
routeToDomain.set(route.name, domains[0]);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Aggregate metrics by domain
|
|
742
|
+
const domainAgg = new Map<string, {
|
|
743
|
+
activeConnections: number;
|
|
744
|
+
bytesInPerSec: number;
|
|
745
|
+
bytesOutPerSec: number;
|
|
746
|
+
routeCount: number;
|
|
747
|
+
}>();
|
|
748
|
+
for (const [routeName, activeConns] of connectionsByRoute) {
|
|
749
|
+
const domain = routeToDomain.get(routeName) || routeName;
|
|
750
|
+
const tp = throughputByRoute.get(routeName) || { in: 0, out: 0 };
|
|
751
|
+
const existing = domainAgg.get(domain);
|
|
752
|
+
if (existing) {
|
|
753
|
+
existing.activeConnections += activeConns;
|
|
754
|
+
existing.bytesInPerSec += tp.in;
|
|
755
|
+
existing.bytesOutPerSec += tp.out;
|
|
756
|
+
existing.routeCount++;
|
|
757
|
+
} else {
|
|
758
|
+
domainAgg.set(domain, {
|
|
759
|
+
activeConnections: activeConns,
|
|
760
|
+
bytesInPerSec: tp.in,
|
|
761
|
+
bytesOutPerSec: tp.out,
|
|
762
|
+
routeCount: 1,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const domainActivity = Array.from(domainAgg.entries())
|
|
767
|
+
.map(([domain, data]) => ({
|
|
768
|
+
domain,
|
|
769
|
+
bytesInPerSecond: data.bytesInPerSec,
|
|
770
|
+
bytesOutPerSecond: data.bytesOutPerSec,
|
|
771
|
+
activeConnections: data.activeConnections,
|
|
772
|
+
routeCount: data.routeCount,
|
|
773
|
+
}))
|
|
774
|
+
.sort((a, b) => (b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond));
|
|
775
|
+
|
|
702
776
|
return {
|
|
703
777
|
connectionsByIP,
|
|
704
778
|
throughputRate,
|
|
705
779
|
topIPs,
|
|
780
|
+
topIPsByBandwidth,
|
|
706
781
|
totalDataTransferred,
|
|
707
782
|
throughputHistory,
|
|
708
783
|
throughputByIP,
|
|
@@ -711,6 +786,7 @@ export class MetricsManager {
|
|
|
711
786
|
backends,
|
|
712
787
|
frontendProtocols,
|
|
713
788
|
backendProtocols,
|
|
789
|
+
domainActivity,
|
|
714
790
|
};
|
|
715
791
|
}, 1000); // 1s cache — matches typical dashboard poll interval
|
|
716
792
|
}
|
|
@@ -127,7 +127,7 @@ export class ConfigHandler {
|
|
|
127
127
|
// (replaces the legacy `dnsChallenge.cloudflareApiKey` constructor field).
|
|
128
128
|
let dnsChallengeEnabled = false;
|
|
129
129
|
try {
|
|
130
|
-
dnsChallengeEnabled = (await dcRouter.dnsManager?.
|
|
130
|
+
dnsChallengeEnabled = (await dcRouter.dnsManager?.hasAnyManagedDomain()) ?? false;
|
|
131
131
|
} catch {
|
|
132
132
|
dnsChallengeEnabled = false;
|
|
133
133
|
}
|
|
@@ -157,5 +157,23 @@ export class DomainHandler {
|
|
|
157
157
|
},
|
|
158
158
|
),
|
|
159
159
|
);
|
|
160
|
+
|
|
161
|
+
// Migrate domain between dcrouter-hosted and provider-managed
|
|
162
|
+
this.typedrouter.addTypedHandler(
|
|
163
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_MigrateDomain>(
|
|
164
|
+
'migrateDomain',
|
|
165
|
+
async (dataArg) => {
|
|
166
|
+
await this.requireAuth(dataArg, 'domains:write');
|
|
167
|
+
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
168
|
+
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
|
169
|
+
return await dnsManager.migrateDomain({
|
|
170
|
+
id: dataArg.id,
|
|
171
|
+
targetSource: dataArg.targetSource,
|
|
172
|
+
targetProviderId: dataArg.targetProviderId,
|
|
173
|
+
deleteExistingProviderRecords: dataArg.deleteExistingProviderRecords,
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
),
|
|
177
|
+
);
|
|
160
178
|
}
|
|
161
179
|
}
|
|
@@ -51,8 +51,8 @@ export class SecurityHandler {
|
|
|
51
51
|
startTime: conn.startTime,
|
|
52
52
|
protocol: conn.type === 'http' ? 'https' : conn.type as any,
|
|
53
53
|
state: conn.status as any,
|
|
54
|
-
bytesReceived:
|
|
55
|
-
bytesSent:
|
|
54
|
+
bytesReceived: (conn as any)._throughputIn || 0,
|
|
55
|
+
bytesSent: (conn as any)._throughputOut || 0,
|
|
56
56
|
}));
|
|
57
57
|
|
|
58
58
|
const summary = {
|
|
@@ -96,9 +96,11 @@ export class SecurityHandler {
|
|
|
96
96
|
connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })),
|
|
97
97
|
throughputRate: networkStats.throughputRate,
|
|
98
98
|
topIPs: networkStats.topIPs,
|
|
99
|
+
topIPsByBandwidth: networkStats.topIPsByBandwidth,
|
|
99
100
|
totalDataTransferred: networkStats.totalDataTransferred,
|
|
100
101
|
throughputHistory: networkStats.throughputHistory || [],
|
|
101
102
|
throughputByIP,
|
|
103
|
+
domainActivity: networkStats.domainActivity || [],
|
|
102
104
|
requestsPerSecond: networkStats.requestsPerSecond || 0,
|
|
103
105
|
requestsTotal: networkStats.requestsTotal || 0,
|
|
104
106
|
backends: networkStats.backends || [],
|
|
@@ -110,9 +112,11 @@ export class SecurityHandler {
|
|
|
110
112
|
connectionsByIP: [],
|
|
111
113
|
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
|
112
114
|
topIPs: [],
|
|
115
|
+
topIPsByBandwidth: [],
|
|
113
116
|
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
|
114
117
|
throughputHistory: [],
|
|
115
118
|
throughputByIP: [],
|
|
119
|
+
domainActivity: [],
|
|
116
120
|
requestsPerSecond: 0,
|
|
117
121
|
requestsTotal: 0,
|
|
118
122
|
backends: [],
|
|
@@ -251,31 +255,31 @@ export class SecurityHandler {
|
|
|
251
255
|
const connectionInfo = await this.opsServerRef.dcRouterRef.metricsManager.getConnectionInfo();
|
|
252
256
|
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
|
253
257
|
|
|
254
|
-
//
|
|
258
|
+
// One aggregate row per IP with real throughput data
|
|
255
259
|
if (networkStats.connectionsByIP && networkStats.connectionsByIP.size > 0) {
|
|
256
260
|
let connIndex = 0;
|
|
257
261
|
const publicIp = this.opsServerRef.dcRouterRef.options.publicIp || 'server';
|
|
258
|
-
|
|
262
|
+
|
|
259
263
|
for (const [ip, count] of networkStats.connectionsByIP) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
})
|
|
278
|
-
}
|
|
264
|
+
const tp = networkStats.throughputByIP?.get(ip);
|
|
265
|
+
connections.push({
|
|
266
|
+
id: `ip-${connIndex++}`,
|
|
267
|
+
type: 'http',
|
|
268
|
+
source: {
|
|
269
|
+
ip: ip,
|
|
270
|
+
port: 0,
|
|
271
|
+
},
|
|
272
|
+
destination: {
|
|
273
|
+
ip: publicIp,
|
|
274
|
+
port: 443,
|
|
275
|
+
service: 'proxy',
|
|
276
|
+
},
|
|
277
|
+
startTime: 0,
|
|
278
|
+
bytesTransferred: count, // Store connection count here
|
|
279
|
+
status: 'active',
|
|
280
|
+
// Attach real throughput for the handler mapping
|
|
281
|
+
...(tp ? { _throughputIn: tp.in, _throughputOut: tp.out } : {}),
|
|
282
|
+
} as any);
|
|
279
283
|
}
|
|
280
284
|
} else if (connectionInfo.length > 0) {
|
|
281
285
|
// Fallback to route-based connection info if no IP data available
|
|
@@ -291,6 +291,20 @@ export class StatsHandler {
|
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// Build connectionDetails from real per-IP data
|
|
295
|
+
const connectionDetails: interfaces.data.IConnectionDetails[] = [];
|
|
296
|
+
for (const [ip, count] of stats.connectionsByIP) {
|
|
297
|
+
const tp = stats.throughputByIP?.get(ip);
|
|
298
|
+
connectionDetails.push({
|
|
299
|
+
remoteAddress: ip,
|
|
300
|
+
protocol: 'https',
|
|
301
|
+
state: 'connected',
|
|
302
|
+
startTime: 0,
|
|
303
|
+
bytesIn: tp?.in || 0,
|
|
304
|
+
bytesOut: tp?.out || 0,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
294
308
|
metrics.network = {
|
|
295
309
|
totalBandwidth: {
|
|
296
310
|
in: stats.throughputRate.bytesInPerSecond,
|
|
@@ -301,12 +315,18 @@ export class StatsHandler {
|
|
|
301
315
|
out: stats.totalDataTransferred.bytesOut,
|
|
302
316
|
},
|
|
303
317
|
activeConnections: serverStats.activeConnections,
|
|
304
|
-
connectionDetails
|
|
318
|
+
connectionDetails,
|
|
305
319
|
topEndpoints: stats.topIPs.map(ip => ({
|
|
306
320
|
endpoint: ip.ip,
|
|
307
|
-
|
|
321
|
+
connections: ip.count,
|
|
308
322
|
bandwidth: ipBandwidth.get(ip.ip) || { in: 0, out: 0 },
|
|
309
323
|
})),
|
|
324
|
+
topEndpointsByBandwidth: stats.topIPsByBandwidth.map(ip => ({
|
|
325
|
+
endpoint: ip.ip,
|
|
326
|
+
connections: ip.count,
|
|
327
|
+
bandwidth: { in: ip.bwIn, out: ip.bwOut },
|
|
328
|
+
})),
|
|
329
|
+
domainActivity: stats.domainActivity || [],
|
|
310
330
|
throughputHistory: stats.throughputHistory || [],
|
|
311
331
|
requestsPerSecond: stats.requestsPerSecond || 0,
|
|
312
332
|
requestsTotal: stats.requestsTotal || 0,
|