Compare commits

...

4 Commits

Author SHA1 Message Date
1a62c52d24 v4.15.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-26 16:39:53 +00:00
e9a08bdd0f feat(edge,hub): add hub-controlled nftables firewall configuration for remote ingress edges 2026-03-26 16:39:53 +00:00
c2c9dd195d v4.14.3
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-26 07:49:44 +00:00
fb6e9c54ad fix(docs): refresh project metadata and README to reflect current ingress tunnel capabilities 2026-03-26 07:49:44 +00:00
12 changed files with 394 additions and 83 deletions

View File

@@ -11,26 +11,26 @@
"githost": "code.foss.global", "githost": "code.foss.global",
"gitscope": "serve.zone", "gitscope": "serve.zone",
"gitrepo": "remoteingress", "gitrepo": "remoteingress",
"description": "Provides a service for creating private tunnels and reaching private clusters from the outside, facilitating secure remote access as part of the @serve.zone stack.", "description": "Edge ingress tunnel for DcRouter - tunnels TCP and UDP traffic from the network edge to SmartProxy over TLS or QUIC, preserving client IP via PROXY protocol.",
"npmPackagename": "@serve.zone/remoteingress", "npmPackagename": "@serve.zone/remoteingress",
"license": "MIT", "license": "MIT",
"projectDomain": "serve.zone", "projectDomain": "serve.zone",
"keywords": [ "keywords": [
"remote access", "remote access",
"private tunnels", "ingress tunnel",
"network security", "network edge",
"TLS encryption", "PROXY protocol",
"connector", "multiplexed tunnel",
"TCP proxy",
"TLS tunnel",
"QUIC transport",
"UDP tunneling",
"serve.zone stack", "serve.zone stack",
"private clusters access", "TypeScript",
"public access management", "Rust",
"TypeScript application", "SmartProxy",
"node.js package", "DcRouter",
"secure communications", "flow control"
"TLS/SSL certificates",
"development tools",
"software development",
"private network integration"
] ]
}, },
"release": { "release": {

View File

@@ -1,5 +1,20 @@
# Changelog # Changelog
## 2026-03-26 - 4.15.0 - feat(edge,hub)
add hub-controlled nftables firewall configuration for remote ingress edges
- add firewallConfig support to allowed edge definitions, handshake responses, and runtime config updates
- emit firewallConfigUpdated events from the Rust bridge and edge runtime when firewall settings change
- initialize SmartNftables on edges, apply blocked IPs, rate limits, and custom rules, and clean up nftables rules on stop
- document centralized firewall management, root requirements, and new edge events in the README
## 2026-03-26 - 4.14.3 - fix(docs)
refresh project metadata and README to reflect current ingress tunnel capabilities
- update package metadata description and keywords to better describe edge ingress, TLS/QUIC transport, and SmartProxy integration
- revise README terminology, API docs, and feature list to document crash recovery, bindAddress support, and current event names
- improve README formatting and examples for architecture, wire protocol, QoS, and token usage
## 2026-03-26 - 4.14.2 - fix(hub-core) ## 2026-03-26 - 4.14.2 - fix(hub-core)
improve stream shutdown handling and connection cleanup in hub and edge improve stream shutdown handling and connection cleanup in hub and edge

View File

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/remoteingress", "name": "@serve.zone/remoteingress",
"version": "4.14.2", "version": "4.15.0",
"private": false, "private": false,
"description": "Edge ingress tunnel for DcRouter - tunnels TCP and UDP traffic from the network edge to SmartProxy over TLS or QUIC, preserving client IP via PROXY protocol.", "description": "Edge ingress tunnel for DcRouter - tunnels TCP and UDP traffic from the network edge to SmartProxy over TLS or QUIC, preserving client IP via PROXY protocol.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
@@ -24,6 +24,7 @@
}, },
"dependencies": { "dependencies": {
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartnftables": "^1.0.1",
"@push.rocks/smartrust": "^1.3.2" "@push.rocks/smartrust": "^1.3.2"
}, },
"repository": { "repository": {

11
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@push.rocks/qenv': '@push.rocks/qenv':
specifier: ^6.1.3 specifier: ^6.1.3
version: 6.1.3 version: 6.1.3
'@push.rocks/smartnftables':
specifier: ^1.0.1
version: 1.0.1
'@push.rocks/smartrust': '@push.rocks/smartrust':
specifier: ^1.3.2 specifier: ^1.3.2
version: 1.3.2 version: 1.3.2
@@ -1204,6 +1207,9 @@ packages:
'@push.rocks/smartnetwork@4.4.0': '@push.rocks/smartnetwork@4.4.0':
resolution: {integrity: sha512-OvFtz41cvQ7lcXwaIOhghNUUlNoMxvwKDctbDvMyuZyEH08SpLjhyv2FuKbKL/mgwA/WxakTbohoC8SW7t+kiw==} resolution: {integrity: sha512-OvFtz41cvQ7lcXwaIOhghNUUlNoMxvwKDctbDvMyuZyEH08SpLjhyv2FuKbKL/mgwA/WxakTbohoC8SW7t+kiw==}
'@push.rocks/smartnftables@1.0.1':
resolution: {integrity: sha512-o822GH4J8dlEBvNLbm+CwU4h6isMUEh03tf2ZnOSWXc5iewRDdKdOCDwI/e+WdnGYWyv7gvH0DHztCmne6rTCg==}
'@push.rocks/smartnpm@2.0.6': '@push.rocks/smartnpm@2.0.6':
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==} resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
@@ -6433,6 +6439,11 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@push.rocks/smartnftables@1.0.1':
dependencies:
'@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartnpm@2.0.6': '@push.rocks/smartnpm@2.0.6':
dependencies: dependencies:
'@push.rocks/consolecolor': 2.0.3 '@push.rocks/consolecolor': 2.0.3

234
readme.md
View File

@@ -1,6 +1,6 @@
# @serve.zone/remoteingress # @serve.zone/remoteingress
Edge ingress tunnel for DcRouter — tunnels **TCP and UDP** traffic from the network edge to a private DcRouter/SmartProxy cluster over encrypted TLS or QUIC connections, preserving the original client IP via PROXY protocol. Edge ingress tunnel for DcRouter — tunnels **TCP and UDP** traffic from the network edge to a private DcRouter/SmartProxy cluster over encrypted TLS or QUIC connections, preserving the original client IP via PROXY protocol. Includes **hub-controlled nftables firewall** for IP blocking, rate limiting, and custom firewall rules applied directly at the edge.
## Issue Reporting and Security ## Issue Reporting and Security
@@ -12,49 +12,55 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
pnpm install @serve.zone/remoteingress pnpm install @serve.zone/remoteingress
``` ```
## 🏗️ Architecture ## Architecture
`@serve.zone/remoteingress` uses a **Hub/Edge** topology with a high-performance Rust core and a TypeScript API surface: `@serve.zone/remoteingress` uses a **Hub/Edge** topology with a high-performance Rust core and a TypeScript API surface:
``` ```
┌─────────────────────┐ TLS or QUIC Tunnel ┌─────────────────────┐ TLS or QUIC Tunnel
│ Network Edge │ ◄══════════════════════════► │ Private Cluster │ ┌─────────────────────┐ ◄══════════════════════════► ┌─────────────────────┐
│ TCP+TLS: frame mux │ Network Edge │ TCP+TLS: frame mux │ Private Cluster
RemoteIngressEdge │ QUIC: native streams │ RemoteIngressHub │ QUIC: native streams
│ UDP: QUIC datagrams │ RemoteIngressEdge │ UDP: QUIC datagrams │ RemoteIngressHub
Accepts TCP & UDP │ │ Forwards to
on hub-assigned │ │ SmartProxy on Accepts TCP & UDP Forwards to
ports │ │ local ports on hub-assigned SmartProxy on
└─────────────────────┘ └─────────────────────┘ │ ports │ local ports │
│ TCP + UDP from end users 🔥 nftables rules │ ◄── firewall config pushed ── Configures edge │
Internet DcRouter / SmartProxy applied locally via FRAME_CONFIG │ firewalls remotely │
└─────────────────────┘ └─────────────────────┘
▲ │
│ TCP + UDP from end users ▼
Internet DcRouter / SmartProxy
``` ```
| Component | Role | | Component | Role |
|-----------|------| |-----------|------|
| **RemoteIngressEdge** | Deployed at the network edge (VPS, cloud instance). Listens on TCP and UDP ports assigned by the hub, accepts connections/datagrams, and tunnels them to the hub. Ports are hot-reloadable at runtime. | | **RemoteIngressEdge** | Deployed at the network edge (VPS, cloud instance). Runs as root. Listens on TCP and UDP ports assigned by the hub, accepts connections/datagrams, and tunnels them to the hub. Applies nftables firewall rules pushed by the hub for IP blocking and rate limiting. Ports and firewall config are hot-reloadable at runtime. |
| **RemoteIngressHub** | Deployed alongside DcRouter/SmartProxy in a private cluster. Accepts edge connections, demuxes streams/datagrams, and forwards each to SmartProxy with PROXY protocol headers so the real client IP is preserved. | | **RemoteIngressHub** | Deployed alongside DcRouter/SmartProxy in a private cluster. Accepts edge connections, demuxes streams/datagrams, and forwards each to SmartProxy with PROXY protocol headers so the real client IP is preserved. Pushes firewall configuration to edges. |
| **Rust Binary** (`remoteingress-bin`) | The performance-critical networking core. Managed via `@push.rocks/smartrust` RustBridge IPC — you never interact with it directly. Cross-compiled for `linux/amd64` and `linux/arm64`. | | **Rust Binary** (`remoteingress-bin`) | The performance-critical networking core. Managed via `@push.rocks/smartrust` RustBridge IPC — you never interact with it directly. Cross-compiled for `linux/amd64` and `linux/arm64`. |
### Key Features ### Key Features
- 🔒 **Dual transport** — choose between TCP+TLS (frame-multiplexed) or QUIC (native stream multiplexing, zero head-of-line blocking) - **Dual transport** — choose between TCP+TLS (frame-multiplexed) or QUIC (native stream multiplexing, zero head-of-line blocking)
- 🌐 **TCP + UDP tunneling** — tunnel any TCP connection or UDP datagram through the same edge/hub pair - **TCP + UDP tunneling** — tunnel any TCP connection or UDP datagram through the same edge/hub pair
- 📋 **PROXY protocol v1 & v2** — SmartProxy sees the real client IP for both TCP (v1 text) and UDP (v2 binary) - **PROXY protocol v1 & v2** — SmartProxy sees the real client IP for both TCP (v1 text) and UDP (v2 binary)
- 🔀 **Multiplexed streams**thousands of concurrent TCP connections over a single tunnel - **Hub-controlled firewall** — push nftables rules (IP blocking, rate limiting, custom rules) from the hub to edges via `@push.rocks/smartnftables`
- **QUIC datagrams**UDP traffic forwarded via QUIC unreliable datagrams for lowest possible latency - **Multiplexed streams** — thousands of concurrent TCP connections over a single tunnel
- 🔑 **Shared-secret authentication**edges must present valid credentials to connect - **QUIC datagrams** — UDP traffic forwarded via QUIC unreliable datagrams for lowest possible latency
- 🎫 **Connection tokens** — encode all connection details into a single opaque base64url string - **Shared-secret authentication** — edges must present valid credentials to connect
- 📡 **STUN-based public IP discovery** — edges automatically discover their public IP via Cloudflare STUN - **Connection tokens** — encode all connection details into a single opaque base64url string
- 🔄 **Auto-reconnect** with exponential backoff if the tunnel drops - **STUN-based public IP discovery** — edges automatically discover their public IP via Cloudflare STUN
- 🎛️ **Dynamic port configuration** — the hub assigns TCP and UDP listen ports per edge, hot-reloadable at runtime - **Auto-reconnect** with exponential backoff if the tunnel drops
- 📣 **Event-driven**both Hub and Edge extend `EventEmitter` for real-time monitoring - **Dynamic port configuration** — the hub assigns TCP and UDP listen ports per edge, hot-reloadable at runtime
- 🎚️ **3-tier QoS**control frames, normal data, and sustained (elephant flow) traffic each get their own priority queue - **Event-driven** — both Hub and Edge extend `EventEmitter` for real-time monitoring
- 📊 **Adaptive flow control** — per-stream windows scale with active stream count to prevent memory overuse - **3-tier QoS** — control frames, normal data, and sustained (elephant flow) traffic each get their own priority queue
- 🕒 **UDP session management**automatic session tracking with 60s idle timeout and cleanup - **Adaptive flow control** — per-stream windows scale with active stream count to prevent memory overuse
- **UDP session management** — automatic session tracking with 60s idle timeout and cleanup
- **Crash recovery** — automatic restart with exponential backoff if the Rust binary crashes unexpectedly
## 🚀 Usage ## Usage
Both classes are imported from the package and communicate with the Rust binary under the hood. Both classes are imported from the package and communicate with the Rust binary under the hood.
@@ -77,7 +83,7 @@ await hub.start({
targetHost: '127.0.0.1', // SmartProxy host to forward traffic to targetHost: '127.0.0.1', // SmartProxy host to forward traffic to
}); });
// Register allowed edges with TCP and UDP listen ports // Register allowed edges with TCP and UDP listen ports + firewall config
await hub.updateAllowedEdges([ await hub.updateAllowedEdges([
{ {
id: 'edge-nyc-01', id: 'edge-nyc-01',
@@ -85,6 +91,15 @@ await hub.updateAllowedEdges([
listenPorts: [80, 443], // TCP ports the edge should listen on listenPorts: [80, 443], // TCP ports the edge should listen on
listenPortsUdp: [53, 51820], // UDP ports (e.g., DNS, WireGuard) listenPortsUdp: [53, 51820], // UDP ports (e.g., DNS, WireGuard)
stunIntervalSecs: 300, stunIntervalSecs: 300,
firewallConfig: {
blockedIps: ['192.168.1.100', '10.0.0.0/8'],
rateLimits: [
{ id: 'http-rate', port: 80, protocol: 'tcp', rate: '100/second', perSourceIP: true },
],
rules: [
{ id: 'allow-ssh', direction: 'input', action: 'accept', sourceIP: '10.0.0.0/24', destPort: 22, protocol: 'tcp' },
],
},
}, },
{ {
id: 'edge-fra-02', id: 'edge-fra-02',
@@ -93,13 +108,19 @@ await hub.updateAllowedEdges([
}, },
]); ]);
// Dynamically update ports — changes are pushed instantly to connected edges // Dynamically update ports and firewall — changes are pushed instantly to connected edges
await hub.updateAllowedEdges([ await hub.updateAllowedEdges([
{ {
id: 'edge-nyc-01', id: 'edge-nyc-01',
secret: 'supersecrettoken1', secret: 'supersecrettoken1',
listenPorts: [80, 443, 8443], // added TCP port 8443 listenPorts: [80, 443, 8443], // added TCP port 8443
listenPortsUdp: [53], // removed WireGuard UDP port listenPortsUdp: [53], // removed WireGuard UDP port
firewallConfig: {
blockedIps: ['192.168.1.100', '10.0.0.0/8', '203.0.113.50'], // added new blocked IP
rateLimits: [
{ id: 'http-rate', port: 80, protocol: 'tcp', rate: '200/second', perSourceIP: true },
],
},
}, },
]); ]);
@@ -112,7 +133,7 @@ await hub.stop();
### Setting Up the Edge (Network Edge Side) ### Setting Up the Edge (Network Edge Side)
The edge can connect via **TCP+TLS** (default) or **QUIC** transport. The edge can connect via **TCP+TLS** (default) or **QUIC** transport. Edges run as **root** so they can bind to privileged ports and apply nftables firewall rules.
#### Option A: Connection Token (Recommended) #### Option A: Connection Token (Recommended)
@@ -125,6 +146,7 @@ edge.on('tunnelConnected', () => console.log('Tunnel established'));
edge.on('tunnelDisconnected', () => console.log('Tunnel lost — will auto-reconnect')); edge.on('tunnelDisconnected', () => console.log('Tunnel lost — will auto-reconnect'));
edge.on('publicIpDiscovered', ({ ip }) => console.log(`Public IP: ${ip}`)); edge.on('publicIpDiscovered', ({ ip }) => console.log(`Public IP: ${ip}`));
edge.on('portsAssigned', ({ listenPorts }) => console.log(`TCP ports: ${listenPorts}`)); edge.on('portsAssigned', ({ listenPorts }) => console.log(`TCP ports: ${listenPorts}`));
edge.on('firewallConfigUpdated', () => console.log('Firewall rules applied'));
await edge.start({ await edge.start({
token: 'eyJoIjoiaHViLmV4YW1wbGUuY29tIiwi...', token: 'eyJoIjoiaHViLmV4YW1wbGUuY29tIiwi...',
@@ -160,7 +182,7 @@ await edge.stop();
| `'quic'` | QUIC with native stream multiplexing. Eliminates head-of-line blocking. Uses QUIC datagrams for UDP traffic. | | `'quic'` | QUIC with native stream multiplexing. Eliminates head-of-line blocking. Uses QUIC datagrams for UDP traffic. |
| `'quicWithFallback'` | Tries QUIC first (5s timeout), falls back to TCP+TLS if UDP is blocked by the network. | | `'quicWithFallback'` | Tries QUIC first (5s timeout), falls back to TCP+TLS if UDP is blocked by the network. |
### 🎫 Connection Tokens ### Connection Tokens
Encode all connection details into a single opaque string for easy distribution: Encode all connection details into a single opaque string for easy distribution:
@@ -183,7 +205,78 @@ const data = decodeConnectionToken(token);
Tokens are base64url-encoded — safe for environment variables, CLI arguments, and config files. Tokens are base64url-encoded — safe for environment variables, CLI arguments, and config files.
## 📖 API Reference ## 🔥 Hub-Controlled Firewall
Edges run as root and use `@push.rocks/smartnftables` to apply nftables rules pushed from the hub. This gives you centralized control over network-level security at every edge node.
### How It Works
1. The hub includes `firewallConfig` when calling `updateAllowedEdges()`
2. The config flows through the Rust binary as an opaque JSON blob via `FRAME_CONFIG`
3. The edge TypeScript layer receives it and applies the rules using `SmartNftables`
4. On each config update, all previous rules are replaced atomically (full replacement, not incremental)
### Firewall Config Structure
```typescript
interface IFirewallConfig {
blockedIps?: string[]; // IPs or CIDRs to block (e.g., '1.2.3.4', '10.0.0.0/8')
rateLimits?: IFirewallRateLimit[];
rules?: IFirewallRule[];
}
interface IFirewallRateLimit {
id: string; // unique identifier for this rate limit
port: number; // port to rate-limit
protocol?: 'tcp' | 'udp'; // default: both
rate: string; // e.g., '100/second', '1000/minute'
burst?: number; // burst allowance
perSourceIP?: boolean; // per-client rate limiting (recommended)
}
interface IFirewallRule {
id: string; // unique identifier for this rule
direction: 'input' | 'output' | 'forward';
action: 'accept' | 'drop' | 'reject';
sourceIP?: string; // source IP or CIDR
destPort?: number; // destination port
protocol?: 'tcp' | 'udp';
comment?: string;
}
```
### Example: Rate Limiting + IP Blocking
```typescript
await hub.updateAllowedEdges([
{
id: 'edge-nyc-01',
secret: 'secret',
listenPorts: [80, 443],
firewallConfig: {
// Block known bad actors
blockedIps: ['198.51.100.0/24', '203.0.113.50'],
// Rate limit HTTP traffic per source IP
rateLimits: [
{ id: 'http', port: 80, protocol: 'tcp', rate: '100/second', burst: 50, perSourceIP: true },
{ id: 'https', port: 443, protocol: 'tcp', rate: '200/second', burst: 100, perSourceIP: true },
],
// Allow monitoring from trusted subnet
rules: [
{ id: 'monitoring', direction: 'input', action: 'accept', sourceIP: '10.0.0.0/24', destPort: 9090, protocol: 'tcp', comment: 'Prometheus scraping' },
],
},
},
]);
```
### Graceful Degradation
If the edge isn't running as root or nftables is unavailable, the SmartNftables initialization logs a warning and continues operating normally — the tunnel works fine, just without kernel-level firewall rules.
## API Reference
### `RemoteIngressHub` ### `RemoteIngressHub`
@@ -191,22 +284,22 @@ Tokens are base64url-encoded — safe for environment variables, CLI arguments,
|-------------------|-------------| |-------------------|-------------|
| `start(config?)` | Start the hub. Config: `{ tunnelPort?, targetHost?, tls?: { certPem?, keyPem? } }`. Listens on both TCP and UDP (QUIC) on the tunnel port. | | `start(config?)` | Start the hub. Config: `{ tunnelPort?, targetHost?, tls?: { certPem?, keyPem? } }`. Listens on both TCP and UDP (QUIC) on the tunnel port. |
| `stop()` | Graceful shutdown. | | `stop()` | Graceful shutdown. |
| `updateAllowedEdges(edges)` | Set authorized edges. Each: `{ id, secret, listenPorts?, listenPortsUdp?, stunIntervalSecs? }`. Port changes are pushed to connected edges in real time. | | `updateAllowedEdges(edges)` | Set authorized edges. Each: `{ id, secret, listenPorts?, listenPortsUdp?, stunIntervalSecs?, firewallConfig? }`. Port and firewall changes are pushed to connected edges in real time. |
| `getStatus()` | Returns `{ running, tunnelPort, connectedEdges: [...] }`. | | `getStatus()` | Returns `{ running, tunnelPort, connectedEdges: [...] }`. |
| `running` | `boolean` — whether the Rust binary is alive. | | `running` | `boolean` — whether the Rust binary is alive. |
**Events:** `edgeConnected`, `edgeDisconnected`, `streamOpened`, `streamClosed` **Events:** `edgeConnected`, `edgeDisconnected`, `streamOpened`, `streamClosed`, `crashRecovered`, `crashRecoveryFailed`
### `RemoteIngressEdge` ### `RemoteIngressEdge`
| Method / Property | Description | | Method / Property | Description |
|-------------------|-------------| |-------------------|-------------|
| `start(config)` | Connect to hub. Accepts `{ token }` or `{ hubHost, hubPort, edgeId, secret, transportMode? }`. | | `start(config)` | Connect to hub. Accepts `{ token }` or `{ hubHost, hubPort, edgeId, secret, bindAddress?, transportMode? }`. |
| `stop()` | Graceful shutdown. | | `stop()` | Graceful shutdown. Cleans up all nftables rules. |
| `getStatus()` | Returns `{ running, connected, publicIp, activeStreams, listenPorts }`. | | `getStatus()` | Returns `{ running, connected, publicIp, activeStreams, listenPorts }`. |
| `running` | `boolean` — whether the Rust binary is alive. | | `running` | `boolean` — whether the Rust binary is alive. |
**Events:** `tunnelConnected`, `tunnelDisconnected`, `publicIpDiscovered`, `portsAssigned`, `portsUpdated` **Events:** `tunnelConnected`, `tunnelDisconnected`, `publicIpDiscovered`, `portsAssigned`, `portsUpdated`, `firewallConfigUpdated`, `crashRecovered`, `crashRecoveryFailed`
### Token Utilities ### Token Utilities
@@ -244,7 +337,7 @@ interface IConnectionTokenData {
} }
``` ```
## 🔌 Wire Protocol ## Wire Protocol
### TCP+TLS Transport (Frame Protocol) ### TCP+TLS Transport (Frame Protocol)
@@ -261,7 +354,7 @@ The tunnel uses a custom binary frame protocol over a single TLS connection:
| `CLOSE` | `0x03` | Edge → Hub | Client closed connection | | `CLOSE` | `0x03` | Edge → Hub | Client closed connection |
| `DATA_BACK` | `0x04` | Hub → Edge | Response data (download) | | `DATA_BACK` | `0x04` | Hub → Edge | Response data (download) |
| `CLOSE_BACK` | `0x05` | Hub → Edge | Upstream closed connection | | `CLOSE_BACK` | `0x05` | Hub → Edge | Upstream closed connection |
| `CONFIG` | `0x06` | Hub → Edge | Runtime config update (JSON payload) | | `CONFIG` | `0x06` | Hub → Edge | Runtime config update (JSON: ports + firewall config) |
| `PING` | `0x07` | Hub → Edge | Heartbeat probe (every 15s) | | `PING` | `0x07` | Hub → Edge | Heartbeat probe (every 15s) |
| `PONG` | `0x08` | Edge → Hub | Heartbeat response | | `PONG` | `0x08` | Edge → Hub | Heartbeat response |
| `WINDOW_UPDATE` | `0x09` | Edge → Hub | Flow control: edge consumed N bytes | | `WINDOW_UPDATE` | `0x09` | Edge → Hub | Flow control: edge consumed N bytes |
@@ -284,19 +377,20 @@ When using QUIC, the frame protocol is replaced by native QUIC primitives:
1. Edge opens a TLS or QUIC connection to the hub 1. Edge opens a TLS or QUIC connection to the hub
2. Edge sends: `EDGE <edgeId> <secret>\n` 2. Edge sends: `EDGE <edgeId> <secret>\n`
3. Hub verifies credentials (constant-time comparison) and responds with JSON: 3. Hub verifies credentials (constant-time comparison) and responds with JSON:
`{"listenPorts":[...],"listenPortsUdp":[...],"stunIntervalSecs":300}\n` `{"listenPorts":[...],"listenPortsUdp":[...],"stunIntervalSecs":300,"firewallConfig":{...}}\n`
4. Edge starts TCP and UDP listeners on the assigned ports 4. Edge starts TCP and UDP listeners on the assigned ports
5. Data flows — TCP frames/QUIC streams for TCP traffic, UDP frames/QUIC datagrams for UDP traffic 5. Edge applies firewall config via nftables (if present and running as root)
6. Data flows — TCP frames/QUIC streams for TCP traffic, UDP frames/QUIC datagrams for UDP traffic
## 🎚️ QoS & Flow Control ## QoS & Flow Control
### Priority Tiers (TCP+TLS Transport) ### Priority Tiers (TCP+TLS Transport)
| Tier | Frames | Behavior | | Tier | Frames | Behavior |
|------|--------|----------| |------|--------|----------|
| 🔴 **Control** | PING, PONG, WINDOW_UPDATE, OPEN, CLOSE, CONFIG | Always drained first. Never delayed. | | **Control** | PING, PONG, WINDOW_UPDATE, OPEN, CLOSE, CONFIG | Always drained first. Never delayed. |
| 🟡 **Data** | DATA/DATA_BACK from normal streams, UDP frames | Drained when control queue is empty. | | **Data** | DATA/DATA_BACK from normal streams, UDP frames | Drained when control queue is empty. |
| 🟢 **Sustained** | DATA/DATA_BACK from elephant flows | Lowest priority with guaranteed **1 MB/s** drain rate. | | **Sustained** | DATA/DATA_BACK from elephant flows | Lowest priority with guaranteed **1 MB/s** drain rate. |
### Sustained Stream Classification ### Sustained Stream Classification
@@ -318,17 +412,17 @@ Each TCP stream has a send window from a shared **200 MB budget**:
UDP traffic uses no flow control — datagrams are fire-and-forget, matching UDP semantics. UDP traffic uses no flow control — datagrams are fire-and-forget, matching UDP semantics.
## 💡 Example Scenarios ## Example Scenarios
### 1. Expose a Private Cluster to the Internet ### 1. 🌐 Expose a Private Cluster to the Internet
Deploy an Edge on a public VPS, point DNS to its IP. The Edge tunnels all TCP and UDP traffic to the Hub running inside your private cluster. No public ports needed on the cluster. Deploy an Edge on a public VPS, point DNS to its IP. The Edge tunnels all TCP and UDP traffic to the Hub running inside your private cluster. No public ports needed on the cluster.
### 2. Multi-Region Edge Ingress ### 2. 🗺️ Multi-Region Edge Ingress
Run Edges in NYC, Frankfurt, and Tokyo — all connecting to a single Hub. Use GeoDNS to route users to their nearest Edge. PROXY protocol ensures the Hub sees real client IPs regardless of which Edge they entered through. Run Edges in NYC, Frankfurt, and Tokyo — all connecting to a single Hub. Use GeoDNS to route users to their nearest Edge. PROXY protocol ensures the Hub sees real client IPs regardless of which Edge they entered through.
### 3. UDP Forwarding (DNS, Gaming, VoIP) ### 3. 📡 UDP Forwarding (DNS, Gaming, VoIP)
Configure UDP listen ports alongside TCP ports. DNS queries, game server traffic, or VoIP packets are tunneled through the same edge/hub connection and forwarded to SmartProxy with a PROXY v2 binary header preserving the client's real IP. Configure UDP listen ports alongside TCP ports. DNS queries, game server traffic, or VoIP packets are tunneled through the same edge/hub connection and forwarded to SmartProxy with a PROXY v2 binary header preserving the client's real IP.
@@ -343,7 +437,7 @@ await hub.updateAllowedEdges([
]); ]);
``` ```
### 4. QUIC Transport for Low-Latency ### 4. 🚀 QUIC Transport for Low-Latency
Use QUIC transport to eliminate head-of-line blocking — a lost packet on one stream doesn't stall others. QUIC also enables 0-RTT reconnection and connection migration. Use QUIC transport to eliminate head-of-line blocking — a lost packet on one stream doesn't stall others. QUIC also enables 0-RTT reconnection and connection migration.
@@ -357,11 +451,13 @@ await edge.start({
}); });
``` ```
### 5. Token-Based Edge Provisioning ### 5. 🔑 Token-Based Edge Provisioning
Generate connection tokens on the hub side and distribute them to edge operators: Generate connection tokens on the hub side and distribute them to edge operators:
```typescript ```typescript
import { encodeConnectionToken, RemoteIngressEdge } from '@serve.zone/remoteingress';
const token = encodeConnectionToken({ const token = encodeConnectionToken({
hubHost: 'hub.prod.example.com', hubHost: 'hub.prod.example.com',
hubPort: 8443, hubPort: 8443,
@@ -374,9 +470,33 @@ const edge = new RemoteIngressEdge();
await edge.start({ token }); await edge.start({ token });
``` ```
### 6. 🛡️ Centralized Firewall Management
Push firewall rules from the hub to all your edge nodes. Block bad actors, rate-limit abusive traffic, and whitelist trusted subnets — all from a single control plane:
```typescript
await hub.updateAllowedEdges([
{
id: 'edge-nyc-01',
secret: 'secret',
listenPorts: [80, 443],
firewallConfig: {
blockedIps: ['198.51.100.0/24'],
rateLimits: [
{ id: 'https', port: 443, protocol: 'tcp', rate: '500/second', perSourceIP: true, burst: 100 },
],
rules: [
{ id: 'allow-monitoring', direction: 'input', action: 'accept', sourceIP: '10.0.0.0/8', destPort: 9090, protocol: 'tcp' },
],
},
},
]);
// Firewall rules are applied at the edge via nftables within seconds
```
## License and Legal Information ## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license.md) file. This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file. **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

View File

@@ -316,6 +316,12 @@ async fn handle_request(
serde_json::json!({ "listenPorts": listen_ports }), serde_json::json!({ "listenPorts": listen_ports }),
); );
} }
EdgeEvent::FirewallConfigUpdated { firewall_config } => {
send_event(
"firewallConfigUpdated",
serde_json::json!({ "firewallConfig": firewall_config }),
);
}
} }
} }
}); });

View File

@@ -67,6 +67,8 @@ struct HandshakeConfig {
listen_ports_udp: Vec<u16>, listen_ports_udp: Vec<u16>,
#[serde(default = "default_stun_interval")] #[serde(default = "default_stun_interval")]
stun_interval_secs: u64, stun_interval_secs: u64,
#[serde(default)]
firewall_config: Option<serde_json::Value>,
} }
fn default_stun_interval() -> u64 { fn default_stun_interval() -> u64 {
@@ -80,6 +82,8 @@ struct ConfigUpdate {
listen_ports: Vec<u16>, listen_ports: Vec<u16>,
#[serde(default)] #[serde(default)]
listen_ports_udp: Vec<u16>, listen_ports_udp: Vec<u16>,
#[serde(default)]
firewall_config: Option<serde_json::Value>,
} }
/// Events emitted by the edge. /// Events emitted by the edge.
@@ -96,6 +100,8 @@ pub enum EdgeEvent {
PortsAssigned { listen_ports: Vec<u16> }, PortsAssigned { listen_ports: Vec<u16> },
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
PortsUpdated { listen_ports: Vec<u16> }, PortsUpdated { listen_ports: Vec<u16> },
#[serde(rename_all = "camelCase")]
FirewallConfigUpdated { firewall_config: serde_json::Value },
} }
/// Edge status response. /// Edge status response.
@@ -439,6 +445,11 @@ async fn handle_edge_frame(
connection_token, connection_token,
bind_address, bind_address,
); );
if let Some(fw_config) = update.firewall_config {
let _ = event_tx.try_send(EdgeEvent::FirewallConfigUpdated {
firewall_config: fw_config,
});
}
} }
} }
FRAME_PING => { FRAME_PING => {
@@ -569,6 +580,13 @@ async fn connect_to_hub_and_run(
listen_ports: handshake.listen_ports.clone(), listen_ports: handshake.listen_ports.clone(),
}); });
// Emit firewall config if present in handshake
if let Some(fw_config) = handshake.firewall_config {
let _ = event_tx.try_send(EdgeEvent::FirewallConfigUpdated {
firewall_config: fw_config,
});
}
// Start STUN discovery // Start STUN discovery
let stun_interval = handshake.stun_interval_secs; let stun_interval = handshake.stun_interval_secs;
let public_ip_clone = public_ip.clone(); let public_ip_clone = public_ip.clone();
@@ -1309,6 +1327,13 @@ async fn connect_to_hub_and_run_quic_with_connection(
listen_ports: handshake.listen_ports.clone(), listen_ports: handshake.listen_ports.clone(),
}); });
// Emit firewall config if present in handshake
if let Some(fw_config) = handshake.firewall_config {
let _ = event_tx.try_send(EdgeEvent::FirewallConfigUpdated {
firewall_config: fw_config,
});
}
// Start STUN discovery // Start STUN discovery
let stun_interval = handshake.stun_interval_secs; let stun_interval = handshake.stun_interval_secs;
let public_ip_clone = public_ip.clone(); let public_ip_clone = public_ip.clone();

View File

@@ -80,6 +80,8 @@ pub struct AllowedEdge {
#[serde(default)] #[serde(default)]
pub listen_ports_udp: Vec<u16>, pub listen_ports_udp: Vec<u16>,
pub stun_interval_secs: Option<u64>, pub stun_interval_secs: Option<u64>,
#[serde(default)]
pub firewall_config: Option<serde_json::Value>,
} }
/// Handshake response sent to edge after authentication. /// Handshake response sent to edge after authentication.
@@ -90,6 +92,8 @@ struct HandshakeResponse {
#[serde(default)] #[serde(default)]
listen_ports_udp: Vec<u16>, listen_ports_udp: Vec<u16>,
stun_interval_secs: u64, stun_interval_secs: u64,
#[serde(skip_serializing_if = "Option::is_none")]
firewall_config: Option<serde_json::Value>,
} }
/// Configuration update pushed to a connected edge at runtime. /// Configuration update pushed to a connected edge at runtime.
@@ -99,6 +103,8 @@ pub struct EdgeConfigUpdate {
pub listen_ports: Vec<u16>, pub listen_ports: Vec<u16>,
#[serde(default)] #[serde(default)]
pub listen_ports_udp: Vec<u16>, pub listen_ports_udp: Vec<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub firewall_config: Option<serde_json::Value>,
} }
/// Runtime status of a connected edge. /// Runtime status of a connected edge.
@@ -192,14 +198,17 @@ impl TunnelHub {
for edge in &edges { for edge in &edges {
if let Some(info) = connected.get(&edge.id) { if let Some(info) = connected.get(&edge.id) {
// Check if ports changed compared to old config // Check if ports changed compared to old config
let ports_changed = match map.get(&edge.id) { let config_changed = match map.get(&edge.id) {
Some(old) => old.listen_ports != edge.listen_ports || old.listen_ports_udp != edge.listen_ports_udp, Some(old) => old.listen_ports != edge.listen_ports
|| old.listen_ports_udp != edge.listen_ports_udp
|| old.firewall_config != edge.firewall_config,
None => true, // newly allowed edge that's already connected None => true, // newly allowed edge that's already connected
}; };
if ports_changed { if config_changed {
let update = EdgeConfigUpdate { let update = EdgeConfigUpdate {
listen_ports: edge.listen_ports.clone(), listen_ports: edge.listen_ports.clone(),
listen_ports_udp: edge.listen_ports_udp.clone(), listen_ports_udp: edge.listen_ports_udp.clone(),
firewall_config: edge.firewall_config.clone(),
}; };
let _ = info.config_tx.try_send(update); let _ = info.config_tx.try_send(update);
} }
@@ -861,14 +870,14 @@ async fn handle_edge_connection(
let secret = parts[2]; let secret = parts[2];
// Verify credentials and extract edge config // Verify credentials and extract edge config
let (listen_ports, listen_ports_udp, stun_interval_secs) = { let (listen_ports, listen_ports_udp, stun_interval_secs, firewall_config) = {
let edges = allowed.read().await; let edges = allowed.read().await;
match edges.get(&edge_id) { match edges.get(&edge_id) {
Some(edge) => { Some(edge) => {
if !constant_time_eq(secret.as_bytes(), edge.secret.as_bytes()) { if !constant_time_eq(secret.as_bytes(), edge.secret.as_bytes()) {
return Err(format!("invalid secret for edge {}", edge_id).into()); return Err(format!("invalid secret for edge {}", edge_id).into());
} }
(edge.listen_ports.clone(), edge.listen_ports_udp.clone(), edge.stun_interval_secs.unwrap_or(300)) (edge.listen_ports.clone(), edge.listen_ports_udp.clone(), edge.stun_interval_secs.unwrap_or(300), edge.firewall_config.clone())
} }
None => { None => {
return Err(format!("unknown edge {}", edge_id).into()); return Err(format!("unknown edge {}", edge_id).into());
@@ -887,6 +896,7 @@ async fn handle_edge_connection(
listen_ports: listen_ports.clone(), listen_ports: listen_ports.clone(),
listen_ports_udp: listen_ports_udp.clone(), listen_ports_udp: listen_ports_udp.clone(),
stun_interval_secs, stun_interval_secs,
firewall_config,
}; };
let mut handshake_json = serde_json::to_string(&handshake)?; let mut handshake_json = serde_json::to_string(&handshake)?;
handshake_json.push('\n'); handshake_json.push('\n');
@@ -1228,14 +1238,14 @@ async fn handle_edge_connection_quic(
let secret = parts[2]; let secret = parts[2];
// Verify credentials // Verify credentials
let (listen_ports, listen_ports_udp, stun_interval_secs) = { let (listen_ports, listen_ports_udp, stun_interval_secs, firewall_config) = {
let edges = allowed.read().await; let edges = allowed.read().await;
match edges.get(&edge_id) { match edges.get(&edge_id) {
Some(edge) => { Some(edge) => {
if !constant_time_eq(secret.as_bytes(), edge.secret.as_bytes()) { if !constant_time_eq(secret.as_bytes(), edge.secret.as_bytes()) {
return Err(format!("invalid secret for edge {}", edge_id).into()); return Err(format!("invalid secret for edge {}", edge_id).into());
} }
(edge.listen_ports.clone(), edge.listen_ports_udp.clone(), edge.stun_interval_secs.unwrap_or(300)) (edge.listen_ports.clone(), edge.listen_ports_udp.clone(), edge.stun_interval_secs.unwrap_or(300), edge.firewall_config.clone())
} }
None => return Err(format!("unknown edge {}", edge_id).into()), None => return Err(format!("unknown edge {}", edge_id).into()),
} }
@@ -1252,6 +1262,7 @@ async fn handle_edge_connection_quic(
listen_ports: listen_ports.clone(), listen_ports: listen_ports.clone(),
listen_ports_udp: listen_ports_udp.clone(), listen_ports_udp: listen_ports_udp.clone(),
stun_interval_secs, stun_interval_secs,
firewall_config,
}; };
let mut handshake_json = serde_json::to_string(&handshake)?; let mut handshake_json = serde_json::to_string(&handshake)?;
handshake_json.push('\n'); handshake_json.push('\n');
@@ -1787,6 +1798,7 @@ mod tests {
listen_ports: vec![443, 8080], listen_ports: vec![443, 8080],
listen_ports_udp: vec![], listen_ports_udp: vec![],
stun_interval_secs: 300, stun_interval_secs: 300,
firewall_config: None,
}; };
let json = serde_json::to_value(&resp).unwrap(); let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["listenPorts"], serde_json::json!([443, 8080])); assert_eq!(json["listenPorts"], serde_json::json!([443, 8080]));
@@ -1801,6 +1813,7 @@ mod tests {
let update = EdgeConfigUpdate { let update = EdgeConfigUpdate {
listen_ports: vec![80, 443], listen_ports: vec![80, 443],
listen_ports_udp: vec![53], listen_ports_udp: vec![53],
firewall_config: None,
}; };
let json = serde_json::to_value(&update).unwrap(); let json = serde_json::to_value(&update).unwrap();
assert_eq!(json["listenPorts"], serde_json::json!([80, 443])); assert_eq!(json["listenPorts"], serde_json::json!([80, 443]));

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/remoteingress', name: '@serve.zone/remoteingress',
version: '4.14.2', version: '4.15.0',
description: 'Edge ingress tunnel for DcRouter - tunnels TCP and UDP traffic from the network edge to SmartProxy over TLS or QUIC, preserving client IP via PROXY protocol.' description: 'Edge ingress tunnel for DcRouter - tunnels TCP and UDP traffic from the network edge to SmartProxy over TLS or QUIC, preserving client IP via PROXY protocol.'
} }

View File

@@ -1,6 +1,7 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { decodeConnectionToken } from './classes.token.js'; import { decodeConnectionToken } from './classes.token.js';
import type { IFirewallConfig } from './classes.remoteingresshub.js';
// Command map for the edge side of remoteingress-bin // Command map for the edge side of remoteingress-bin
type TEdgeCommands = { type TEdgeCommands = {
@@ -55,6 +56,7 @@ export class RemoteIngressEdge extends EventEmitter {
private restartBackoffMs = 1000; private restartBackoffMs = 1000;
private restartAttempts = 0; private restartAttempts = 0;
private statusInterval: ReturnType<typeof setInterval> | undefined; private statusInterval: ReturnType<typeof setInterval> | undefined;
private nft: InstanceType<typeof plugins.smartnftables.SmartNftables> | null = null;
constructor() { constructor() {
super(); super();
@@ -110,6 +112,83 @@ export class RemoteIngressEdge extends EventEmitter {
console.log(`[RemoteIngressEdge] Ports updated by hub: ${data.listenPorts.join(', ')}`); console.log(`[RemoteIngressEdge] Ports updated by hub: ${data.listenPorts.join(', ')}`);
this.emit('portsUpdated', data); this.emit('portsUpdated', data);
}); });
this.bridge.on('management:firewallConfigUpdated', (data: { firewallConfig: IFirewallConfig }) => {
console.log(`[RemoteIngressEdge] Firewall config updated from hub`);
this.applyFirewallConfig(data.firewallConfig);
this.emit('firewallConfigUpdated', data);
});
}
/**
* Initialize the nftables manager. Fails gracefully if not running as root.
*/
private async initNft(): Promise<void> {
try {
this.nft = new plugins.smartnftables.SmartNftables({
tableName: 'remoteingress',
dryRun: false,
});
await this.nft.initialize();
console.log('[RemoteIngressEdge] SmartNftables initialized');
} catch (err) {
console.warn(`[RemoteIngressEdge] Failed to initialize nftables (not root?): ${err}`);
this.nft = null;
}
}
/**
* Apply firewall configuration received from the hub.
* Performs a full replacement: cleans up existing rules, then applies the new config.
*/
private async applyFirewallConfig(config: IFirewallConfig): Promise<void> {
if (!this.nft) {
return;
}
try {
// Full cleanup and reinitialize to replace all rules atomically
await this.nft.cleanup();
await this.nft.initialize();
// Apply blocked IPs
if (config.blockedIps && config.blockedIps.length > 0) {
for (const ip of config.blockedIps) {
await this.nft.firewall.blockIP(ip);
}
console.log(`[RemoteIngressEdge] Blocked ${config.blockedIps.length} IPs`);
}
// Apply rate limits
if (config.rateLimits && config.rateLimits.length > 0) {
for (const rl of config.rateLimits) {
await this.nft.rateLimit.addRateLimit(rl.id, {
port: rl.port,
protocol: rl.protocol,
rate: rl.rate,
burst: rl.burst,
perSourceIP: rl.perSourceIP,
});
}
console.log(`[RemoteIngressEdge] Applied ${config.rateLimits.length} rate limits`);
}
// Apply firewall rules
if (config.rules && config.rules.length > 0) {
for (const rule of config.rules) {
await this.nft.firewall.addRule(rule.id, {
direction: rule.direction,
action: rule.action,
sourceIP: rule.sourceIP,
destPort: rule.destPort,
protocol: rule.protocol,
comment: rule.comment,
});
}
console.log(`[RemoteIngressEdge] Applied ${config.rules.length} firewall rules`);
}
} catch (err) {
console.error(`[RemoteIngressEdge] Failed to apply firewall config: ${err}`);
}
} }
/** /**
@@ -156,6 +235,9 @@ export class RemoteIngressEdge extends EventEmitter {
this.restartAttempts = 0; this.restartAttempts = 0;
this.restartBackoffMs = 1000; this.restartBackoffMs = 1000;
// Initialize nftables (graceful degradation if not root)
await this.initNft();
// Start periodic status logging // Start periodic status logging
this.statusInterval = setInterval(async () => { this.statusInterval = setInterval(async () => {
try { try {
@@ -180,6 +262,15 @@ export class RemoteIngressEdge extends EventEmitter {
clearInterval(this.statusInterval); clearInterval(this.statusInterval);
this.statusInterval = undefined; this.statusInterval = undefined;
} }
// Clean up nftables rules before stopping
if (this.nft) {
try {
await this.nft.cleanup();
} catch (err) {
console.warn(`[RemoteIngressEdge] nftables cleanup error: ${err}`);
}
this.nft = null;
}
if (this.started) { if (this.started) {
try { try {
await this.bridge.sendCommand('stopEdge', {} as Record<string, never>); await this.bridge.sendCommand('stopEdge', {} as Record<string, never>);
@@ -261,6 +352,9 @@ export class RemoteIngressEdge extends EventEmitter {
this.restartAttempts = 0; this.restartAttempts = 0;
this.restartBackoffMs = 1000; this.restartBackoffMs = 1000;
// Re-initialize nftables (hub will re-push config via handshake)
await this.initNft();
// Restart periodic status logging // Restart periodic status logging
this.statusInterval = setInterval(async () => { this.statusInterval = setInterval(async () => {
try { try {

View File

@@ -22,7 +22,7 @@ type THubCommands = {
}; };
updateAllowedEdges: { updateAllowedEdges: {
params: { params: {
edges: Array<{ id: string; secret: string; listenPorts?: number[]; listenPortsUdp?: number[]; stunIntervalSecs?: number }>; edges: Array<{ id: string; secret: string; listenPorts?: number[]; listenPortsUdp?: number[]; stunIntervalSecs?: number; firewallConfig?: IFirewallConfig }>;
}; };
result: { updated: boolean }; result: { updated: boolean };
}; };
@@ -41,6 +41,31 @@ type THubCommands = {
}; };
}; };
export interface IFirewallRateLimit {
id: string;
port: number;
protocol?: 'tcp' | 'udp';
rate: string;
burst?: number;
perSourceIP?: boolean;
}
export interface IFirewallRule {
id: string;
direction: 'input' | 'output' | 'forward';
action: 'accept' | 'drop' | 'reject';
sourceIP?: string;
destPort?: number;
protocol?: 'tcp' | 'udp';
comment?: string;
}
export interface IFirewallConfig {
blockedIps?: string[];
rateLimits?: IFirewallRateLimit[];
rules?: IFirewallRule[];
}
export interface IHubConfig { export interface IHubConfig {
tunnelPort?: number; tunnelPort?: number;
targetHost?: string; targetHost?: string;
@@ -50,7 +75,7 @@ export interface IHubConfig {
}; };
} }
type TAllowedEdge = { id: string; secret: string; listenPorts?: number[]; listenPortsUdp?: number[]; stunIntervalSecs?: number }; type TAllowedEdge = { id: string; secret: string; listenPorts?: number[]; listenPortsUdp?: number[]; stunIntervalSecs?: number; firewallConfig?: IFirewallConfig };
const MAX_RESTART_ATTEMPTS = 10; const MAX_RESTART_ATTEMPTS = 10;
const MAX_RESTART_BACKOFF_MS = 30_000; const MAX_RESTART_BACKOFF_MS = 30_000;

View File

@@ -3,5 +3,6 @@ import * as path from 'path';
export { path }; export { path };
// @push.rocks scope // @push.rocks scope
import * as smartnftables from '@push.rocks/smartnftables';
import * as smartrust from '@push.rocks/smartrust'; import * as smartrust from '@push.rocks/smartrust';
export { smartrust }; export { smartnftables, smartrust };