feat(remoteingress,radius): add remote ingress performance overrides and update RADIUS integration
This commit is contained in:
@@ -11,6 +11,14 @@
|
||||
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
- add remote ingress performance overrides and update RADIUS integration (remoteingress,radius)
|
||||
- Persist and propagate optional remote ingress performance overrides through remote ingress create/update APIs, database documents, and hub allowed-edge sync.
|
||||
- Add web UI controls and status display for per-edge maximum connection overrides.
|
||||
- Extend remote ingress performance interfaces with stream payload, timeout, and server-first port settings.
|
||||
- Update RADIUS server integration for smartradius 1.2 request/response handling and client secret resolution, including CIDR matching.
|
||||
|
||||
## 2026-05-30 - 13.38.4
|
||||
|
||||
### Fixes
|
||||
|
||||
+2
-2
@@ -61,8 +61,8 @@
|
||||
"@push.rocks/smartnetwork": "^4.7.2",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.4",
|
||||
"@push.rocks/smartproxy": "^27.11.1",
|
||||
"@push.rocks/smartradius": "^1.1.2",
|
||||
"@push.rocks/smartproxy": "^27.11.2",
|
||||
"@push.rocks/smartradius": "^1.2.0",
|
||||
"@push.rocks/smartrequest": "^5.0.3",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/smartstate": "^2.3.1",
|
||||
|
||||
Generated
+10
-10
@@ -84,11 +84,11 @@ importers:
|
||||
specifier: ^4.2.4
|
||||
version: 4.2.4
|
||||
'@push.rocks/smartproxy':
|
||||
specifier: ^27.11.1
|
||||
version: 27.11.1
|
||||
specifier: ^27.11.2
|
||||
version: 27.11.2
|
||||
'@push.rocks/smartradius':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
'@push.rocks/smartrequest':
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
@@ -1429,14 +1429,14 @@ packages:
|
||||
'@push.rocks/smartpromise@4.2.4':
|
||||
resolution: {integrity: sha512-8FUyYt94hOIY9mqHjitn4h69u0jbEtTF2RKKw2DpiTVFjpDTk9gXbVHZ/V+xEcBrN4mrzdQES0OiDmkNPoddEQ==}
|
||||
|
||||
'@push.rocks/smartproxy@27.11.1':
|
||||
resolution: {integrity: sha512-29THhFUTr9NtU1/UBqqOgcbsHcUMHj7Dhh2XfXp6NP/rfDGUFiFFmCNcAdC3OJ0n6BgwTBOtOzo+4rJbrGJRpw==}
|
||||
'@push.rocks/smartproxy@27.11.2':
|
||||
resolution: {integrity: sha512-8FtOEWY49ipkMc/Yo5MQXcSnCU3Ee9NV3qLTUFgH6MeSZeWTanb1WXD3e4zXfyyviyegkKIorV79mYHo6jxx9Q==}
|
||||
|
||||
'@push.rocks/smartpuppeteer@2.0.6':
|
||||
resolution: {integrity: sha512-G+8cyDERvbXQcb9Sd8lnYdWYz8b3Mv2LfFf1ULmucDqQhcRHvxrWX/dKsvBZrwKPR4Wg+795Dyd+E1iOOh3tHw==}
|
||||
|
||||
'@push.rocks/smartradius@1.1.2':
|
||||
resolution: {integrity: sha512-p4fHhMgXZRuyRuMQjFQLVnXBG1Fz2latJ7BGAsfInOuVUaitBr/Wni9mZULAuIIddeWwUx9QvIGlv3tgmFn/ow==}
|
||||
'@push.rocks/smartradius@1.2.0':
|
||||
resolution: {integrity: sha512-dKh3l3WmjG+NiNRkVQilYbGSFIMXQ6OOnLGZISFRw8rK15GXY4WLvurqWs+EnJG/veskztjjzcjw7dD7dbA3IQ==}
|
||||
|
||||
'@push.rocks/smartrequest@2.1.0':
|
||||
resolution: {integrity: sha512-3eHLTRInHA+u+W98TqJwgTES7rRimBAsJC4JxVNQC3UUezmblAhM5/TIQsEBQTsbjAY8SeQKy6NHzW6iTiaD8w==}
|
||||
@@ -6696,7 +6696,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/smartpromise@4.2.4': {}
|
||||
|
||||
'@push.rocks/smartproxy@27.11.1':
|
||||
'@push.rocks/smartproxy@27.11.2':
|
||||
dependencies:
|
||||
'@push.rocks/smartcrypto': 2.0.4
|
||||
'@push.rocks/smartlog': 3.2.2
|
||||
@@ -6720,7 +6720,7 @@ snapshots:
|
||||
- typescript
|
||||
- utf-8-validate
|
||||
|
||||
'@push.rocks/smartradius@1.1.2':
|
||||
'@push.rocks/smartradius@1.2.0':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.1.0
|
||||
'@push.rocks/smartpromise': 4.2.4
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
||||
import type { IRemoteIngressPerformanceConfig } from '../../../ts_interfaces/data/remoteingress.js';
|
||||
|
||||
const getDb = () => DcRouterDb.getInstance().getDb();
|
||||
|
||||
@@ -27,6 +28,9 @@ export class RemoteIngressEdgeDoc extends plugins.smartdata.SmartDataDbDoc<Remot
|
||||
@plugins.smartdata.svDb()
|
||||
public autoDerivePorts!: boolean;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public performance?: IRemoteIngressPerformanceConfig;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public tags!: string[];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type * as plugins from '../plugins.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* Configuration for HTTP/3 (QUIC) route augmentation.
|
||||
@@ -36,22 +36,6 @@ export interface IHttp3Config {
|
||||
};
|
||||
}
|
||||
|
||||
type TPortRange = plugins.smartproxy.IRouteConfig['match']['ports'];
|
||||
|
||||
/**
|
||||
* Check whether a TPortRange includes port 443.
|
||||
*/
|
||||
function portRangeIncludes443(ports: TPortRange): boolean {
|
||||
if (typeof ports === 'number') return ports === 443;
|
||||
if (Array.isArray(ports)) {
|
||||
return ports.some((p) => {
|
||||
if (typeof p === 'number') return p === 443;
|
||||
return p.from <= 443 && p.to >= 443;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a route name indicates an email route that should not get HTTP/3.
|
||||
*/
|
||||
@@ -85,7 +69,7 @@ export function routeQualifiesForHttp3(
|
||||
if (route.action.type !== 'forward') return false;
|
||||
|
||||
// Must include port 443
|
||||
if (!portRangeIncludes443(route.match.ports)) return false;
|
||||
if (!plugins.smartproxy.portRangeIncludes(route.match.ports, 443)) return false;
|
||||
|
||||
// Must have TLS
|
||||
if (!route.action.tls) return false;
|
||||
|
||||
@@ -67,6 +67,7 @@ export class RemoteIngressHandler {
|
||||
dataArg.listenPorts || [],
|
||||
dataArg.tags,
|
||||
dataArg.autoDerivePorts ?? true,
|
||||
dataArg.performance,
|
||||
);
|
||||
|
||||
// Sync allowed edges with the hub
|
||||
@@ -129,6 +130,7 @@ export class RemoteIngressHandler {
|
||||
listenPorts: dataArg.listenPorts,
|
||||
autoDerivePorts: dataArg.autoDerivePorts,
|
||||
enabled: dataArg.enabled,
|
||||
performance: dataArg.performance,
|
||||
tags: dataArg.tags,
|
||||
});
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ export class RadiusServer {
|
||||
authPort: this.config.authPort,
|
||||
acctPort: this.config.acctPort,
|
||||
bindAddress: this.config.bindAddress,
|
||||
defaultSecret: this.getDefaultSecret(),
|
||||
secretResolver: this.resolveClientSecret.bind(this),
|
||||
authenticationHandler: this.handleAuthentication.bind(this),
|
||||
accountingHandler: this.handleAccounting.bind(this),
|
||||
});
|
||||
@@ -189,19 +189,21 @@ export class RadiusServer {
|
||||
/**
|
||||
* Handle authentication request
|
||||
*/
|
||||
private async handleAuthentication(request: any): Promise<any> {
|
||||
private async handleAuthentication(
|
||||
request: plugins.smartradius.IAuthenticationRequest,
|
||||
): Promise<plugins.smartradius.IAuthenticationResponse> {
|
||||
this.stats.authRequests++;
|
||||
|
||||
const authData: IAuthRequestData = {
|
||||
username: request.attributes?.UserName || '',
|
||||
password: request.attributes?.UserPassword,
|
||||
nasIpAddress: request.attributes?.NasIpAddress || request.source?.address || '',
|
||||
nasPort: request.attributes?.NasPort,
|
||||
nasPortType: request.attributes?.NasPortType,
|
||||
nasIdentifier: request.attributes?.NasIdentifier,
|
||||
calledStationId: request.attributes?.CalledStationId,
|
||||
callingStationId: request.attributes?.CallingStationId,
|
||||
serviceType: request.attributes?.ServiceType,
|
||||
username: request.username || '',
|
||||
password: request.password,
|
||||
nasIpAddress: request.nasIpAddress || request.clientAddress || '',
|
||||
nasPort: request.nasPort,
|
||||
nasPortType: request.nasPortType !== undefined ? String(request.nasPortType) : undefined,
|
||||
nasIdentifier: request.nasIdentifier,
|
||||
calledStationId: request.calledStationId,
|
||||
callingStationId: request.callingStationId,
|
||||
serviceType: request.serviceType !== undefined ? String(request.serviceType) : undefined,
|
||||
};
|
||||
|
||||
logger.log('debug', `RADIUS Auth Request: user=${authData.username}, NAS=${authData.nasIpAddress}`);
|
||||
@@ -215,15 +217,15 @@ export class RadiusServer {
|
||||
logger.log('info', `RADIUS Auth Accept: user=${authData.username}, VLAN=${result.vlanId}`);
|
||||
|
||||
// Build response with VLAN attributes
|
||||
const response: any = {
|
||||
const response: plugins.smartradius.IAuthenticationResponse = {
|
||||
code: plugins.smartradius.ERadiusCode.AccessAccept,
|
||||
replyMessage: result.replyMessage,
|
||||
};
|
||||
|
||||
// Add VLAN attributes if assigned
|
||||
if (result.vlanId !== undefined) {
|
||||
response.tunnelType = 13; // VLAN
|
||||
response.tunnelMediumType = 6; // IEEE 802
|
||||
response.tunnelType = plugins.smartradius.ETunnelType.Vlan;
|
||||
response.tunnelMediumType = plugins.smartradius.ETunnelMediumType.Ieee802;
|
||||
response.tunnelPrivateGroupId = String(result.vlanId);
|
||||
}
|
||||
|
||||
@@ -257,34 +259,35 @@ export class RadiusServer {
|
||||
/**
|
||||
* Handle accounting request
|
||||
*/
|
||||
private async handleAccounting(request: any): Promise<any> {
|
||||
private async handleAccounting(
|
||||
request: plugins.smartradius.IAccountingRequest,
|
||||
): Promise<plugins.smartradius.IAccountingResponse> {
|
||||
this.stats.accountingRequests++;
|
||||
|
||||
if (!this.config.accounting?.enabled) {
|
||||
// Still respond even if not tracking
|
||||
return { code: plugins.smartradius.ERadiusCode.AccountingResponse };
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const statusType = request.attributes?.AcctStatusType;
|
||||
const sessionId = request.attributes?.AcctSessionId || '';
|
||||
const statusType = request.statusType;
|
||||
const sessionId = request.sessionId || '';
|
||||
|
||||
const accountingData = {
|
||||
sessionId,
|
||||
username: request.attributes?.UserName || '',
|
||||
macAddress: request.attributes?.CallingStationId,
|
||||
nasIpAddress: request.attributes?.NasIpAddress || request.source?.address || '',
|
||||
nasPort: request.attributes?.NasPort,
|
||||
nasPortType: request.attributes?.NasPortType,
|
||||
nasIdentifier: request.attributes?.NasIdentifier,
|
||||
calledStationId: request.attributes?.CalledStationId,
|
||||
callingStationId: request.attributes?.CallingStationId,
|
||||
inputOctets: request.attributes?.AcctInputOctets,
|
||||
outputOctets: request.attributes?.AcctOutputOctets,
|
||||
inputPackets: request.attributes?.AcctInputPackets,
|
||||
outputPackets: request.attributes?.AcctOutputPackets,
|
||||
sessionTime: request.attributes?.AcctSessionTime,
|
||||
terminateCause: request.attributes?.AcctTerminateCause,
|
||||
serviceType: request.attributes?.ServiceType,
|
||||
username: request.username || '',
|
||||
macAddress: request.callingStationId,
|
||||
nasIpAddress: request.nasIpAddress || request.clientAddress || '',
|
||||
nasPort: request.nasPort,
|
||||
nasPortType: request.nasPortType !== undefined ? String(request.nasPortType) : undefined,
|
||||
nasIdentifier: request.nasIdentifier,
|
||||
calledStationId: request.calledStationId,
|
||||
callingStationId: request.callingStationId,
|
||||
inputOctets: request.inputOctets,
|
||||
outputOctets: request.outputOctets,
|
||||
inputPackets: request.inputPackets,
|
||||
outputPackets: request.outputPackets,
|
||||
sessionTime: request.sessionTime,
|
||||
terminateCause: request.terminateCause !== undefined ? String(request.terminateCause) : undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -311,7 +314,7 @@ export class RadiusServer {
|
||||
logger.log('error', `RADIUS accounting error: ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
return { code: plugins.smartradius.ERadiusCode.AccountingResponse };
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -399,29 +402,58 @@ export class RadiusServer {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle CIDR ranges
|
||||
if (client.ipRange.includes('/')) {
|
||||
// For CIDR ranges, we'll use the network address as key
|
||||
// In practice, smartradius may handle this differently
|
||||
const [network] = client.ipRange.split('/');
|
||||
this.clientSecrets.set(network, client.secret);
|
||||
} else {
|
||||
if (!client.ipRange.includes('/')) {
|
||||
this.clientSecrets.set(client.ipRange, client.secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default secret for unknown clients
|
||||
*/
|
||||
private getDefaultSecret(): string {
|
||||
// Use first enabled client's secret as default, or a random one
|
||||
private resolveClientSecret(clientAddress: string): string | undefined {
|
||||
const normalizedClientAddress = this.normalizeClientAddress(clientAddress);
|
||||
if (!normalizedClientAddress) return undefined;
|
||||
|
||||
const exactSecret = this.clientSecrets.get(normalizedClientAddress);
|
||||
if (exactSecret !== undefined) return exactSecret;
|
||||
|
||||
for (const client of this.config.clients) {
|
||||
if (client.enabled) {
|
||||
return client.secret;
|
||||
}
|
||||
if (!client.enabled || !client.ipRange.includes('/')) continue;
|
||||
if (this.clientIpMatchesCidr(normalizedClientAddress, client.ipRange)) return client.secret;
|
||||
}
|
||||
return plugins.crypto.randomBytes(16).toString('hex');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private normalizeClientAddress(clientAddress: string): string | undefined {
|
||||
const trimmedAddress = clientAddress.trim();
|
||||
const normalizedAddress = trimmedAddress.startsWith('::ffff:')
|
||||
? trimmedAddress.slice('::ffff:'.length)
|
||||
: trimmedAddress;
|
||||
return plugins.net.isIP(normalizedAddress) ? normalizedAddress : undefined;
|
||||
}
|
||||
|
||||
private clientIpMatchesCidr(clientAddress: string, cidr: string): boolean {
|
||||
const [networkAddress, rawPrefix] = cidr.trim().split('/');
|
||||
const normalizedNetworkAddress = this.normalizeClientAddress(networkAddress || '');
|
||||
if (!normalizedNetworkAddress || rawPrefix === undefined) return false;
|
||||
|
||||
const clientIp = this.ipv4ToNumber(clientAddress);
|
||||
const networkIp = this.ipv4ToNumber(normalizedNetworkAddress);
|
||||
if (clientIp === undefined || networkIp === undefined) return false;
|
||||
|
||||
const prefix = Number(rawPrefix);
|
||||
if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) return false;
|
||||
|
||||
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
|
||||
return ((clientIp & mask) >>> 0) === ((networkIp & mask) >>> 0);
|
||||
}
|
||||
|
||||
private ipv4ToNumber(ipAddress: string): number | undefined {
|
||||
if (plugins.net.isIP(ipAddress) !== 4) return undefined;
|
||||
const parts = ipAddress.split('.').map((part) => Number(part));
|
||||
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
||||
return undefined;
|
||||
}
|
||||
return (((parts[0] * 256 + parts[1]) * 256 + parts[2]) * 256 + parts[3]) >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -436,16 +468,10 @@ export class RadiusServer {
|
||||
this.config.clients.push(client);
|
||||
}
|
||||
|
||||
// Update client secrets if running
|
||||
if (this.running && this.radiusServer && client.enabled) {
|
||||
if (client.ipRange.includes('/')) {
|
||||
const [network] = client.ipRange.split('/');
|
||||
this.radiusServer.setClientSecret(network, client.secret);
|
||||
this.clientSecrets.set(network, client.secret);
|
||||
} else {
|
||||
this.radiusServer.setClientSecret(client.ipRange, client.secret);
|
||||
this.clientSecrets.set(client.ipRange, client.secret);
|
||||
}
|
||||
this.buildClientSecretsMap();
|
||||
|
||||
if (this.running && this.radiusServer && client.enabled && !client.ipRange.includes('/')) {
|
||||
this.radiusServer.setClientSecret(client.ipRange, client.secret);
|
||||
}
|
||||
|
||||
logger.log('info', `RADIUS client ${client.enabled ? 'added' : 'disabled'}: ${client.name} (${client.ipRange})`);
|
||||
@@ -460,12 +486,10 @@ export class RadiusServer {
|
||||
const client = this.config.clients[index];
|
||||
this.config.clients.splice(index, 1);
|
||||
|
||||
// Remove from secrets map
|
||||
if (client.ipRange.includes('/')) {
|
||||
const [network] = client.ipRange.split('/');
|
||||
this.clientSecrets.delete(network);
|
||||
} else {
|
||||
this.clientSecrets.delete(client.ipRange);
|
||||
this.buildClientSecretsMap();
|
||||
|
||||
if (this.radiusServer && !client.ipRange.includes('/')) {
|
||||
this.radiusServer.removeClientSecret(client.ipRange);
|
||||
}
|
||||
|
||||
logger.log('info', `RADIUS client removed: ${name}`);
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IRemoteIngress, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
||||
import type { IRemoteIngress, IRemoteIngressPerformanceConfig, 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.
|
||||
*/
|
||||
function extractPorts(portRange: number | Array<number | { from: number; to: number }>): number[] {
|
||||
const ports = new Set<number>();
|
||||
if (typeof portRange === 'number') {
|
||||
ports.add(portRange);
|
||||
} else if (Array.isArray(portRange)) {
|
||||
for (const entry of portRange) {
|
||||
if (typeof entry === 'number') {
|
||||
ports.add(entry);
|
||||
} else if (typeof entry === 'object' && 'from' in entry && 'to' in entry) {
|
||||
for (let p = entry.from; p <= entry.to; p++) {
|
||||
ports.add(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function extractPorts(portRange: plugins.smartproxy.IRouteConfig['match']['ports']): number[] {
|
||||
const ports = new Set<number>(plugins.smartproxy.expandPortRange(portRange) as number[]);
|
||||
return [...ports].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
@@ -59,6 +43,7 @@ export class RemoteIngressManager {
|
||||
listenPortsUdp: doc.listenPortsUdp,
|
||||
enabled: doc.enabled,
|
||||
autoDerivePorts: doc.autoDerivePorts,
|
||||
performance: doc.performance,
|
||||
tags: doc.tags,
|
||||
createdAt: doc.createdAt,
|
||||
updatedAt: doc.updatedAt,
|
||||
@@ -189,6 +174,7 @@ export class RemoteIngressManager {
|
||||
listenPorts: number[] = [],
|
||||
tags?: string[],
|
||||
autoDerivePorts: boolean = true,
|
||||
performance?: IRemoteIngressPerformanceConfig,
|
||||
): Promise<IRemoteIngress> {
|
||||
const id = plugins.uuid.v4();
|
||||
const secret = plugins.crypto.randomBytes(32).toString('hex');
|
||||
@@ -201,6 +187,7 @@ export class RemoteIngressManager {
|
||||
listenPorts,
|
||||
enabled: true,
|
||||
autoDerivePorts,
|
||||
performance,
|
||||
tags: tags || [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
@@ -237,6 +224,7 @@ export class RemoteIngressManager {
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
enabled?: boolean;
|
||||
performance?: IRemoteIngressPerformanceConfig;
|
||||
tags?: string[];
|
||||
},
|
||||
): Promise<IRemoteIngress | null> {
|
||||
@@ -249,6 +237,7 @@ export class RemoteIngressManager {
|
||||
if (updates.listenPorts !== undefined) edge.listenPorts = updates.listenPorts;
|
||||
if (updates.autoDerivePorts !== undefined) edge.autoDerivePorts = updates.autoDerivePorts;
|
||||
if (updates.enabled !== undefined) edge.enabled = updates.enabled;
|
||||
if (updates.performance !== undefined) edge.performance = updates.performance;
|
||||
if (updates.tags !== undefined) edge.tags = updates.tags;
|
||||
edge.updatedAt = Date.now();
|
||||
|
||||
@@ -317,17 +306,19 @@ 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[]; firewallConfig?: IRemoteIngressFirewallConfig }> {
|
||||
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig }> = [];
|
||||
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig; performance?: IRemoteIngressPerformanceConfig }> {
|
||||
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig; performance?: IRemoteIngressPerformanceConfig }> = [];
|
||||
for (const edge of this.edges.values()) {
|
||||
if (edge.enabled) {
|
||||
const listenPortsUdp = this.getEffectiveListenPortsUdp(edge);
|
||||
const performance = edge.performance && Object.keys(edge.performance).length > 0 ? edge.performance : undefined;
|
||||
result.push({
|
||||
id: edge.id,
|
||||
secret: edge.secret,
|
||||
listenPorts: this.getEffectiveListenPorts(edge),
|
||||
...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}),
|
||||
...(this.firewallConfig ? { firewallConfig: this.firewallConfig } : {}),
|
||||
...(performance ? { performance } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface IRemoteIngress {
|
||||
enabled: boolean;
|
||||
/** Whether to auto-derive ports from remoteIngress-tagged routes. Defaults to true. */
|
||||
autoDerivePorts: boolean;
|
||||
/** Optional per-edge performance overrides. */
|
||||
performance?: IRemoteIngressPerformanceConfig;
|
||||
tags?: string[];
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
@@ -55,6 +57,10 @@ export interface IRemoteIngressPerformanceConfig {
|
||||
maxStreamWindowBytes?: number;
|
||||
sustainedStreamWindowBytes?: number;
|
||||
quicDatagramReceiveBufferBytes?: number;
|
||||
streamFramePayloadBytes?: number;
|
||||
firstDataConnectTimeoutMs?: number;
|
||||
clientWriteTimeoutMs?: number;
|
||||
serverFirstPorts?: number[];
|
||||
}
|
||||
|
||||
export interface IRemoteIngressPerformanceEffective {
|
||||
@@ -65,6 +71,10 @@ export interface IRemoteIngressPerformanceEffective {
|
||||
maxStreamWindowBytes: number;
|
||||
sustainedStreamWindowBytes: number;
|
||||
quicDatagramReceiveBufferBytes: number;
|
||||
streamFramePayloadBytes: number;
|
||||
firstDataConnectTimeoutMs: number;
|
||||
clientWriteTimeoutMs: number;
|
||||
serverFirstPorts: number[];
|
||||
}
|
||||
|
||||
export interface IRemoteIngressFlowControlStatus {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as authInterfaces from '../data/auth.js';
|
||||
import type { IRemoteIngress, IRemoteIngressStatus } from '../data/remoteingress.js';
|
||||
import type { IRemoteIngress, IRemoteIngressPerformanceConfig, IRemoteIngressStatus } from '../data/remoteingress.js';
|
||||
|
||||
// ============================================================================
|
||||
// Remote Ingress Edge Management
|
||||
@@ -20,6 +20,7 @@ export interface IReq_CreateRemoteIngress extends plugins.typedrequestInterfaces
|
||||
name: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
performance?: IRemoteIngressPerformanceConfig;
|
||||
tags?: string[];
|
||||
};
|
||||
response: {
|
||||
@@ -63,6 +64,7 @@ export interface IReq_UpdateRemoteIngress extends plugins.typedrequestInterfaces
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
enabled?: boolean;
|
||||
performance?: IRemoteIngressPerformanceConfig;
|
||||
tags?: string[];
|
||||
};
|
||||
response: {
|
||||
|
||||
@@ -1120,6 +1120,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
name: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
performance?: interfaces.data.IRemoteIngressPerformanceConfig;
|
||||
tags?: string[];
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
|
||||
const context = getActionContext();
|
||||
@@ -1135,6 +1136,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
name: dataArg.name,
|
||||
listenPorts: dataArg.listenPorts,
|
||||
autoDerivePorts: dataArg.autoDerivePorts,
|
||||
performance: dataArg.performance,
|
||||
tags: dataArg.tags,
|
||||
});
|
||||
|
||||
@@ -1187,6 +1189,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
name?: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
performance?: interfaces.data.IRemoteIngressPerformanceConfig;
|
||||
tags?: string[];
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
|
||||
const context = getActionContext();
|
||||
@@ -1203,6 +1206,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
name: dataArg.name,
|
||||
listenPorts: dataArg.listenPorts,
|
||||
autoDerivePorts: dataArg.autoDerivePorts,
|
||||
performance: dataArg.performance,
|
||||
tags: dataArg.tags,
|
||||
});
|
||||
|
||||
|
||||
@@ -242,6 +242,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
publicIp: this.getEdgePublicIp(edge.id),
|
||||
ports: this.getPortsHtml(edge),
|
||||
tunnels: this.getEdgeTunnelCount(edge.id),
|
||||
maxConnections: this.getMaxConnectionsHtml(edge),
|
||||
window: this.getWindowHtml(edge.id),
|
||||
queues: this.getQueuesHtml(edge.id),
|
||||
traffic: this.getTrafficHtml(edge.id),
|
||||
@@ -261,6 +262,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'listenPorts'} .label=${'Manual Ports'} .description=${'Comma-separated port numbers, optional'}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${true}></dees-input-checkbox>
|
||||
<dees-input-text .key=${'maxStreamsPerEdge'} .label=${'Max Connections'} .description=${'Optional maximum concurrent client connections for this edge. Leave empty to use the hub default.'}></dees-input-text>
|
||||
<dees-input-text .key=${'tags'} .label=${'Tags'} .description=${'Comma-separated, optional'}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
@@ -284,12 +286,20 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p))
|
||||
: undefined;
|
||||
const autoDerivePorts = formData.autoDerivePorts !== false;
|
||||
let performance: interfaces.data.IRemoteIngressPerformanceConfig | undefined;
|
||||
try {
|
||||
performance = this.collectPerformanceOverride(formData);
|
||||
} catch (err: unknown) {
|
||||
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||
DeesToast.show({ message: (err as Error).message, type: 'error', duration: 4000 });
|
||||
return;
|
||||
}
|
||||
const tags = formData.tags
|
||||
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
await appstate.remoteIngressStatePart.dispatchAction(
|
||||
appstate.createRemoteIngressAction,
|
||||
{ name, listenPorts, autoDerivePorts, tags },
|
||||
{ name, listenPorts, autoDerivePorts, performance, tags },
|
||||
);
|
||||
await modalArg.destroy();
|
||||
},
|
||||
@@ -338,6 +348,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .value=${edge.name}></dees-input-text>
|
||||
<dees-input-text .key=${'listenPorts'} .label=${'Manual Ports'} .description=${'Comma-separated port numbers'} .value=${(edge.listenPorts || []).join(', ')}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${edge.autoDerivePorts !== false}></dees-input-checkbox>
|
||||
<dees-input-text .key=${'maxStreamsPerEdge'} .label=${'Max Connections'} .description=${'Optional maximum concurrent client connections for this edge. Leave empty to use the hub default.'} .value=${edge.performance?.maxStreamsPerEdge?.toString() || ''}></dees-input-text>
|
||||
<dees-input-text .key=${'tags'} .label=${'Tags'} .description=${'Comma-separated'} .value=${(edge.tags || []).join(', ')}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
@@ -359,6 +370,14 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p))
|
||||
: [];
|
||||
const autoDerivePorts = formData.autoDerivePorts !== false;
|
||||
let performance: interfaces.data.IRemoteIngressPerformanceConfig | undefined;
|
||||
try {
|
||||
performance = this.collectPerformanceOverride(formData, edge.performance);
|
||||
} catch (err: unknown) {
|
||||
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||
DeesToast.show({ message: (err as Error).message, type: 'error', duration: 4000 });
|
||||
return;
|
||||
}
|
||||
const tags = formData.tags
|
||||
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: [];
|
||||
@@ -369,6 +388,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
name: formData.name || edge.name,
|
||||
listenPorts,
|
||||
autoDerivePorts,
|
||||
performance,
|
||||
tags,
|
||||
},
|
||||
);
|
||||
@@ -475,6 +495,19 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
return status?.activeTunnels || 0;
|
||||
}
|
||||
|
||||
private getMaxConnectionsHtml(edge: interfaces.data.IRemoteIngress): TemplateResult | string {
|
||||
const status = this.getEdgeStatus(edge.id);
|
||||
const override = edge.performance?.maxStreamsPerEdge;
|
||||
const effective = status?.performance?.maxStreamsPerEdge;
|
||||
if (!override && !effective) return '-';
|
||||
return html`
|
||||
<div class="metricStack">
|
||||
<span>${override || effective}</span>
|
||||
<span class="metricMuted">${override ? 'edge override' : 'hub default'}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getTransportHtml(edgeId: string): TemplateResult | string {
|
||||
const status = this.getEdgeStatus(edgeId);
|
||||
if (!status?.connected) return '-';
|
||||
@@ -535,4 +568,27 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
}
|
||||
return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
private collectPerformanceOverride(
|
||||
formData: Record<string, any>,
|
||||
base?: interfaces.data.IRemoteIngressPerformanceConfig,
|
||||
): interfaces.data.IRemoteIngressPerformanceConfig | undefined {
|
||||
const next: interfaces.data.IRemoteIngressPerformanceConfig = { ...(base || {}) };
|
||||
const maxStreamsText = `${formData.maxStreamsPerEdge || ''}`.trim();
|
||||
if (maxStreamsText) {
|
||||
const maxStreamsPerEdge = Number.parseInt(maxStreamsText, 10);
|
||||
if (!Number.isInteger(maxStreamsPerEdge) || maxStreamsPerEdge < 1) {
|
||||
throw new Error('Max Connections must be a positive integer');
|
||||
}
|
||||
next.maxStreamsPerEdge = maxStreamsPerEdge;
|
||||
} else {
|
||||
delete next.maxStreamsPerEdge;
|
||||
}
|
||||
|
||||
if (Object.keys(next).length > 0) {
|
||||
return next;
|
||||
}
|
||||
|
||||
return base ? {} : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +304,16 @@ export class OpsViewConfig extends DeesElement {
|
||||
{ key: 'Connected Edge IPs', value: ri.connectedEdgeIps?.length > 0 ? ri.connectedEdgeIps : null, type: 'pills' },
|
||||
];
|
||||
|
||||
if (ri.performance) {
|
||||
fields.push(
|
||||
{ key: 'Performance Profile', value: ri.performance.profile || null, type: 'badge' },
|
||||
{ key: 'Max Connections / Edge', value: ri.performance.maxStreamsPerEdge ?? null },
|
||||
{ key: 'Client Write Timeout', value: ri.performance.clientWriteTimeoutMs ? `${ri.performance.clientWriteTimeoutMs} ms` : null },
|
||||
{ key: 'First Data Timeout', value: ri.performance.firstDataConnectTimeoutMs ? `${ri.performance.firstDataConnectTimeoutMs} ms` : null },
|
||||
{ key: 'Server-first Ports', value: ri.performance.serverFirstPorts?.length ? ri.performance.serverFirstPorts.map(String) : null, type: 'pills' },
|
||||
);
|
||||
}
|
||||
|
||||
const actions: IConfigSectionAction[] = [
|
||||
{ label: 'View Remote Ingress', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'network', subview: 'remoteingress' } },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user