# @serve.zone/remoteingress

`@serve.zone/remoteingress` is a Rust-powered, TypeScript-controlled edge tunnel for moving TCP and UDP traffic from public edge nodes into a private dcrouter or SmartProxy host while preserving the original client IP through PROXY protocol. It also supports controlled outbound TCP egress through QUIC-connected edges for use cases such as SMTP delivery from the edge IP.

## Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.

## What It Does

Remote ingress solves the common edge problem: your workload or gateway lives behind NAT, a firewall, or a private network, but public traffic should enter through one or more hardened edge VPS nodes.

```
Internet clients
      |
      v
public edge node: RemoteIngressEdge
      |
      | TLS or QUIC tunnel
      v
private site: RemoteIngressHub -> dcrouter / SmartProxy
```

The edge binds public TCP and UDP ports assigned by the hub. Each accepted connection or datagram is tunneled to the hub, which forwards it to a local target host with PROXY headers so downstream routing can still see the real client address.

For outbound egress, the hub creates a one-shot localhost TCP proxy. The selected edge resolves the logical target, enforces the configured egress policy on the resolved IP address, dials the destination from the edge host, and pipes bytes over the tunnel. Outbound egress does not use PROXY protocol.

## Highlights

- ⚡ Rust networking core managed from TypeScript through `@push.rocks/smartrust`
- 🔁 Hub/edge model with dynamic `updateAllowedEdges()` reconciliation
- 🌐 TCP forwarding over frame-multiplexed TLS or native QUIC streams
- 📡 UDP forwarding over TCP frames or QUIC datagrams
- 🧾 PROXY protocol preservation for client IP visibility at SmartProxy
- 🛡️ Hub-pushed nftables firewall snapshots for blocklists, rate limits, and custom rules
- 🔐 Shared-secret edge authentication and compact connection tokens
- 🚦 Transport modes: `tcpTls`, `quic`, and `quicWithFallback`
- 📤 One-shot outbound TCP egress proxy over QUIC-connected edges
- 📊 EventEmitter status events for edge lifecycle, streams, port assignments, and crash recovery

## Install

Use the package as a TypeScript library:

```bash
pnpm add @serve.zone/remoteingress
```

Install the CLI on a Linux edge or hub host with the released self-extracting binary:

```bash
curl -sSL https://code.foss.global/serve.zone/remoteingress/raw/branch/main/install.sh | sudo bash
```

The installer downloads `remoteingress-linux-x64` or `remoteingress-linux-arm64` from the latest Gitea release, installs it under `/opt/remoteingress`, and links `/usr/local/bin/remoteingress`. Use `--version vX.Y.Z` to pin a release, `--install-dir /path` to change the target directory, or `--source` to clone the tag and build the NodeNext package locally.

```bash
curl -sSL https://code.foss.global/serve.zone/remoteingress/raw/branch/main/install.sh | sudo bash -s -- --source
```

## Hub Side

Run the hub next to the private service you want to expose. In dcrouter deployments the target is usually SmartProxy on `127.0.0.1`.

```typescript
import { RemoteIngressHub } from '@serve.zone/remoteingress';

const hub = new RemoteIngressHub();

hub.on('edgeConnected', ({ edgeId }) => console.log('edge connected', edgeId));
hub.on('edgeDisconnected', ({ edgeId }) => console.log('edge disconnected', edgeId));
hub.on('streamSummary', ({ edgeId, activeStreams }) => console.log('streams', edgeId, activeStreams));
hub.on('egressConnection', (event) => console.log('egress', event));
hub.on('egressIdentityChanged', ({ edgeId, egressIdentity }) => {
  console.log('egress identity', edgeId, egressIdentity);
});

await hub.start({
  tunnelPort: 8443,
  targetHost: '127.0.0.1',
  streamEventMode: 'summary',
});

await hub.updateAllowedEdges([
  {
    id: 'edge-fra-01',
    secret: 'replace-with-a-long-random-secret',
    listenPorts: [80, 443],
    listenPortsUdp: [443],
    stunIntervalSecs: 300,
    egress: {
      enabled: true,
      allowedPorts: [25],
      allowedHostPatterns: ['*.example.net'],
      allowPrivateRanges: false,
      deniedCidrs: ['10.0.0.0/8', '192.168.0.0/16'],
      maxConcurrentStreams: 50,
    },
    firewallConfig: {
      blockedIps: ['198.51.100.25'],
      rateLimits: [
        { id: 'https-per-ip', port: 443, protocol: 'tcp', rate: '200/second', burst: 100, perSourceIP: true },
      ],
    },
  },
]);

const status = await hub.getStatus();
console.log(status.connectedEdges);

const proxy = await hub.startEgressTcpProxy({
  edgeId: 'edge-fra-01',
  logicalHost: 'mx.example.net',
  port: 25,
  serverFirst: true,
  connectTimeoutMs: 10_000,
});

// Connect your SMTP client to proxy.listenHost:proxy.listenPort.
// The proxy accepts one local connection and then removes itself.
console.log(proxy);

await hub.stop();
```

## Edge Side

The edge runs on the public node. It normally needs root privileges to bind privileged ports and apply nftables rules. If nftables cannot be initialized, the tunnel can still run, but kernel-level edge firewalling is skipped.

```typescript
import { RemoteIngressEdge } from '@serve.zone/remoteingress';

const edge = new RemoteIngressEdge();

edge.on('tunnelConnected', () => console.log('tunnel connected'));
edge.on('portsAssigned', ({ listenPorts }) => console.log('TCP ports', listenPorts));
edge.on('firewallConfigUpdated', () => console.log('firewall snapshot applied'));

await edge.start({
  hubHost: 'ingress-hub.example.com',
  hubPort: 8443,
  edgeId: 'edge-fra-01',
  secret: 'replace-with-a-long-random-secret',
  transportMode: 'quicWithFallback',
  egressSourceIpv4: '203.0.113.10',
  egressSourceIpv6: '2001:db8::10',
});

console.log(await edge.getStatus());

await edge.stop();
```

## Outbound TCP Egress Proxy

Outbound egress is disabled by default. Enable it per edge with `TAllowedEdge.egress`, then request one-shot proxies from the hub with `startEgressTcpProxy()`.

The proxy always binds `127.0.0.1` on the hub and accepts a single local connection. It then opens an egress stream to the selected QUIC-connected edge. The edge resolves `logicalHost`, checks the requested port, filters resolved IPs, dials the destination, returns the chosen IP for audit/status, and pipes bytes.

Policy rules:

- `allowedPorts` defaults to `[25]` when omitted.
- `allowedHostPatterns` may contain exact names or `*.example.com` patterns.
- `deniedCidrs` always wins after DNS resolution.
- `allowPrivateRanges` only allows RFC1918/ULA private ranges; loopback, link-local, metadata, multicast, unspecified, documentation, shared-address, and reserved ranges remain denied.
- Egress currently requires the edge to be connected over QUIC. If `quicWithFallback` falls back to `tcpTls`, egress proxy creation fails instead of falling back to direct hub egress.
- IPv4 and IPv6 destinations require the matching `egressSourceIpv4` or `egressSourceIpv6` edge setting. Each address must be a concrete unicast address assigned to the edge host. The edge binds the outbound socket to that address and fails closed when the matching family is missing or cannot be bound; it never falls back to an OS-selected source address.

Use `serverFirst: true` for SMTP and other protocols where the remote server sends the first bytes after connect.

### Egress identity status

Edges with configured, locally bindable egress source addresses report the `egressIdentityV1` capability. `RemoteIngressEdge.getStatus()` exposes the latest `IEgressIdentityReport`, and each connected entry from `RemoteIngressHub.getStatus()` exposes `IEgressIdentityStatus`.

The proof method is `localSocketBind`: it proves that the edge successfully bound a local socket to the configured address, not that an external service observed that address after routing or NAT. Edge observations include `observedAtUnixMs`; hub status adds `hubReceivedAtUnixMs` and marks an address `stale` when no fresh observation has arrived for more than 45 seconds. The value is `null` for legacy edges and for edges without a currently bindable configured source address.

The hub emits `egressIdentityChanged` with `{ edgeId, egressIdentity }` when the reported address or proof changes. Timestamp-only refreshes and stale-state transitions do not emit the event. Older edges remain wire-compatible through legacy empty heartbeat PONG payloads, but they do not advertise `egressIdentityV1` or provide identity status.

## Connection Tokens

Tokens are base64url-encoded compact JSON. They are useful when the hub operator provisions an edge and wants to hand over one opaque string.

```typescript
import {
  RemoteIngressEdge,
  encodeConnectionToken,
  decodeConnectionToken,
} from '@serve.zone/remoteingress';

const token = encodeConnectionToken({
  hubHost: 'ingress-hub.example.com',
  hubPort: 8443,
  edgeId: 'edge-fra-01',
  secret: 'replace-with-a-long-random-secret',
});

console.log(decodeConnectionToken(token));

const edge = new RemoteIngressEdge();
await edge.start({
  token,
  egressSourceIpv4: '203.0.113.10',
  egressSourceIpv6: '2001:db8::10',
});
```

`RemoteIngressEdge.start()` accepts either `IEdgeConfig` with explicit hub credentials or `IEdgeTokenConfig` with a token. Both forms accept `bindAddress`, `transportMode`, `egressSourceIpv4`, and `egressSourceIpv6`.

## CLI Mode

The package entry point exports `runCli()`, and the `remoteingress` CLI can run hub or edge mode.

```bash
remoteingress hub --tunnel-port 8443 --target-host 127.0.0.1 --stream-event-mode summary
remoteingress edge --token eyJoIjoiaW5ncmVzcy1odWIuZXhhbXBsZS5jb20i... \
  --egress-source-ipv4 203.0.113.10 --egress-source-ipv6 2001:db8::10
```

Environment-based startup is also supported:

| Variable | Purpose |
| --- | --- |
| `REMOTEINGRESS_MODE` | `hub` or `edge` |
| `REMOTEINGRESS_TOKEN` | Edge connection token |
| `REMOTEINGRESS_HUB_HOST` / `REMOTEINGRESS_HUB_PORT` | Explicit edge connection target |
| `REMOTEINGRESS_EDGE_ID` / `REMOTEINGRESS_SECRET` | Explicit edge credentials |
| `REMOTEINGRESS_EGRESS_SOURCE_IPV4` | Concrete locally assigned IPv4 source for outbound egress |
| `REMOTEINGRESS_EGRESS_SOURCE_IPV6` | Concrete locally assigned IPv6 source for outbound egress |
| `REMOTEINGRESS_TARGET_HOST` | Hub-side forwarding target, default `127.0.0.1` |
| `REMOTEINGRESS_ALLOWED_EDGES_JSON` | Hub-side allowed edge list; entries can include `egress` policy objects |
| `REMOTEINGRESS_PERFORMANCE_JSON` | Optional performance configuration |
| `REMOTEINGRESS_STREAM_EVENT_MODE` | Hub stream event mode: `perStream`, `summary`, or `off` |

## Docker Image

Release builds publish a multi-arch OCI image at `code.foss.global/serve.zone/remoteingress:latest` for `linux/amd64` and `linux/arm64`.

Hub example:

```bash
docker run --rm --name remoteingress-hub \
  --network host \
  -e REMOTEINGRESS_MODE=hub \
  -e REMOTEINGRESS_TARGET_HOST=127.0.0.1 \
  -e REMOTEINGRESS_ALLOWED_EDGES_JSON='[{"id":"edge-fra-01","secret":"replace-me","listenPorts":[80,443],"egress":{"enabled":true,"allowedPorts":[25]}}]' \
  code.foss.global/serve.zone/remoteingress:latest
```

Edge example:

```bash
docker run --rm --name remoteingress-edge \
  --network host \
  --cap-add NET_ADMIN \
  -e REMOTEINGRESS_MODE=edge \
  -e REMOTEINGRESS_TOKEN='<connection-token>' \
  -e REMOTEINGRESS_EGRESS_SOURCE_IPV4='203.0.113.10' \
  -e REMOTEINGRESS_EGRESS_SOURCE_IPV6='2001:db8::10' \
  code.foss.global/serve.zone/remoteingress:latest
```

Use host networking when the container must bind public ports directly or reach a localhost target on the host. `NET_ADMIN` is needed only when the edge should apply nftables firewall snapshots.

## Transport Modes

| Mode | Behavior |
| --- | --- |
| `tcpTls` | Single TLS connection with frame-based stream multiplexing. Good for conservative networks. |
| `quic` | QUIC streams for TCP and QUIC datagrams for UDP. Best latency and no TCP head-of-line blocking. |
| `quicWithFallback` | Default edge mode. Tries QUIC and falls back to TCP/TLS when UDP is blocked. |

Outbound egress proxies require a QUIC-connected edge because the hub opens a dedicated QUIC bidirectional stream to the edge. A `quicWithFallback` edge can use egress when QUIC succeeds; if it falls back to `tcpTls`, egress proxy creation fails closed.

## Stream Event Modes

The hub can emit stream telemetry in three modes through `IHubConfig.streamEventMode` or `REMOTEINGRESS_STREAM_EVENT_MODE`.

| Mode | Behavior |
| --- | --- |
| `perStream` | Compatibility/debug mode. Emits `streamOpened` and `streamClosed` for each stream. This is the library default. |
| `summary` | Production dashboard mode. Suppresses per-stream events and emits `streamSummary` once per second per connected edge. |
| `off` | Suppresses stream telemetry events. Consumers should poll `getStatus()` when they need counts. |

`streamSummary` carries `{ edgeId, activeStreams, streamsOpenedTotal, streamsClosedTotal }`. The hub status returned by `getStatus()` remains the authoritative source for current stream and traffic counters.

## Firewall Config

`firewallConfig` travels in the same hub-to-edge configuration update as port assignments. Each update is a full desired-state snapshot.

```typescript
await hub.updateAllowedEdges([
  {
    id: 'edge-fra-01',
    secret: 'secret',
    listenPorts: [80, 443],
    firewallConfig: {
      blockedIps: ['203.0.113.0/24'],
      rateLimits: [
        { id: 'http', port: 80, protocol: 'tcp', rate: '100/second', perSourceIP: true },
      ],
      rules: [
        { id: 'allow-monitoring', direction: 'input', action: 'accept', sourceIP: '10.0.0.0/8', destPort: 9090, protocol: 'tcp' },
      ],
    },
  },
]);
```

## API Surface

| Export | Purpose |
| --- | --- |
| `RemoteIngressHub` | Starts/stops the private hub, authorizes edges, pushes runtime config, and reports connected edges. |
| `RemoteIngressEdge` | Starts/stops the public edge, connects to the hub, binds assigned ports, and applies firewall rules. |
| `encodeConnectionToken()` | Encodes hub host, port, edge ID, and secret into a token. |
| `decodeConnectionToken()` | Decodes and validates a token. |
| `IHubConfig`, `IEdgeConfig`, `IEdgeTokenConfig`, `TAllowedEdge`, `TStreamEventMode` | Primary TypeScript shapes for integrating the module. |
| `IEgressPolicy`, `IEgressTcpProxyRequest`, `IEgressTcpProxyInfo`, `TAddressFamilyPreference` | Outbound TCP egress policy and proxy request/response types. |
| `IEgressIdentityReport`, `IEgressIdentityStatus`, `IEgressSourceBindingObservation`, `IEgressSourceBindingStatus`, `TEgressIdentityProofMethod` | Edge-reported and hub-received source-binding identity shapes. |
| `IRemoteIngressHubEgressIdentityChangedEvent` | Typed payload for the public `egressIdentityChanged` hub event. |

`RemoteIngressHub` additionally exposes `startEgressTcpProxy()` and `stopEgressTcpProxy()`. Connected edge status entries include `capabilities`, `egressEnabled`, and `egressIdentity`. The hub emits `egressConnection` events with `{ proxyId, edgeId, logicalHost, port, outcome, resolvedIp, error }` and `egressIdentityChanged` events with `{ edgeId, egressIdentity }`.

## Development

```bash
pnpm run build
pnpm test
```

Useful source entry points:

- `ts/index.ts` exports the public API and CLI runner.
- `ts/classes.remoteingresshub.ts` wraps hub management commands and hub events.
- `ts/classes.remoteingressedge.ts` wraps edge management commands, nftables application, and edge events.
- `ts/classes.token.ts` implements compact connection tokens.
- `rust/` contains the performance-critical tunnel implementation compiled by `tsrust`.

## 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.

**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.

### Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

### Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
