Skip to content

Commit 5abc36c

Browse files
authored
feat(relay): route forwarding through ForwardTcp (#1029)
1 parent 3f0a058 commit 5abc36c

36 files changed

Lines changed: 1898 additions & 1038 deletions

.agents/skills/debug-openshell-cluster/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ helm -n openshell get values openshell | grep -E 'repository|tag|supervisorImage
128128

129129
The gateway image and `server.supervisorImage` should use the same build tag in branch and E2E deploys. A stale supervisor image can make sandbox behavior lag behind gateway policy or proto changes.
130130

131+
For local/external pull mode (the default local path via `mise run cluster`), local images are tagged to the configured local registry base, pushed to that registry, and pulled by k3s via the `registries.yaml` mirror endpoint. The `cluster` task pushes prebuilt local tags (`openshell/*:dev`, falling back to `localhost:5000/openshell/*:dev` or `127.0.0.1:5000/openshell/*:dev`).
132+
133+
Gateway image builds stage a partial Rust workspace from `deploy/docker/Dockerfile.images`. If cargo fails with a missing manifest under `/build/crates/...`, or an imported symbol exists locally but is missing in the image build, verify that every current gateway dependency crate, including `openshell-driver-docker`, `openshell-driver-kubernetes`, and `openshell-ocsf`, is copied into the staged workspace there.
134+
131135
For plaintext local evaluation, confirm the chart has:
132136

133137
```bash

architecture/gateway.md

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ workloads.
99

1010
- Authenticate clients and sandbox callbacks.
1111
- Serve gRPC APIs for sandbox lifecycle, provider management, policy updates,
12-
settings, inference configuration, logs, and watch streams.
13-
- Serve HTTP endpoints for health, SSH tunnel upgrades, and edge-auth flows.
12+
settings, inference configuration, logs, watch streams, and relay forwarding.
13+
- Serve HTTP endpoints for health, WebSocket tunnels, and edge-auth flows.
1414
- Persist domain objects in SQLite or Postgres.
1515
- Resolve provider credentials and inference bundles for sandbox supervisors.
16-
- Coordinate supervisor relay sessions for connect, exec, and file sync.
16+
- Coordinate supervisor relay sessions for connect, exec, file sync, and
17+
service forwarding.
1718

1819
The gateway does not enforce agent network policy at request time. That happens
1920
inside each sandbox, where the supervisor and proxy can observe local process
@@ -44,7 +45,7 @@ The gateway API is organized around platform objects and operational streams:
4445

4546
| Area | Examples |
4647
|---|---|
47-
| Sandbox lifecycle | Create, list, delete, watch, exec, SSH session bootstrap. |
48+
| Sandbox lifecycle | Create, list, delete, watch, exec, SSH session bootstrap, ForwardTcp service forwarding. |
4849
| Providers | Store provider records, discover credentials, resolve runtime environment. |
4950
| Policy and settings | Get effective sandbox config, update sandbox policy, manage global settings. |
5051
| Inference | Set gateway-level model/provider config and resolve sandbox route bundles. |
@@ -115,22 +116,35 @@ sequenceDiagram
115116
participant CLI
116117
participant GW as Gateway
117118
participant SUP as Sandbox supervisor
118-
participant SSH as Sandbox SSH socket
119+
participant Target as Sandbox target
119120
120121
SUP->>GW: ConnectSupervisor stream
121-
CLI->>GW: connect / exec / sync request
122-
GW->>SUP: RelayOpen(channel)
122+
CLI->>GW: ForwardTcp / exec / sync request
123+
GW->>SUP: RelayOpen(channel, target)
124+
SUP->>Target: Dial SSH socket or loopback service
123125
SUP->>GW: RelayStream(channel)
124-
SUP->>SSH: Bridge bytes to Unix socket
125126
CLI->>GW: Client bytes
126127
GW-->>CLI: Client bytes
127128
GW->>SUP: Relay bytes
128129
SUP-->>GW: Relay bytes
129130
```
130131

131-
The same relay pattern backs interactive SSH, command execution, and file sync.
132-
The gateway tracks live sessions in memory and persists session records so
133-
tokens can expire or be revoked.
132+
The same relay pattern backs interactive SSH, command execution, file sync, and
133+
local service forwarding. The gateway tracks live sessions in memory and
134+
persists session records so tokens can expire or be revoked.
135+
136+
`ForwardTcp` is the client-facing byte stream for SSH and service forwarding.
137+
The first frame is a `TcpForwardInit` that carries the sandbox ID, an
138+
authorization token from `CreateSshSession`, and an explicit target:
139+
`target.ssh` for the sandbox SSH socket or `target.tcp` for a loopback service
140+
inside the sandbox. The gateway validates the token and sandbox readiness,
141+
sends a targeted `RelayOpen` to the supervisor, then bridges
142+
`TcpForwardFrame::Data` to `RelayFrame::Data` until either side closes.
143+
144+
For `target.tcp`, the gateway only accepts loopback destinations such as
145+
`localhost`, `127.0.0.0/8`, or `::1`. The gateway never needs to know or dial a
146+
sandbox pod IP; supervisors connect outbound and bridge only the explicit target
147+
requested for that relay.
134148

135149
## PKI Bootstrap
136150

@@ -143,13 +157,13 @@ created. Both deployment paths use it:
143157
| Filesystem | `--output-dir <DIR>` | `<dir>/{ca.crt, ca.key, server/tls.{crt,key}, client/tls.{crt,key}}`. Also copies client materials to `$XDG_CONFIG_HOME/openshell/gateways/openshell/mtls/` for CLI auto-discovery. |
144158

145159
On Kubernetes, the Helm chart runs the command via a pre-install/pre-upgrade
146-
hook Job using the gateway image itself no separate cert-generation image,
160+
hook Job using the gateway image itself -- no separate cert-generation image,
147161
no extra mirror burden in air-gapped environments. On the RPM gateway, the
148162
same command runs from the systemd unit's `ExecStartPre` to bootstrap PKI
149163
into the user's state directory on first start.
150164

151-
Both modes share the same idempotency contract: all targets present skip;
152-
partial state fail with a recovery hint; nothing present generate and
165+
Both modes share the same idempotency contract: all targets present -> skip;
166+
partial state -> fail with a recovery hint; nothing present -> generate and
153167
write. This guards mTLS continuity across restarts and upgrades while still
154168
recovering cleanly if an operator deletes everything and starts over.
155169

crates/openshell-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ tokio-tungstenite = { workspace = true }
6868

6969
# Streams
7070
futures = { workspace = true }
71+
tokio-stream = { workspace = true }
7172
nix = { workspace = true }
7273

7374
# URL parsing

crates/openshell-cli/src/main.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use clap_complete::engine::ArgValueCompleter;
88
use clap_complete::env::CompleteEnv;
99
use miette::Result;
1010
use owo_colors::OwoColorize;
11+
use std::collections::HashMap;
1112
use std::io::Write;
1213
use std::path::PathBuf;
1314

@@ -266,6 +267,7 @@ const FORWARD_EXAMPLES: &str = "\x1b[1mALIAS\x1b[0m
266267
\x1b[1mEXAMPLES\x1b[0m
267268
$ openshell forward start 8080
268269
$ openshell forward start 3000 my-sandbox
270+
$ openshell forward service my-sandbox --target-port 8000 --local 8000
269271
$ openshell forward stop 8080
270272
$ openshell forward list
271273
";
@@ -1612,6 +1614,26 @@ enum ForwardCommands {
16121614
/// List active port forwards.
16131615
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
16141616
List,
1617+
1618+
/// Forward a local TCP port to a loopback service inside a sandbox over gRPC.
1619+
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
1620+
Service {
1621+
/// Sandbox name (defaults to last-used sandbox).
1622+
#[arg(add = ArgValueCompleter::new(completers::complete_sandbox_names))]
1623+
name: Option<String>,
1624+
1625+
/// Target service port inside the sandbox.
1626+
#[arg(long)]
1627+
target_port: u16,
1628+
1629+
/// Target service host inside the sandbox. Phase 1 accepts loopback only.
1630+
#[arg(long, default_value = "127.0.0.1")]
1631+
target_host: String,
1632+
1633+
/// Local bind address and port: `[bind_address:]port`. Defaults to the target port. Use port 0 for dynamic assignment.
1634+
#[arg(long)]
1635+
local: Option<String>,
1636+
},
16151637
}
16161638

16171639
#[tokio::main]
@@ -1854,6 +1876,27 @@ async fn main() -> Result<()> {
18541876
}
18551877
}
18561878
}
1879+
ForwardCommands::Service {
1880+
name,
1881+
target_port,
1882+
target_host,
1883+
local,
1884+
} => {
1885+
let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
1886+
let mut tls = tls.with_gateway_name(&ctx.name);
1887+
apply_auth(&mut tls, &ctx.name);
1888+
let name = resolve_sandbox_name(name, &ctx.name)?;
1889+
let local = local.unwrap_or_else(|| target_port.to_string());
1890+
run::service_forward_tcp(
1891+
&ctx.endpoint,
1892+
&name,
1893+
Some(&local),
1894+
&target_host,
1895+
target_port,
1896+
&tls,
1897+
)
1898+
.await?;
1899+
}
18571900
ForwardCommands::Start {
18581901
port,
18591902
name,
@@ -2237,7 +2280,7 @@ async fn main() -> Result<()> {
22372280
};
22382281

22392282
// Parse --label flags into a HashMap<String, String>.
2240-
let mut labels_map = std::collections::HashMap::new();
2283+
let mut labels_map = HashMap::new();
22412284
for label_str in &labels {
22422285
let parts: Vec<&str> = label_str.splitn(2, '=').collect();
22432286
if parts.len() != 2 {

0 commit comments

Comments
 (0)