Space-to-Space Port Forwarding
Space-to-space port forwarding allows spaces to communicate directly with each other. This is useful for microservices architectures where one space needs to access a service running in another space (for example, a frontend space connecting to a backend API space).
Requirements
For space-to-space port forwarding to work:
- The source space must be running and have an active agent (or use
--persistentto pre-configure a forward for when it starts) - Spaces must be in the same zone
- The target must be a space or pool you own (any port), or another user’s space or pool on a port their template declares shared
- Local port must be in the range 1-65535
Commands
The following commands are run from inside a space using the knot agent:
Forward Port
Forward a port from the current space to another space:
knot port forward <local-port> <space-name> <remote-port> [--persistent] [--force]Example:
knot port forward 8080 backend-api 3000This forwards port 8080 in the current space to port 3000 in the backend-api space.
--persistent(-p): Persist the forward across agent restarts.--force(-f): Create the forward even if the target space is not currently running.
List Active Forwards
View all active port forwards from the current space:
knot port listExample output:
Active port forwards:
8080 -> backend-api:3000 (persistent, direct)
5432 -> database:5432 (temporary, relay)
6379 -> cache:6379 (temporary, —)The mode shown after the state indicates how traffic is routed:
- direct Pro : traffic flows directly between agents without going through the server
- relay: traffic is relayed through the knot server
- —: the forward hasn’t carried a connection yet, so its mode isn’t known
Direct mode requires Knot Pro and both spaces running on the same host or network. When direct isn’t available or fails, the forward automatically falls back to relay.
Stop Port Forward
Stop an active port forward:
knot port stop <local-port>Example:
knot port stop 8080Targets: Spaces, Pools, and Other Users
A forward target is resolved by name, in this order:
- A space you own with that name: any port.
- A pool you own with that name: any port. Traffic is spread across the pool’s healthy, unleased members, round-robin per connection. Exclusively leased members are skipped; lease holders reach their own member by its space name.
- Another user’s space or pool, written as
user--space: only ports the target’s template declares shared.
A shared port is a template port declaration with type Shared instead of HTTP, HTTPS or TCP. Unlike those it is not published anywhere: no dev URL, no host port, and it does not appear in the space list port menus. It exists purely as a cross-user forward target. Every space (and pool) created from that template accepts forwards on that port from any user in the same zone, and access is re-checked on every connection, so changing a port’s type takes effect on running spaces immediately. All other ports remain forwardable by the owner only, exactly like ports that aren’t declared at all.
# Forward to a space you own
knot space port forward frontend 8080 backend-api 3000
# Forward to your pool; connections spread across members
knot space port forward frontend 5432 db-pool 5432
# Forward to another user's space (port must be shared on its template)
knot space port forward frontend 5432 paul--shared-pg 5432
# Another user's pool, same rule
knot space port forward frontend 6379 paul--cache-pool 6379“Shared” means every user in the zone can connect to that port, not the whole internet. If the service behind the port has no authentication of its own, any user in the zone can talk to it.
A forward to a stopped target fails until the target starts again; nothing is auto-started. Pools keep capacity up for you: dead members are replaced and traffic moves to live ones on the next connection.
Wiring Forwards into Templates Pro
A template can carry a list of port forwards that are seeded into every space created from it, so client spaces connect to their services automatically when they start, with no per-space setup:
- Each entry is a local port, a target reference (space or pool name, or
user--spacefor another user’s shared port) and a remote port. - The Pro template form has a Port Forwards editor for this; the API (
port_forwards), template export/import andknot.template.create/updateaccept it everywhere. - A pool target is the sturdiest wiring: the pool replaces dead members and keeps capacity, and each connection picks a live one.
- The space owns its copy from creation; later template edits don’t propagate to existing spaces, and users can manage the forwards like any others.
Port Forward Throttling
Apply latency, jitter, and bandwidth limits to simulate real-world network conditions on any port forward.
From Inside a Space
knot port throttle <local-port> [--latency Nms] [--jitter Nms] [--bandwidth NKB/s] [--reset]Examples:
# Add 200ms latency with 50ms jitter
knot port throttle 3306 --latency 200ms --jitter 50ms
# Limit to 100 KB/s
knot port throttle 3306 --bandwidth 100
# Kill connections after 5 seconds
knot port throttle 3306 --timeout 5000ms
# Simulate slow, unstable connection
knot port throttle 3306 --latency 100ms --jitter 200ms --bandwidth 50
# Clear all limits
knot port throttle 3306 --resetFrom Desktop or Web UI (Pro)
knot space port throttle <space> <local-port> --latency 200ms --jitter 50msOr use the web UI Pro : open the space’s port forwards panel, click edit on a forward, and set latency, jitter, and bandwidth values. Empty fields mean no limit.
How It Works
- Latency: each direction gets the specified delay (e.g. 50ms each way = 100ms round trip)
- Jitter: random variance added to or subtracted from the latency, uniform distribution
- Bandwidth: proportional throttling per write, limiting throughput in both directions independently
- Timeout: kills each connection after the specified number of milliseconds, simulating a connection drop
- Settings are runtime-only: they do not persist across agent restarts and are not stored in the space record
- Throttling applies to both relay and direct connections equally
- Changes take effect immediately on existing connections (no need to recreate the forward)
knot port list shows active throttle settings:
Active port forwards:
3306 -> db:3306 (persistent, direct, 200ms ±50ms 100KB/s)
9000 -> payments:9000 (persistent, relay, timeout=5000ms)
6379 -> cache:6379 (persistent, relay)Client Commands
The following commands are run from your desktop machine using the knot CLI:
Forward Port
Forward a port from one space to another space:
knot space port forward <from-space> <from-port> <to-space> <to-port> [--persistent] [--force]Example:
knot space port forward frontend 8080 backend-api 3000This forwards port 8080 in the frontend space to port 3000 in the backend-api space.
--persistent(-p): Persist the forward across agent restarts. Also allows creating a forward when the source space is not currently running.--force(-f): Create the forward even if the target space is not currently running.
List Active Forwards
View all active port forwards from a specific space:
knot space port list <space-name>Example:
knot space port list frontendStop Port Forward
Stop an active port forward in a specific space:
knot space port stop <space-name> <local-port>Example:
knot space port stop frontend 8080The client commands require you to be authenticated with the knot server. Use --server and --token flags or configure them in your knot config file.
Use Cases
- Microservices Communication: Connect frontend containers to backend services
- Database Access: Allow application containers to access database containers
- Service Discovery: Enable development environments that mirror production architectures
Apply Port Forwards
Replace all port forwards for a space with a new set. This is useful for declarative configuration where you want to ensure the forwarding state matches a desired list.
From a Space (Scripting)
import knot.space as space
# Apply a set of port forwards, replacing any existing ones
result = space.port_apply("frontend", [
{"local_port": 8080, "space": "backend-api", "remote_port": 3000},
{"local_port": 5432, "space": "database", "remote_port": 5432},
])
print(f"Applied: {result['applied']}")
print(f"Stopped: {result['stopped']}")Via API
curl -X POST https://knot.example.com/space-io/{space_id}/port/apply \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"forwards": [
{"local_port": 8080, "space": "backend-api", "remote_port": 3000},
{"local_port": 5432, "space": "database", "remote_port": 5432}
]
}'Response:
{
"applied": [
{"local_port": 8080, "space": "backend-api", "remote_port": 3000, "persistent": false}
],
"stopped": [
{"local_port": 9090, "space": "old-service", "remote_port": 8080, "persistent": false}
],
"errors": []
}Each forward entry supports optional persistent and force fields (same as individual port forward).
Space-to-space port forwarding only works between spaces in the same zone. Same-user targets and shared ports are authenticated and authorized on every connection.
Direct Agent-to-Agent Connections Pro
In Knot Pro, port-forwarded traffic between spaces on the same host or network can flow directly between agents without relaying through the server. This reduces latency and server load.
How It Works
- When a port forward is created, the server resolves the target space’s direct address and sends it to the source agent.
- The source agent dials the target agent directly over TCP, authenticating with a user-scoped shared secret (HMAC challenge-response).
- A single yamux session per peer multiplexes all forwarded connections — one TCP connection carries multiple streams.
- If the direct connection fails (target restarted, network issue), traffic automatically falls back to server relay.
- The mode (
directorrelay, or—before the forward’s first connection) is shown inknot port listand the UI.
Environment Variables
The server injects these automatically when launching containers. You do not normally need to set them manually.
| Variable | Default | Purpose |
|---|---|---|
KNOT_PEER_PORT |
12202 |
Port the agent listens on inside the container for direct peer connections. Set in the template by the user; the server reads it to know which container port to map. Set to 0 to disable. |
KNOT_PEER_EXTERNAL_PORT |
(set by server) | The published host port that peers dial. Injected by the server at container launch. Reported to the server at registration. 0 = no direct connections. |
Enabling Direct Connections
Direct connections are enabled by default in Knot Pro for Docker, Podman, and Apple Containers. No configuration is needed — the server allocates a host port, injects the environment variables, and coordinates introductions automatically.
Disabling Direct Connections
To disable direct peer connections for a specific space, set KNOT_PEER_PORT=0 in the template’s environment section:
environment:
- "KNOT_PEER_PORT=0"This tells the agent not to listen for direct connections. All port-forwarded traffic for that space will use the server relay.
To disable peer mesh globally, add to your server configuration:
[server.peermesh]
enabled = falseNomad Requirements
For Nomad-launched spaces, the server injects a dynamic port and service registration, but the template must set mode = "bridge" in the network block for Nomad to create the host→container port mapping:
network {
mode = "bridge"
port "redis_port" { to = 6379 }
}Without bridge mode, ports are allocated but not reachable from outside the container. Traffic falls back to relay automatically.
Configuration
The peer mesh port range and behaviour can be tuned in the server configuration:
[server.peermesh]
enabled = true # default: true in Pro
port_range_min = 30001 # default: 30001
port_range_max = 32767 # default: 32767Security
- Direct connections are authenticated with a user-scoped shared secret — only spaces owned by the same user can connect directly. Shared spaces (owned by a different user) use relay.
- The secret is derived from the zone encryption key and never sent in cleartext — an HMAC challenge-response proves identity per connection.
- Note: data over direct connections flows as cleartext TCP (the HMAC proves identity but does not encrypt). For internet-exposed deployments, keep relay mode (which uses TLS via WebSocket).