krun_transport
krun_transport
¶
TCP-bridged OpenSSH transport for KrunRuntime.
Implements the KrunTransport
protocol by shelling out to the system ssh client and reaching the
guest's sshd over a host TCP port that podman's passt has forwarded into
the guest namespace. No custom wire protocol: sshd handles auth, PTY
allocation, signal forwarding, exit codes.
Why TCP-over-passt and not vsock: crun-krun does not configure
host-visible vsock for libkrun guests (it never calls
krun_add_vsock/krun_add_vsock_port), and libkrun's vsock
implementation is a userspace TSI bridge rather than a vhost-vsock
device the host kernel can route to. socat - VSOCK-CONNECT:cid:port
from the host therefore can't reach the guest regardless of CID.
podman -p HOST:GUEST does compose correctly with crun-krun's
passt, so we forward a per-container host port to the guest's sshd
instead. Costs a host-visible TCP port per task — acceptable while the
krun runtime stays behind the experimental flag.
Design choices and why:
- stock ssh CLI rather than a paramiko client. The binary is battle-tested for the edge cases (PTY allocation, signal forwarding, EOF semantics) that we would otherwise reimplement.
- Pubkey-only, with
IdentitiesOnly=yesso a stray host-side ssh-agent can't offer unrelated identities. The host holds the private key; the guest receives the public half via a per-task bind-mount onto/etc/ssh/authorized_keys.d/terok(the image ships an empty placeholder, so it carries no per-installation secret and caches identically across hosts). - Argv-quoted remote command:
ssh host -- a b cconcatenates the tokens and runs the result through the in-guest user's shell, so the transportshlex.quotes each token to preserve thecmd: list[str]argv contract on the wire. - No host-key persistence:
StrictHostKeyChecking=noplusUserKnownHostsFile=/dev/null. The forwarded port is bound to127.0.0.1only (orchestrator-side reservation) and the krun runtime is gated on the experimental flag, so a wrong-endpoint connect is structurally restricted to a host with podman access (already root-equivalent). Full host-key pinning would need orchestrator-sideknown_hostsplumbing and is tracked as a follow-up.
Endpoint discovery is pluggable via endpoint_resolver so unit tests
can synthesise endpoints without an actual microVM. The default
production factory
podman_port_resolver
asks podman directly for the host port forwarded to the guest's sshd —
no terok-private annotation in between.
DEFAULT_GUEST_SSHD_PORT = 22
module-attribute
¶
DEFAULT_SSH_HOST = '127.0.0.1'
module-attribute
¶
DEFAULT_SSH_USER = 'dev'
module-attribute
¶
TcpEndpoint(port, host=DEFAULT_SSH_HOST)
dataclass
¶
A host TCP endpoint reachable via podman's passt port-forward.
port is the host-side TCP port podman bound for this container's
-p <port>:22 mapping; host is the loopback address that port
was bound to.
Fields are int-coerced and range-checked in __post_init__ — the
transport interpolates port into the ssh argv and host into the
user@host token, so a string carrying shell metacharacters or
structural junk would otherwise reach the system ssh CLI. Catching
it here means a bad endpoint_resolver fails loudly at
construction rather than silently building a hostile invocation.
port
instance-attribute
¶
host = DEFAULT_SSH_HOST
class-attribute
instance-attribute
¶
__post_init__()
¶
Coerce + bound-check both fields so the ssh argv stays safe.
Source code in src/terok_sandbox/runtime/krun_transport.py
TcpSSHTransport(*, identity_file, endpoint_resolver, ssh_user=DEFAULT_SSH_USER, ssh_binary='ssh')
¶
OpenSSH-over-loopback-TCP implementation of
KrunTransport.
Holds the host-side identity (private key path) and an endpoint
resolver that maps a Container
to a TcpEndpoint.
The transport never touches the credentials vault directly — the
orchestrator exports the %host key to a tmpfs file and passes
that path in, keeping vault access out of the runtime layer.
Source code in src/terok_sandbox/runtime/krun_transport.py
exec(container, cmd, *, timeout=None)
¶
Run cmd in the guest and return its outcome.
Each cmd token is shlex.quoted into a single remote
command string so the in-guest shell treats embedded
metacharacters as literal data — argv semantics are preserved
across the inherently-shell-parsed ssh wire format.
Source code in src/terok_sandbox/runtime/krun_transport.py
exec_stdio(container, cmd, *, stdin, stdout, stderr=None, env=None, timeout=None)
¶
Bridge byte streams to cmd in the guest; return its exit code.
Environment variables are propagated via a remote env prefix
rather than SendEnv so the transport doesn't depend on the
guest's AcceptEnv whitelist. Env var names are
validated against [A-Za-z_][A-Za-z0-9_]* because the remote
env command expects bare identifiers; values and cmd
tokens are shlex.quoted so embedded shell metacharacters
cross the wire as literal data.
Source code in src/terok_sandbox/runtime/krun_transport.py
login_command(container, *, command=())
¶
Return an ssh argv that attaches a PTY to the guest's shell.
Mirrors what PodmanContainer.login_command
does for the conventional runtime — emits the argv the operator
(or terok login) execs into. Adds -tt so sshd allocates
a real PTY even when stdin isn't a terminal (the caller may be
running under tmux or an IDE proxy).
Both the empty-command path (interactive login → bash -l)
and the explicit-command path land at /workspace via
_at_workspace, so the operator's starting cwd matches what
podman exec gives under crun. Argv tokens past -- are shlex.quoted
(same helper the exec paths use) so the SSH wire format
preserves argv semantics across the login-shell parse on the
far side.
Source code in src/terok_sandbox/runtime/krun_transport.py
podman_port_resolver(*, guest_port=DEFAULT_GUEST_SSHD_PORT, host=DEFAULT_SSH_HOST)
¶
Return a resolver that reads the forwarded host port via podman port.
The orchestrator launches the container with -p <reserved>:22;
podman already records that mapping in its own metadata, so this
resolver just asks for it back — no terok-private annotation in the
middle. podman port <name> <guest_port>/tcp emits a single
<host_ip>:<host_port> line per matching mapping, which is
exactly what we need.
The resolved host is overridden to host (loopback by default) so
the SSH connect goes through 127.0.0.1 even when pasta bound
the forward to 0.0.0.0; trusting whatever podman reports would
open the door to reaching the guest via a routable interface.
Source code in src/terok_sandbox/runtime/krun_transport.py
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | |