Skip to content

Serve SFTP through the gateway - #18

Merged
czpython merged 9 commits into
mainfrom
gateway-sftp
Aug 26, 2026
Merged

Serve SFTP through the gateway#18
czpython merged 9 commits into
mainfrom
gateway-sftp

Conversation

@czpython

@czpython czpython commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Adds SFTP to the gateway, so callers transfer files the same way they open shells — over the sandbox exec channel. This unblocks consumers that do all file transfer over SFTP and failed the moment they requested the subsystem.

Approach

The daemon owns the data plane, so the gateway has no filesystem to serve directly. Rather than reimplement file operations with shell primitives, the gateway runs the sandbox's own OpenSSH sftp-server over a persistent sbx exec and forwards SFTP to it with an SFTP client. File semantics — offsets, EOF, attributes, errors, binary safety — come from OpenSSH and asyncssh, not from drukbox.

  • gateway/backend.py: one SFTP backend per SSH connection. It opens the sftp-server on first use, shares it across every SFTP session on the connection, closes after ~30s idle, and reopens on the next use. Thus a poll loop that opens a fresh SFTP session each time starts no new process, and an inactive sandbox still sleeps.
  • gateway/sftp.py: a thin SFTPServer that delegates each operation to the backend client. Unsupported operations (rename, symlink, …) and port forwarding are refused; scp stays unsupported.
  • gateway/server.py: wires the SFTP factory and closes the backend when the connection ends.

Verification

  • uv run ruff check / ruff format --check / pyright: clean; uv run pytest: 403 pass.
  • End to end on a real host against a live sandbox: put to a nested path, chmod, get back byte-identical; stat a missing file without killing the session; poll a growing file at increasing offsets with a fresh session per poll and no new process; and the sandbox returns to sleep ~45s after the last operation.

The gateway forwards SFTP to the sandbox's own OpenSSH SFTP server over a
persistent sbx exec, driven by an SFTP client. One backend per SSH
connection is shared by every SFTP session, so a poll loop that opens a
fresh session each time starts no new process. The backend closes after
an idle period and reopens on the next use, thus an inactive sandbox
still sleeps. Port forwarding and scp stay refused.
…he cleanup task

A handshake failure now tears down the exec process instead of leaking
a live session. The connection-close cleanup task is held until it
finishes. Teardown no longer re-raises a receive task that ended with a
pipe error.
…n trim out of import

Rewrite the added comments and docstrings in Simplified Technical
English. Remove the useless test docstrings. Move the SFTP extension
trim from an import-time global mutation into start(), so it is a
deliberate step when the server starts.
The command that starts the sandbox's SFTP server is a property of the
sandbox image, not of the gateway. It moves from a constant in the
generic backend to a class attribute on SandboxProcess, set by the sbx
process. The gateway backend stays provider-agnostic.
Replace the per-call casts and handle annotations with one narrow pyright
pragma in each module. The SFTP handles pass straight from the server to
the client, so the annotations added no safety.

Disable the unsupported SFTP extensions on the first SFTP session, not at
import time, and guard the change so it runs once.

Keep the tests that cover the gateway logic (one forwarding round trip,
error survival, poll reuse, shared backend, refused forwarding, idle
close, failed handshake). Drop the ones that only measured the upstream
server.
The command names a path in the sandbox image, so it is not a fixed
property of a process class. Move it to a gateway setting with the
standard Debian path as the default; an operator sets it for an image
with a different path. Drop the run-once guard on the extension filter,
which is idempotent.
The command names a path in the provider's image, so the provider owns
it, next to default_image. The gateway reads it from the provider when it
builds the SFTP backend, and stays free of any image path. The base
returns an empty command, which a provider the gateway does not serve
keeps.
The sbx image is fixed, so its SFTP server path does not change. Drop the
setting and return the standard path from the provider. A later provider
that needs a different path can add a seam then.
The command is fixed, so the backend holds it as a constant where it runs
it. Drop the provider property and the plumbing that passed it through.
The tests point the constant at the host's own sftp-server.
@czpython
czpython merged commit 0b76b11 into main Aug 26, 2026
6 checks passed
@czpython
czpython deleted the gateway-sftp branch August 26, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant