Compare commits

...

5 Commits

Author SHA1 Message Date
jkunz 1567606c49 v13.23.0
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-26 15:15:27 +00:00
jkunz af31982d58 feat(security): add managed security policies with IP intelligence and remote ingress firewall propagation 2026-04-26 15:15:27 +00:00
jkunz a322308623 v13.22.0
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-26 12:14:51 +00:00
jkunz ec5374900c feat(remoteingress): add remote ingress performance configuration and expose tunnel transport metrics 2026-04-26 12:14:51 +00:00
jkunz 49ce265d7e fix(deps): bump @push.rocks/smartproxy to ^27.8.2 2026-04-26 11:32:57 +00:00
27 changed files with 1010 additions and 34 deletions
+16
View File
@@ -1,5 +1,21 @@
# Changelog
## 2026-04-26 - 13.23.0 - feat(security)
add managed security policies with IP intelligence and remote ingress firewall propagation
- introduces a SecurityPolicyManager that observes public IPs, stores IP intelligence, compiles block policies, and audits policy changes
- adds database documents and shared interfaces for security block rules, IP intelligence records, and security policy audit events
- exposes ops/admin request handlers to list IP intelligence and create, update, or delete security block rules
- applies merged security policies to SmartProxy and propagates firewall snapshots to remote ingress edges and tunnel synchronization
## 2026-04-26 - 13.22.0 - feat(remoteingress)
add remote ingress performance configuration and expose tunnel transport metrics
- upgrade @serve.zone/remoteingress to support performance tuning and richer tunnel status data
- pass remote ingress performance settings through router startup and config APIs
- serialize allowed-edge sync operations and await route update hooks to avoid tunnel sync races
- expose UDP listen ports and transport, flow control, queue, and traffic metrics in remote ingress APIs and ops UI
## 2026-04-26 - 13.21.1 - fix(deps)
bump @push.rocks/smartproxy to ^27.8.1
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "13.21.1",
"version": "13.23.0",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -54,7 +54,7 @@
"@push.rocks/smartnetwork": "^4.6.0",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartproxy": "^27.8.1",
"@push.rocks/smartproxy": "^27.9.0",
"@push.rocks/smartradius": "^1.1.1",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10",
@@ -64,7 +64,7 @@
"@push.rocks/taskbuffer": "^8.0.2",
"@serve.zone/catalog": "^2.12.4",
"@serve.zone/interfaces": "^5.4.3",
"@serve.zone/remoteingress": "^4.15.3",
"@serve.zone/remoteingress": "^4.17.1",
"@tsclass/tsclass": "^9.5.0",
"@types/qrcode": "^1.5.6",
"lru-cache": "^11.3.5",
+19 -11
View File
@@ -81,8 +81,8 @@ importers:
specifier: ^4.2.3
version: 4.2.3
'@push.rocks/smartproxy':
specifier: ^27.8.1
version: 27.8.1
specifier: ^27.9.0
version: 27.9.0
'@push.rocks/smartradius':
specifier: ^1.1.1
version: 1.1.1
@@ -111,8 +111,8 @@ importers:
specifier: ^5.4.3
version: 5.4.3
'@serve.zone/remoteingress':
specifier: ^4.15.3
version: 4.15.3
specifier: ^4.17.1
version: 4.17.1
'@tsclass/tsclass':
specifier: ^9.5.0
version: 9.5.0
@@ -1263,6 +1263,9 @@ packages:
'@push.rocks/smartnftables@1.1.0':
resolution: {integrity: sha512-7JNzerlW20HEl2wKMBIHltwneCQRpXiD2lJkXZZc02ctnfjgFejXVDIeWomhPx6PZ0Z6zmqdF6rrFDtDHyqqfA==}
'@push.rocks/smartnftables@1.2.0':
resolution: {integrity: sha512-VTRHnxHrJj9VOq2MaCOqxiA4JLGRnzEaZ7kXxA7v3ljX+Y2wWK9VYpwKKBEbjgjoTpQyOf+I0gEG9wkR/jtUvQ==}
'@push.rocks/smartnpm@2.0.6':
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
@@ -1284,8 +1287,8 @@ packages:
'@push.rocks/smartpromise@4.2.3':
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
'@push.rocks/smartproxy@27.8.1':
resolution: {integrity: sha512-1Gaubl/M7tMdlNUi1gKV8A2LQ4+9/Eu3ynVzMO2SRXWHdBBOUms2v2Iv+gOUf+8OIYRL16FreJyF2/DbsrQjKg==}
'@push.rocks/smartproxy@27.9.0':
resolution: {integrity: sha512-lzOxueA89pBf4ZcTzF+VkjXQ0es8z8C20PW6FA0HcIzcCpnh4NjLwnXyD8NnTpCf+HKh/EAgD77Kt9Dn+sssUQ==}
'@push.rocks/smartpuppeteer@2.0.5':
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
@@ -1594,8 +1597,8 @@ packages:
'@serve.zone/interfaces@5.4.3':
resolution: {integrity: sha512-9ijFhHoC7GYyyAUJbBoDYmcoCmIXTFPiD6fI3x68SWiC0xA+2LG0nOe14D32c1QN9X/3i2Ac5/1sUibfjHsIGg==}
'@serve.zone/remoteingress@4.15.3':
resolution: {integrity: sha512-kg/bmR+qcFRFuigTDr5Fao72cb7m/mSkI5APm7KZDKSUYTFuytNoj6KCIE0ICkc3Nh34y8oDwFJsS6oFo64AyQ==}
'@serve.zone/remoteingress@4.17.1':
resolution: {integrity: sha512-k3n+AF1rNybiKPlHHyhwCVEF0/T7eZD46kNn7JlEJPCxfUy09mjkpwDQ2CzaUkppqNgFOAYXgAKqjDqpJ27RvA==}
'@sindresorhus/is@5.6.0':
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
@@ -6443,6 +6446,11 @@ snapshots:
'@push.rocks/smartlog': 3.2.2
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartnftables@1.2.0':
dependencies:
'@push.rocks/smartlog': 3.2.2
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartnpm@2.0.6':
dependencies:
'@push.rocks/consolecolor': 2.0.3
@@ -6512,7 +6520,7 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@27.8.1':
'@push.rocks/smartproxy@27.9.0':
dependencies:
'@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartlog': 3.2.2
@@ -6933,10 +6941,10 @@ snapshots:
'@push.rocks/smartlog-interfaces': 3.0.2
'@tsclass/tsclass': 9.5.0
'@serve.zone/remoteingress@4.15.3':
'@serve.zone/remoteingress@4.17.1':
dependencies:
'@push.rocks/qenv': 6.1.3
'@push.rocks/smartnftables': 1.1.0
'@push.rocks/smartnftables': 1.2.0
'@push.rocks/smartrust': 1.3.2
'@sindresorhus/is@5.6.0': {}
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '13.21.1',
version: '13.23.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}
+106 -6
View File
@@ -27,12 +27,13 @@ import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
import { VpnManager, type IVpnManagerConfig } from './vpn/index.js';
import { RouteConfigManager, ApiTokenManager, ReferenceResolver, DbSeeder, TargetProfileManager } from './config/index.js';
import type { TIpAllowEntry } from './config/classes.route-config-manager.js';
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
import { SecurityLogger, ContentScanner, IPReputationChecker, SecurityPolicyManager } from './security/index.js';
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
import { DnsManager } from './dns/manager.dns.js';
import { AcmeConfigManager } from './acme/manager.acme-config.js';
import { EmailDomainManager, SmartMtaStorageManager, buildEmailDnsRecords } from './email/index.js';
import type { IRoute } from '../ts_interfaces/data/route-management.js';
import type { ISecurityCompiledPolicy } from '../ts_interfaces/data/security-policy.js';
export interface IDcRouterOptions {
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
@@ -178,6 +179,8 @@ export interface IDcRouterOptions {
certPath?: string;
keyPath?: string;
};
/** Performance profile and limits for remote ingress hub/edge tunnels. */
performance?: import('../ts_interfaces/data/remoteingress.js').IRemoteIngressPerformanceConfig;
};
/**
@@ -282,6 +285,7 @@ export class DcRouter {
// ACME configuration (DB-backed singleton, replaces tls.contactEmail)
public acmeConfigManager?: AcmeConfigManager;
public emailDomainManager?: EmailDomainManager;
public securityPolicyManager?: SecurityPolicyManager;
// Auto-discovered public IP (populated by generateAuthoritativeRecords)
public detectedPublicIp: string | null = null;
@@ -469,12 +473,36 @@ export class DcRouter {
);
}
// SecurityPolicyManager: optional, depends on DcRouterDb — owns IP intelligence
// and compiles the global block policy for SmartProxy and remote ingress edges.
if (this.options.dbConfig?.enabled !== false) {
this.serviceManager.addService(
new plugins.taskbuffer.Service('SecurityPolicyManager')
.optional()
.dependsOn('DcRouterDb')
.withStart(async () => {
this.securityPolicyManager = new SecurityPolicyManager({
onPolicyChanged: () => this.applySecurityPolicy(),
});
await this.securityPolicyManager.start();
})
.withStop(async () => {
if (this.securityPolicyManager) {
await this.securityPolicyManager.stop();
this.securityPolicyManager = undefined;
}
})
.withRetry({ maxRetries: 1, baseDelayMs: 500 }),
);
}
// SmartProxy: critical, depends on DcRouterDb + DnsManager + AcmeConfigManager (if enabled)
const smartProxyDeps: string[] = [];
if (this.options.dbConfig?.enabled !== false) {
smartProxyDeps.push('DcRouterDb');
smartProxyDeps.push('DnsManager');
smartProxyDeps.push('AcmeConfigManager');
smartProxyDeps.push('SecurityPolicyManager');
}
this.serviceManager.addService(
new plugins.taskbuffer.Service('SmartProxy')
@@ -570,12 +598,16 @@ export class DcRouter {
this.referenceResolver,
// Sync routes to RemoteIngressManager whenever routes change,
// then push updated derived ports to the Rust hub binary
(routes) => {
async (routes) => {
if (this.remoteIngressManager) {
this.remoteIngressManager.setRoutes(routes as any[]);
}
if (this.tunnelManager) {
this.tunnelManager.syncAllowedEdges();
try {
await this.tunnelManager.syncAllowedEdges();
} catch (err: unknown) {
logger.log('error', `Failed to sync Remote Ingress allowed edges: ${(err as Error).message}`);
}
}
},
undefined,
@@ -965,6 +997,12 @@ export class DcRouter {
logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
}
const compiledSecurityPolicy = await this.securityPolicyManager?.compileSmartProxyPolicy();
const mergedSecurityPolicy = this.mergeSecurityPolicies(
(this.options.smartProxyConfig as any)?.securityPolicy,
compiledSecurityPolicy,
);
// If we have routes or need a basic SmartProxy instance, create it
if (routes.length > 0 || this.options.smartProxyConfig) {
logger.log('info', 'Setting up SmartProxy with combined configuration');
@@ -996,6 +1034,7 @@ export class DcRouter {
// --- always set by dcrouter (after spread) ---
routes,
acme: acmeConfig,
...(mergedSecurityPolicy ? { securityPolicy: mergedSecurityPolicy } as any : {}),
certStore: {
loadAll: async () => {
const docs = await ProxyCertDoc.findAll();
@@ -1120,7 +1159,12 @@ export class DcRouter {
// to SmartProxy with PROXY protocol v1 headers to preserve client IPs.
if (this.options.remoteIngressConfig?.enabled) {
smartProxyConfig.acceptProxyProtocol = true;
smartProxyConfig.proxyIPs = ['127.0.0.1'];
if (!smartProxyConfig.proxyIPs) {
smartProxyConfig.proxyIPs = [];
}
if (!smartProxyConfig.proxyIPs.includes('127.0.0.1')) {
smartProxyConfig.proxyIPs.push('127.0.0.1');
}
}
// VPN uses socket mode with PP v2 — SmartProxy must accept proxy protocol from localhost
@@ -1233,8 +1277,60 @@ export class DcRouter {
logger.log('info', `SmartProxy started with ${routes.length} routes`);
}
}
public async applySecurityPolicy(): Promise<void> {
if (!this.securityPolicyManager) {
return;
}
const compiledSmartProxyPolicy = await this.securityPolicyManager.compileSmartProxyPolicy();
const mergedSecurityPolicy = this.mergeSecurityPolicies(
(this.options.smartProxyConfig as any)?.securityPolicy,
compiledSmartProxyPolicy,
);
if (this.smartProxy && mergedSecurityPolicy) {
const smartProxyWithPolicyApi = this.smartProxy as any;
if (typeof smartProxyWithPolicyApi.updateSecurityPolicy === 'function') {
await smartProxyWithPolicyApi.updateSecurityPolicy(mergedSecurityPolicy);
}
}
const firewallConfig = await this.securityPolicyManager.compileRemoteIngressFirewall();
if (this.remoteIngressManager) {
(this.remoteIngressManager as any).setFirewallConfig?.(firewallConfig);
}
if (this.tunnelManager) {
await this.tunnelManager.syncAllowedEdges();
}
}
private mergeSecurityPolicies(
...policies: Array<Partial<ISecurityCompiledPolicy> | undefined>
): ISecurityCompiledPolicy | undefined {
const blockedIps = new Set<string>();
const blockedCidrs = new Set<string>();
for (const policy of policies) {
for (const ip of policy?.blockedIps || []) {
if (ip) blockedIps.add(ip);
}
for (const cidr of policy?.blockedCidrs || []) {
if (cidr) blockedCidrs.add(cidr);
}
}
if (blockedIps.size === 0 && blockedCidrs.size === 0) {
return undefined;
}
return {
blockedIps: [...blockedIps].sort(),
blockedCidrs: [...blockedCidrs].sort(),
};
}
/**
* Generate SmartProxy routes for email configuration
@@ -2221,6 +2317,9 @@ export class DcRouter {
// Initialize the edge registration manager
this.remoteIngressManager = new RemoteIngressManager();
await this.remoteIngressManager.initialize();
this.remoteIngressManager.setFirewallConfig(
await this.securityPolicyManager?.compileRemoteIngressFirewall(),
);
// Pass current bootstrap routes so the manager can derive edge ports initially.
// Once RouteConfigManager applies the full DB set, the onRoutesApplied callback
@@ -2270,6 +2369,7 @@ export class DcRouter {
tunnelPort: riCfg.tunnelPort ?? 8443,
targetHost: '127.0.0.1',
tls: tlsConfig,
performance: riCfg.performance,
});
await this.tunnelManager.start();
+2 -2
View File
@@ -59,7 +59,7 @@ export class RouteConfigManager {
private getHttp3Config?: () => IHttp3Config | undefined,
private getVpnClientIpsForRoute?: (route: IDcRouterRouteConfig, routeId?: string) => TIpAllowEntry[],
private referenceResolver?: ReferenceResolver,
private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void,
private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void | Promise<void>,
private getRuntimeRoutes?: () => plugins.smartproxy.IRouteConfig[],
private hydrateStoredRoute?: (storedRoute: IRoute) => plugins.smartproxy.IRouteConfig | undefined,
) {}
@@ -540,7 +540,7 @@ export class RouteConfigManager {
// Notify listeners (e.g. RemoteIngressManager) of the route set
if (this.onRoutesApplied) {
this.onRoutesApplied(enabledRoutes);
await this.onRoutesApplied(enabledRoutes);
}
logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.routes.size} total)`);
@@ -0,0 +1,75 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
import type { IIpIntelligenceRecord } from '../../../ts_interfaces/data/security-policy.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class IpIntelligenceDoc extends plugins.smartdata.SmartDataDbDoc<IpIntelligenceDoc, IpIntelligenceDoc> implements IIpIntelligenceRecord {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public ipAddress!: string;
@plugins.smartdata.svDb()
public asn: number | null = null;
@plugins.smartdata.svDb()
public asnOrg: string | null = null;
@plugins.smartdata.svDb()
public registrantOrg: string | null = null;
@plugins.smartdata.svDb()
public registrantCountry: string | null = null;
@plugins.smartdata.svDb()
public networkRange: string | null = null;
@plugins.smartdata.svDb()
public abuseContact: string | null = null;
@plugins.smartdata.svDb()
public country: string | null = null;
@plugins.smartdata.svDb()
public countryCode: string | null = null;
@plugins.smartdata.svDb()
public city: string | null = null;
@plugins.smartdata.svDb()
public latitude: number | null = null;
@plugins.smartdata.svDb()
public longitude: number | null = null;
@plugins.smartdata.svDb()
public accuracyRadius: number | null = null;
@plugins.smartdata.svDb()
public timezone: string | null = null;
@plugins.smartdata.svDb()
public firstSeenAt: number = Date.now();
@plugins.smartdata.svDb()
public lastSeenAt: number = Date.now();
@plugins.smartdata.svDb()
public updatedAt: number = Date.now();
@plugins.smartdata.svDb()
public seenCount: number = 0;
constructor() {
super();
}
public static async findByIp(ipAddress: string): Promise<IpIntelligenceDoc | null> {
return await IpIntelligenceDoc.getInstance({ ipAddress });
}
public static async findAll(): Promise<IpIntelligenceDoc[]> {
return await IpIntelligenceDoc.getInstances({});
}
}
@@ -0,0 +1,52 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
import type { ISecurityBlockRule, TSecurityBlockRuleMatchMode, TSecurityBlockRuleType } from '../../../ts_interfaces/data/security-policy.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class SecurityBlockRuleDoc extends plugins.smartdata.SmartDataDbDoc<SecurityBlockRuleDoc, SecurityBlockRuleDoc> implements ISecurityBlockRule {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public id!: string;
@plugins.smartdata.svDb()
public type!: TSecurityBlockRuleType;
@plugins.smartdata.svDb()
public value!: string;
@plugins.smartdata.svDb()
public matchMode?: TSecurityBlockRuleMatchMode;
@plugins.smartdata.svDb()
public enabled: boolean = true;
@plugins.smartdata.svDb()
public reason?: string;
@plugins.smartdata.svDb()
public createdAt: number = Date.now();
@plugins.smartdata.svDb()
public updatedAt: number = Date.now();
@plugins.smartdata.svDb()
public createdBy: string = 'system';
constructor() {
super();
}
public static async findById(id: string): Promise<SecurityBlockRuleDoc | null> {
return await SecurityBlockRuleDoc.getInstance({ id });
}
public static async findAll(): Promise<SecurityBlockRuleDoc[]> {
return await SecurityBlockRuleDoc.getInstances({});
}
public static async findEnabled(): Promise<SecurityBlockRuleDoc[]> {
return await SecurityBlockRuleDoc.getInstances({ enabled: true });
}
}
@@ -0,0 +1,33 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
import type { ISecurityPolicyAuditEvent } from '../../../ts_interfaces/data/security-policy.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class SecurityPolicyAuditDoc extends plugins.smartdata.SmartDataDbDoc<SecurityPolicyAuditDoc, SecurityPolicyAuditDoc> implements ISecurityPolicyAuditEvent {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public id!: string;
@plugins.smartdata.svDb()
public action!: string;
@plugins.smartdata.svDb()
public actor!: string;
@plugins.smartdata.svDb()
public details!: Record<string, unknown>;
@plugins.smartdata.svDb()
public createdAt: number = Date.now();
constructor() {
super();
}
public static async findRecent(limit = 100): Promise<SecurityPolicyAuditDoc[]> {
const docs = await SecurityPolicyAuditDoc.getInstances({});
return docs.sort((a, b) => b.createdAt - a.createdAt).slice(0, limit);
}
}
+3
View File
@@ -1,6 +1,9 @@
// Cached/TTL document classes
export * from './classes.cached.email.js';
export * from './classes.cached.ip.reputation.js';
export * from './classes.ip-intelligence.doc.js';
export * from './classes.security-block-rule.doc.js';
export * from './classes.security-policy-audit.doc.js';
// Config document classes
export * from './classes.route.doc.js';
+2
View File
@@ -725,6 +725,8 @@ export class MetricsManager {
.slice(0, 10)
.map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut }));
void this.dcRouter.securityPolicyManager?.observeIps([...allIPData.keys()]);
// Build domain activity using per-IP domain request counts from Rust engine
const connectionsByRoute = proxyMetrics.connections.byRoute();
const throughputByRoute = proxyMetrics.throughput.byRoute();
+1
View File
@@ -206,6 +206,7 @@ export class ConfigHandler {
hubDomain: riCfg?.hubDomain || null,
tlsMode,
connectedEdgeIps,
performance: riCfg?.performance,
};
return {
@@ -29,6 +29,7 @@ export class RemoteIngressHandler {
...e,
secret: '********', // Never expose secrets via API
effectiveListenPorts: manager.getEffectiveListenPorts(e),
effectiveListenPortsUdp: manager.getEffectiveListenPortsUdp(e),
manualPorts: breakdown.manual,
derivedPorts: breakdown.derived,
};
@@ -133,6 +134,7 @@ export class RemoteIngressHandler {
...edge,
secret: '********',
effectiveListenPorts: manager.getEffectiveListenPorts(edge),
effectiveListenPortsUdp: manager.getEffectiveListenPortsUdp(edge),
manualPorts: breakdown.manual,
derivedPorts: breakdown.derived,
},
+69
View File
@@ -157,6 +157,75 @@ export class SecurityHandler {
}
)
);
router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListSecurityBlockRules>(
'listSecurityBlockRules',
async () => {
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
return { rules: manager ? await manager.listBlockRules() : [] };
},
),
);
router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListIpIntelligence>(
'listIpIntelligence',
async () => {
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
return { records: manager ? await manager.listIpIntelligence() : [] };
},
),
);
const adminRouter = this.opsServerRef.adminRouter;
adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateSecurityBlockRule>(
'createSecurityBlockRule',
async (dataArg) => {
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
if (!manager) return { success: false, message: 'Security policy manager not initialized' };
const rule = await manager.createBlockRule({
type: dataArg.type,
value: dataArg.value,
matchMode: dataArg.matchMode,
reason: dataArg.reason,
enabled: dataArg.enabled,
}, dataArg.identity.userId);
return { success: true, rule };
},
),
);
adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateSecurityBlockRule>(
'updateSecurityBlockRule',
async (dataArg) => {
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
if (!manager) return { success: false, message: 'Security policy manager not initialized' };
const rule = await manager.updateBlockRule(dataArg.id, {
value: dataArg.value,
matchMode: dataArg.matchMode,
reason: dataArg.reason,
enabled: dataArg.enabled,
}, dataArg.identity.userId);
return rule ? { success: true, rule } : { success: false, message: 'Rule not found' };
},
),
);
adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteSecurityBlockRule>(
'deleteSecurityBlockRule',
async (dataArg) => {
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
if (!manager) return { success: false, message: 'Security policy manager not initialized' };
const success = await manager.deleteBlockRule(dataArg.id, dataArg.identity.userId);
return { success, message: success ? undefined : 'Rule not found' };
},
),
);
}
private async collectSecurityMetrics(): Promise<{
@@ -2,6 +2,10 @@ import * as plugins from '../plugins.js';
import type { IRemoteIngress, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
import { RemoteIngressEdgeDoc } from '../db/index.js';
interface IRemoteIngressFirewallConfig {
blockedIps?: string[];
}
/**
* Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array.
*/
@@ -31,6 +35,7 @@ function extractPorts(portRange: number | Array<number | { from: number; to: num
export class RemoteIngressManager {
private edges: Map<string, IRemoteIngress> = new Map();
private routes: IDcRouterRouteConfig[] = [];
private firewallConfig?: IRemoteIngressFirewallConfig;
constructor() {
}
@@ -69,6 +74,13 @@ export class RemoteIngressManager {
this.routes = routes;
}
/**
* Set the full desired firewall snapshot pushed to all edges.
*/
public setFirewallConfig(firewallConfig?: IRemoteIngressFirewallConfig): void {
this.firewallConfig = firewallConfig;
}
/**
* Derive listen ports for an edge from routes tagged with remoteIngress.enabled.
* When a route specifies edgeFilter, only edges whose id or tags match get that route's ports.
@@ -305,8 +317,8 @@ export class RemoteIngressManager {
* Get the list of allowed edges (enabled only) for the Rust hub.
* Includes listenPortsUdp when routes with transport 'udp' or 'all' are present.
*/
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> {
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> = [];
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig }> {
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig }> = [];
for (const edge of this.edges.values()) {
if (edge.enabled) {
const listenPortsUdp = this.getEffectiveListenPortsUdp(edge);
@@ -315,6 +327,7 @@ export class RemoteIngressManager {
secret: edge.secret,
listenPorts: this.getEffectiveListenPorts(edge),
...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}),
...(this.firewallConfig ? { firewallConfig: this.firewallConfig } : {}),
});
}
}
+25 -5
View File
@@ -9,6 +9,7 @@ export interface ITunnelManagerConfig {
certPem?: string;
keyPem?: string;
};
performance?: import('../../ts_interfaces/data/remoteingress.js').IRemoteIngressPerformanceConfig;
}
/**
@@ -20,6 +21,7 @@ export class TunnelManager {
private config: ITunnelManagerConfig;
private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
private reconcileInterval: ReturnType<typeof setInterval> | null = null;
private syncChain: Promise<void> = Promise.resolve();
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
this.manager = manager;
@@ -66,7 +68,8 @@ export class TunnelManager {
tunnelPort: this.config.tunnelPort ?? 8443,
targetHost: this.config.targetHost ?? '127.0.0.1',
tls: this.config.tls,
});
...(this.config.performance ? { performance: this.config.performance } : {}),
} as any);
// Send allowed edges to the hub
await this.syncAllowedEdges();
@@ -107,20 +110,23 @@ export class TunnelManager {
if (existing) {
existing.activeTunnels = rustEdge.activeStreams;
existing.lastHeartbeat = Date.now();
this.applyRustStatus(existing, rustEdge);
// Update peer address if available from Rust hub
if (rustEdge.peerAddr) {
existing.publicIp = rustEdge.peerAddr;
}
} else {
// Missed edgeConnected event — add entry
this.edgeStatuses.set(rustEdge.edgeId, {
const status: IRemoteIngressStatus = {
edgeId: rustEdge.edgeId,
connected: true,
publicIp: rustEdge.peerAddr || null,
activeTunnels: rustEdge.activeStreams,
lastHeartbeat: Date.now(),
connectedAt: rustEdge.connectedAt * 1000,
});
};
this.applyRustStatus(status, rustEdge);
this.edgeStatuses.set(rustEdge.edgeId, status);
}
}
@@ -137,8 +143,22 @@ export class TunnelManager {
* Call this after creating/deleting/updating edges.
*/
public async syncAllowedEdges(): Promise<void> {
const edges = this.manager.getAllowedEdges();
await this.hub.updateAllowedEdges(edges);
const run = this.syncChain.catch(() => {}).then(async () => {
const edges = this.manager.getAllowedEdges();
await this.hub.updateAllowedEdges(edges as any);
});
this.syncChain = run;
await run;
}
private applyRustStatus(status: IRemoteIngressStatus, rustEdge: any): void {
status.transportMode = rustEdge.transportMode;
status.fallbackUsed = rustEdge.fallbackUsed;
status.performance = rustEdge.performance;
status.flowControl = rustEdge.flowControl;
status.queues = rustEdge.queues;
status.traffic = rustEdge.traffic;
status.udp = rustEdge.udp;
}
/**
@@ -0,0 +1,315 @@
import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { IpIntelligenceDoc, SecurityBlockRuleDoc, SecurityPolicyAuditDoc } from '../db/index.js';
import type {
IIpIntelligenceRecord,
ISecurityBlockRule,
ISecurityCompiledPolicy,
TSecurityBlockRuleMatchMode,
TSecurityBlockRuleType,
} from '../../ts_interfaces/data/security-policy.js';
export interface ISecurityPolicyManagerOptions {
intelligenceRefreshMs?: number;
onPolicyChanged?: () => void | Promise<void>;
}
export interface IRemoteIngressFirewallSnapshot {
blockedIps?: string[];
}
export class SecurityPolicyManager {
private readonly smartNetwork = new plugins.smartnetwork.SmartNetwork({
cacheTtl: 24 * 60 * 60 * 1000,
});
private readonly intelligenceRefreshMs: number;
private readonly inFlightObservations = new Set<string>();
private readonly onPolicyChanged?: () => void | Promise<void>;
constructor(options: ISecurityPolicyManagerOptions = {}) {
this.intelligenceRefreshMs = options.intelligenceRefreshMs ?? 24 * 60 * 60 * 1000;
this.onPolicyChanged = options.onPolicyChanged;
}
public async start(): Promise<void> {
logger.log('info', 'SecurityPolicyManager started');
}
public async stop(): Promise<void> {
await this.smartNetwork.stop();
}
public async observeIps(ips: string[]): Promise<void> {
const uniqueIps = [...new Set(ips.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])];
await Promise.allSettled(uniqueIps.map((ip) => this.observeIp(ip)));
}
public async observeIp(ipAddress: string): Promise<void> {
const ip = this.normalizeIp(ipAddress);
if (!ip || !this.isPublicIp(ip) || this.inFlightObservations.has(ip)) {
return;
}
this.inFlightObservations.add(ip);
try {
const now = Date.now();
let doc = await IpIntelligenceDoc.findByIp(ip);
if (doc && now - doc.updatedAt < this.intelligenceRefreshMs) {
if (now - doc.lastSeenAt > 60_000) {
doc.lastSeenAt = now;
doc.seenCount = (doc.seenCount || 0) + 1;
await doc.save();
}
return;
}
const intelligence = await this.smartNetwork.getIpIntelligence(ip);
if (!doc) {
doc = new IpIntelligenceDoc();
doc.ipAddress = ip;
doc.firstSeenAt = now;
}
Object.assign(doc, intelligence);
doc.lastSeenAt = now;
doc.updatedAt = now;
doc.seenCount = (doc.seenCount || 0) + 1;
await doc.save();
if (await this.matchesAnyReactiveRule(doc)) {
await this.notifyPolicyChanged();
}
} catch (err) {
logger.log('warn', `Failed to enrich IP ${ip}: ${(err as Error).message}`);
} finally {
this.inFlightObservations.delete(ip);
}
}
public async listBlockRules(): Promise<ISecurityBlockRule[]> {
return (await SecurityBlockRuleDoc.findAll()).map((doc) => this.ruleFromDoc(doc));
}
public async listIpIntelligence(): Promise<IIpIntelligenceRecord[]> {
return (await IpIntelligenceDoc.findAll()).map((doc) => ({
ipAddress: doc.ipAddress,
asn: doc.asn,
asnOrg: doc.asnOrg,
registrantOrg: doc.registrantOrg,
registrantCountry: doc.registrantCountry,
networkRange: doc.networkRange,
abuseContact: doc.abuseContact,
country: doc.country,
countryCode: doc.countryCode,
city: doc.city,
latitude: doc.latitude,
longitude: doc.longitude,
accuracyRadius: doc.accuracyRadius,
timezone: doc.timezone,
firstSeenAt: doc.firstSeenAt,
lastSeenAt: doc.lastSeenAt,
updatedAt: doc.updatedAt,
seenCount: doc.seenCount,
}));
}
public async createBlockRule(input: {
type: TSecurityBlockRuleType;
value: string;
matchMode?: TSecurityBlockRuleMatchMode;
reason?: string;
enabled?: boolean;
}, actor = 'system'): Promise<ISecurityBlockRule> {
const now = Date.now();
const doc = new SecurityBlockRuleDoc();
doc.id = plugins.uuid.v4();
doc.type = input.type;
doc.value = input.value.trim();
doc.matchMode = input.matchMode;
doc.reason = input.reason;
doc.enabled = input.enabled ?? true;
doc.createdAt = now;
doc.updatedAt = now;
doc.createdBy = actor;
await doc.save();
await this.writeAudit('createBlockRule', actor, { rule: this.ruleFromDoc(doc) });
await this.notifyPolicyChanged();
return this.ruleFromDoc(doc);
}
public async updateBlockRule(id: string, patch: Partial<Pick<ISecurityBlockRule, 'value' | 'matchMode' | 'reason' | 'enabled'>>, actor = 'system'): Promise<ISecurityBlockRule | null> {
const doc = await SecurityBlockRuleDoc.findById(id);
if (!doc) {
return null;
}
if (patch.value !== undefined) doc.value = patch.value.trim();
if (patch.matchMode !== undefined) doc.matchMode = patch.matchMode;
if (patch.reason !== undefined) doc.reason = patch.reason;
if (patch.enabled !== undefined) doc.enabled = patch.enabled;
doc.updatedAt = Date.now();
await doc.save();
await this.writeAudit('updateBlockRule', actor, { id, patch });
await this.notifyPolicyChanged();
return this.ruleFromDoc(doc);
}
public async deleteBlockRule(id: string, actor = 'system'): Promise<boolean> {
const doc = await SecurityBlockRuleDoc.findById(id);
if (!doc) {
return false;
}
await doc.delete();
await this.writeAudit('deleteBlockRule', actor, { id });
await this.notifyPolicyChanged();
return true;
}
public async compilePolicy(): Promise<ISecurityCompiledPolicy> {
const rules = await SecurityBlockRuleDoc.findEnabled();
const intelligenceDocs = await IpIntelligenceDoc.findAll();
const blockedIps = new Set<string>();
const blockedCidrs = new Set<string>();
for (const rule of rules) {
const normalizedValue = rule.value.trim();
if (!normalizedValue) continue;
if (rule.type === 'ip') {
const ip = this.normalizeIp(normalizedValue);
if (ip && plugins.net.isIP(ip)) blockedIps.add(ip);
continue;
}
if (rule.type === 'cidr') {
const cidr = this.normalizeCidr(normalizedValue);
if (cidr) blockedCidrs.add(cidr);
continue;
}
for (const doc of intelligenceDocs) {
if (!this.ruleMatchesIntelligence(rule, doc)) continue;
const cidr = this.normalizeCidr(doc.networkRange || '');
if (cidr) {
blockedCidrs.add(cidr);
} else if (this.normalizeIp(doc.ipAddress)) {
blockedIps.add(this.normalizeIp(doc.ipAddress)!);
}
}
}
return {
blockedIps: [...blockedIps].sort(),
blockedCidrs: [...blockedCidrs].sort(),
};
}
public async compileSmartProxyPolicy(): Promise<ISecurityCompiledPolicy> {
return await this.compilePolicy();
}
public async compileRemoteIngressFirewall(): Promise<IRemoteIngressFirewallSnapshot | undefined> {
const policy = await this.compilePolicy();
const blockedIps = [
...policy.blockedIps.filter((ip) => plugins.net.isIP(ip) === 4),
...policy.blockedCidrs.filter((cidr) => plugins.net.isIP(cidr.split('/')[0]) === 4),
];
return blockedIps.length > 0 ? { blockedIps } : undefined;
}
private async matchesAnyReactiveRule(doc: IpIntelligenceDoc): Promise<boolean> {
const rules = await SecurityBlockRuleDoc.findEnabled();
return rules.some((rule) => rule.type === 'asn' || rule.type === 'organization'
? this.ruleMatchesIntelligence(rule, doc)
: false);
}
private ruleMatchesIntelligence(rule: SecurityBlockRuleDoc, doc: IpIntelligenceDoc): boolean {
const value = rule.value.trim().toLowerCase();
if (!value) return false;
if (rule.type === 'asn') {
return String(doc.asn ?? '') === value.replace(/^as/i, '');
}
if (rule.type === 'organization') {
const candidates = [doc.asnOrg, doc.registrantOrg]
.filter(Boolean)
.map((candidate) => candidate!.toLowerCase());
if (rule.matchMode === 'exact') {
return candidates.some((candidate) => candidate === value);
}
return candidates.some((candidate) => candidate.includes(value));
}
return false;
}
private normalizeIp(ipAddress: string): string | undefined {
const ip = ipAddress.trim();
if (ip.startsWith('::ffff:')) {
return ip.slice('::ffff:'.length);
}
return plugins.net.isIP(ip) ? ip : undefined;
}
private normalizeCidr(value: string): string | undefined {
const [rawIp, rawPrefix] = value.trim().split('/');
if (!rawIp || !rawPrefix) return undefined;
const ip = this.normalizeIp(rawIp);
if (!ip) return undefined;
const prefix = Number(rawPrefix);
const maxPrefix = plugins.net.isIP(ip) === 4 ? 32 : 128;
if (!Number.isInteger(prefix) || prefix < 0 || prefix > maxPrefix) return undefined;
return `${ip}/${prefix}`;
}
private isPublicIp(ip: string): boolean {
const family = plugins.net.isIP(ip);
if (family === 4) {
const parts = ip.split('.').map((part) => Number(part));
const [a, b] = parts;
if (a === 10 || a === 127 || a === 0 || a >= 224) return false;
if (a === 100 && b >= 64 && b <= 127) return false;
if (a === 169 && b === 254) return false;
if (a === 172 && b >= 16 && b <= 31) return false;
if (a === 192 && b === 168) return false;
return true;
}
if (family === 6) {
const lower = ip.toLowerCase();
if (lower === '::1' || lower === '::') return false;
if (lower.startsWith('fe80:') || lower.startsWith('fc') || lower.startsWith('fd')) return false;
return true;
}
return false;
}
private ruleFromDoc(doc: SecurityBlockRuleDoc): ISecurityBlockRule {
return {
id: doc.id,
type: doc.type,
value: doc.value,
matchMode: doc.matchMode,
enabled: doc.enabled,
reason: doc.reason,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
createdBy: doc.createdBy,
};
}
private async writeAudit(action: string, actor: string, details: Record<string, unknown>): Promise<void> {
const doc = new SecurityPolicyAuditDoc();
doc.id = plugins.uuid.v4();
doc.action = action;
doc.actor = actor;
doc.details = details;
doc.createdAt = Date.now();
await doc.save();
}
private async notifyPolicyChanged(): Promise<void> {
if (this.onPolicyChanged) {
await this.onPolicyChanged();
}
}
}
+7 -1
View File
@@ -18,4 +18,10 @@ export {
ThreatCategory,
type IScanResult,
type IContentScannerOptions
} from './classes.contentscanner.js';
} from './classes.contentscanner.js';
export {
SecurityPolicyManager,
type ISecurityPolicyManagerOptions,
type IRemoteIngressFirewallSnapshot,
} from './classes.security-policy-manager.js';
+6
View File
@@ -9,12 +9,14 @@ export class RemoteIngress {
public name: string;
public secret: string;
public listenPorts: number[];
public listenPortsUdp?: number[];
public enabled: boolean;
public autoDerivePorts: boolean;
public tags?: string[];
public createdAt: number;
public updatedAt: number;
public effectiveListenPorts?: number[];
public effectiveListenPortsUdp?: number[];
public manualPorts?: number[];
public derivedPorts?: number[];
@@ -24,12 +26,14 @@ export class RemoteIngress {
this.name = data.name;
this.secret = data.secret;
this.listenPorts = data.listenPorts;
this.listenPortsUdp = data.listenPortsUdp;
this.enabled = data.enabled;
this.autoDerivePorts = data.autoDerivePorts;
this.tags = data.tags;
this.createdAt = data.createdAt;
this.updatedAt = data.updatedAt;
this.effectiveListenPorts = data.effectiveListenPorts;
this.effectiveListenPortsUdp = data.effectiveListenPortsUdp;
this.manualPorts = data.manualPorts;
this.derivedPorts = data.derivedPorts;
}
@@ -52,11 +56,13 @@ export class RemoteIngress {
const edge = response.edge;
this.name = edge.name;
this.listenPorts = edge.listenPorts;
this.listenPortsUdp = edge.listenPortsUdp;
this.enabled = edge.enabled;
this.autoDerivePorts = edge.autoDerivePorts;
this.tags = edge.tags;
this.updatedAt = edge.updatedAt;
this.effectiveListenPorts = edge.effectiveListenPorts;
this.effectiveListenPortsUdp = edge.effectiveListenPortsUdp;
this.manualPorts = edge.manualPorts;
this.derivedPorts = edge.derivedPorts;
}
+2 -1
View File
@@ -8,4 +8,5 @@ export * from './dns-provider.js';
export * from './domain.js';
export * from './dns-record.js';
export * from './acme-config.js';
export * from './email-domain.js';
export * from './email-domain.js';
export * from './security-policy.js';
+58
View File
@@ -36,6 +36,64 @@ export interface IRemoteIngressStatus {
activeTunnels: number;
lastHeartbeat: number | null;
connectedAt: number | null;
transportMode?: 'tcpTls' | 'quic' | 'quicWithFallback';
fallbackUsed?: boolean;
performance?: IRemoteIngressPerformanceEffective;
flowControl?: IRemoteIngressFlowControlStatus;
queues?: IRemoteIngressQueueStatus;
traffic?: IRemoteIngressTrafficStatus;
udp?: IRemoteIngressUdpStatus;
}
export type TRemoteIngressPerformanceProfile = 'balanced' | 'throughput' | 'highConcurrency';
export interface IRemoteIngressPerformanceConfig {
profile?: TRemoteIngressPerformanceProfile;
maxStreamsPerEdge?: number;
totalWindowBudgetBytes?: number;
minStreamWindowBytes?: number;
maxStreamWindowBytes?: number;
sustainedStreamWindowBytes?: number;
quicDatagramReceiveBufferBytes?: number;
}
export interface IRemoteIngressPerformanceEffective {
profile: TRemoteIngressPerformanceProfile;
maxStreamsPerEdge: number;
totalWindowBudgetBytes: number;
minStreamWindowBytes: number;
maxStreamWindowBytes: number;
sustainedStreamWindowBytes: number;
quicDatagramReceiveBufferBytes: number;
}
export interface IRemoteIngressFlowControlStatus {
applies: boolean;
currentWindowBytes: number;
minWindowBytes: number;
maxWindowBytes: number;
totalWindowBudgetBytes: number;
estimatedInFlightBytes: number;
stalledStreams: number;
}
export interface IRemoteIngressQueueStatus {
ctrlQueueDepth: number;
dataQueueDepth: number;
sustainedQueueDepth: number;
}
export interface IRemoteIngressTrafficStatus {
bytesIn: number;
bytesOut: number;
streamsOpenedTotal: number;
streamsClosedTotal: number;
rejectedStreams: number;
}
export interface IRemoteIngressUdpStatus {
activeSessions: number;
droppedDatagrams: number;
}
/**
+37
View File
@@ -0,0 +1,37 @@
import type { IIpIntelligenceResult } from '@push.rocks/smartnetwork';
export type TSecurityBlockRuleType = 'ip' | 'cidr' | 'asn' | 'organization';
export type TSecurityBlockRuleMatchMode = 'exact' | 'contains';
export interface IIpIntelligenceRecord extends IIpIntelligenceResult {
ipAddress: string;
firstSeenAt: number;
lastSeenAt: number;
updatedAt: number;
seenCount: number;
}
export interface ISecurityBlockRule {
id: string;
type: TSecurityBlockRuleType;
value: string;
matchMode?: TSecurityBlockRuleMatchMode;
enabled: boolean;
reason?: string;
createdAt: number;
updatedAt: number;
createdBy: string;
}
export interface ISecurityCompiledPolicy {
blockedIps: string[];
blockedCidrs: string[];
}
export interface ISecurityPolicyAuditEvent {
id: string;
action: string;
actor: string;
details: Record<string, unknown>;
createdAt: number;
}
+1
View File
@@ -71,6 +71,7 @@ export interface IConfigData {
hubDomain: string | null;
tlsMode: 'custom' | 'acme' | 'self-signed';
connectedEdgeIps: string[];
performance?: import('../data/remoteingress.js').IRemoteIngressPerformanceConfig;
};
}
+2 -1
View File
@@ -18,4 +18,5 @@ export * from './dns-providers.js';
export * from './domains.js';
export * from './dns-records.js';
export * from './acme-config.js';
export * from './email-domains.js';
export * from './email-domains.js';
export * from './security-policy.js';
+89
View File
@@ -0,0 +1,89 @@
import * as plugins from '../plugins.js';
import type * as authInterfaces from '../data/auth.js';
import type {
IIpIntelligenceRecord,
ISecurityBlockRule,
TSecurityBlockRuleMatchMode,
TSecurityBlockRuleType,
} from '../data/security-policy.js';
export interface IReq_ListSecurityBlockRules extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_ListSecurityBlockRules
> {
method: 'listSecurityBlockRules';
request: {
identity: authInterfaces.IIdentity;
};
response: {
rules: ISecurityBlockRule[];
};
}
export interface IReq_CreateSecurityBlockRule extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_CreateSecurityBlockRule
> {
method: 'createSecurityBlockRule';
request: {
identity: authInterfaces.IIdentity;
type: TSecurityBlockRuleType;
value: string;
matchMode?: TSecurityBlockRuleMatchMode;
reason?: string;
enabled?: boolean;
};
response: {
success: boolean;
rule?: ISecurityBlockRule;
message?: string;
};
}
export interface IReq_UpdateSecurityBlockRule extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_UpdateSecurityBlockRule
> {
method: 'updateSecurityBlockRule';
request: {
identity: authInterfaces.IIdentity;
id: string;
value?: string;
matchMode?: TSecurityBlockRuleMatchMode;
reason?: string;
enabled?: boolean;
};
response: {
success: boolean;
rule?: ISecurityBlockRule;
message?: string;
};
}
export interface IReq_DeleteSecurityBlockRule extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_DeleteSecurityBlockRule
> {
method: 'deleteSecurityBlockRule';
request: {
identity: authInterfaces.IIdentity;
id: string;
};
response: {
success: boolean;
message?: string;
};
}
export interface IReq_ListIpIntelligence extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_ListIpIntelligence
> {
method: 'listIpIntelligence';
request: {
identity: authInterfaces.IIdentity;
};
response: {
records: IIpIntelligenceRecord[];
};
}
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '13.21.1',
version: '13.23.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}
@@ -125,6 +125,18 @@ export class OpsViewRemoteIngress extends DeesElement {
color: ${cssManager.bdTheme('#047857', '#34d399')};
border: 1px dashed ${cssManager.bdTheme('#6ee7b7', '#065f46')};
}
.metricStack {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 12px;
line-height: 1.35;
}
.metricMuted {
color: var(--text-muted, #6b7280);
}
`,
];
@@ -226,9 +238,13 @@ export class OpsViewRemoteIngress extends DeesElement {
.displayFunction=${(edge: interfaces.data.IRemoteIngress) => ({
name: edge.name,
status: this.getEdgeStatusHtml(edge),
transport: this.getTransportHtml(edge.id),
publicIp: this.getEdgePublicIp(edge.id),
ports: this.getPortsHtml(edge),
tunnels: this.getEdgeTunnelCount(edge.id),
window: this.getWindowHtml(edge.id),
queues: this.getQueuesHtml(edge.id),
traffic: this.getTrafficHtml(edge.id),
lastHeartbeat: this.getLastHeartbeat(edge.id),
})}
.dataActions=${[
@@ -459,6 +475,46 @@ export class OpsViewRemoteIngress extends DeesElement {
return status?.activeTunnels || 0;
}
private getTransportHtml(edgeId: string): TemplateResult | string {
const status = this.getEdgeStatus(edgeId);
if (!status?.connected) return '-';
const mode = status.transportMode || 'unknown';
const label = mode === 'quic' ? 'QUIC' : mode === 'tcpTls' ? 'TCP/TLS' : mode;
return html`<div class="metricStack"><strong>${label}</strong><span class="metricMuted">${status.fallbackUsed ? 'fallback' : status.performance?.profile || 'default'}</span></div>`;
}
private getWindowHtml(edgeId: string): TemplateResult | string {
const status = this.getEdgeStatus(edgeId);
if (!status?.connected || !status.flowControl) return '-';
if (!status.flowControl.applies) {
return html`<div class="metricStack"><span>native QUIC</span><span class="metricMuted">max ${status.performance?.maxStreamsPerEdge || '-'} streams</span></div>`;
}
return html`
<div class="metricStack">
<span>${this.formatBytes(status.flowControl.currentWindowBytes)} window</span>
<span class="metricMuted">${this.formatBytes(status.flowControl.estimatedInFlightBytes)} est. in-flight</span>
</div>
`;
}
private getQueuesHtml(edgeId: string): TemplateResult | string {
const status = this.getEdgeStatus(edgeId);
if (!status?.connected || !status.queues) return '-';
return html`<div class="metricStack"><span>C ${status.queues.ctrlQueueDepth} / D ${status.queues.dataQueueDepth}</span><span class="metricMuted">S ${status.queues.sustainedQueueDepth}</span></div>`;
}
private getTrafficHtml(edgeId: string): TemplateResult | string {
const status = this.getEdgeStatus(edgeId);
if (!status?.connected || !status.traffic) return '-';
const drops = (status.traffic.rejectedStreams || 0) + (status.udp?.droppedDatagrams || 0);
return html`
<div class="metricStack">
<span>${this.formatBytes(status.traffic.bytesIn)} in / ${this.formatBytes(status.traffic.bytesOut)} out</span>
<span class="metricMuted">${drops} rejected/dropped</span>
</div>
`;
}
private getLastHeartbeat(edgeId: string): string {
const status = this.getEdgeStatus(edgeId);
if (!status?.lastHeartbeat) return '-';
@@ -467,4 +523,16 @@ export class OpsViewRemoteIngress extends DeesElement {
if (ago < 3600000) return `${Math.floor(ago / 60000)}m ago`;
return `${Math.floor(ago / 3600000)}h ago`;
}
private formatBytes(bytes: number): string {
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let value = bytes;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value = value / 1024;
unitIndex++;
}
return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`;
}
}