Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/egress.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ verdict into `/etc/silkd-lane` before the golden snapshot (or on a cold boot):
`relay` on a locked bridge lane, `direct` on a CNI lane. Where silkd reads
`relay` it hands the proxy variables to every exec exactly as on the none lane,
and the image's units wait for the same file instead of guessing from NIC
presence; git keeps running as on any lane with a NIC. A golden built under
presence. A golden built under
another verdict is rebuilt, not adopted.

Egress-lane sandboxes never archive and do not hibernate, fork, checkpoint, or
Expand Down
19 changes: 10 additions & 9 deletions docs/sdk-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,22 +520,23 @@ when the relay drops, which `w.error` tells apart from a clean close (`None`).
## Git

```python
sb.git_clone(url, "/work/repo", branch="main", depth=1, auth=token) # egress lane only
sb.git_clone(url, "/work/repo", branch="main", depth=1, auth=token)
st = sb.git_status("/work/repo") # {"type", "branch", "ahead", "behind", "files", "truncated"?}
sb.git_add("/work/repo", ["a.txt"])
sha = sb.git_commit("/work/repo", "message", "Dev <dev@example.com>")
sb.git_push("/work/repo", auth=token) # egress lane only
sb.git_pull("/work/repo", auth=token) # egress lane only
sb.git_push("/work/repo", auth=token)
sb.git_pull("/work/repo", auth=token)
br = sb.git_branches("/work/repo") # {"type", "current", "branches"}
sb.git_create_branch("/work/repo", "feature")
sb.git_checkout("/work/repo", "feature")
sb.git_delete_branch("/work/repo", "feature")
```

Results are structured (porcelain v2 under the hood), never scraped stdout.
Auth tokens travel as an in-memory header, never touching guest disk. On
the no-network lane, clone/push/pull raise a typed `unimplemented` error
pointing at `push`.
Auth tokens travel as an in-memory header, never touching guest disk.
Clone, push and pull reach the remote the way an exec does: directly on the
egress lane, through the egress proxy elsewhere, so a pool whose policy does
not allow the git host fails them with git's own error.

## Terminals

Expand Down Expand Up @@ -584,8 +585,8 @@ zero. `undrain()` lifts only a drain and `uncordon()` only a

```python
try:
sb.git_clone(url, "/work/repo")
sb.read_file("/work/missing.txt")
except SilkdError as e:
if e.kind == "unimplemented": # no-network lane: fall back to push
sb.push("/work/repo", tar_bytes)
if e.kind == "not_found":
...
```
17 changes: 9 additions & 8 deletions docs/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -652,22 +652,23 @@ overflow instead of the stream silently dropping events.
## Git

```go
err = sb.GitClone(ctx, url, "/work/repo", "main", 0, token) // egress lane only; depth > 0 = shallow
err = sb.GitClone(ctx, url, "/work/repo", "main", 0, token) // depth > 0 = shallow
st, err := sb.GitStatus(ctx, "/work/repo") // Branch, Ahead, Behind, Files[]; Truncated when the list was cut at ~1 MiB
err = sb.GitAdd(ctx, "/work/repo", "a.txt")
hash, err := sb.GitCommit(ctx, "/work/repo", "message", "Dev <dev@example.com>")
err = sb.GitPush(ctx, "/work/repo", token) // egress lane only
err = sb.GitPull(ctx, "/work/repo", token) // egress lane only
err = sb.GitPush(ctx, "/work/repo", token)
err = sb.GitPull(ctx, "/work/repo", token)
br, err := sb.GitBranches(ctx, "/work/repo") // Current + Branches
err = sb.GitCreateBranch(ctx, "/work/repo", "feature")
err = sb.GitCheckout(ctx, "/work/repo", "feature")
err = sb.GitDeleteBranch(ctx, "/work/repo", "feature")
```

Results are structured (porcelain v2 under the hood), never scraped stdout.
Auth tokens travel as an in-memory header, never touching guest disk. On the
no-network lane, clone/push/pull fail fast with a typed `unimplemented`
error pointing at `Push`.
Auth tokens travel as an in-memory header, never touching guest disk.
Clone, push and pull reach the remote the way an exec does: directly on the
egress lane, through the egress proxy elsewhere, so a pool whose policy does
not allow the git host fails them with git's own error.

## Terminals

Expand Down Expand Up @@ -737,8 +738,8 @@ tenant.

```go
var e *wire.ErrorResp
if errors.As(err, &e) && e.Kind == wire.KindUnimplemented {
// no-network lane: fall back to sb.Push
if errors.As(err, &e) && e.Kind == wire.KindNotFound {
// the guest path does not exist
}
```

Expand Down
4 changes: 2 additions & 2 deletions docs/silkd.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ A failed verb terminates with `error {kind, message}`:
|---|---|
| `bad_request` | malformed frame, empty argv, invalid pattern, an exec argv or cwd the guest cannot spawn (missing, not a directory, not executable), an exec or pty `user` the guest cannot resolve (`unknown user`, or `look up user: <errno>` when the name service itself fails) |
| `not_found` | unknown pid/session/path, a `port_forward` port nothing listens on, a language with no LSP manifest |
| `unimplemented` | verb unavailable on this lane — notably git clone/push/pull on the no-network lane, whose message points at `fs.push` |
| `unimplemented` | reserved in the protocol; no verb answers it today |
| `internal` | everything else (other spawn failures, git errors, I/O) |

## Network lanes

git clone/push/pull consult lane detection: an interface under
Lane detection decides whether execs route directly: an interface under
`/sys/class/net` counts only when it has a `device` backing (a virtio or
physical NIC). Name-based checks are not enough — the all-builtin sandbox
kernel auto-creates virtual tunnels (`sit0` and friends) even on the no-NIC
Expand Down
15 changes: 7 additions & 8 deletions e2e/cmd/smoke/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func main() {
addr := flag.String("addr", "127.0.0.1:7777", "sandboxd address")
token := flag.String("token", "", "node api token")
template := flag.String("template", "rt:24.04", "template ref")
egress := flag.Bool("egress", false, "also claim an egress-lane sandbox and check lane detection")
egress := flag.Bool("egress", false, "also claim an egress-lane sandbox and run git in it")
lspTemplate := flag.String("lsp-template", "", "python-flavor template ref; when set, adds the LSP broker step")
flag.Parse()

Expand All @@ -48,7 +48,6 @@ func run(addr, token, template, lspTemplate string, egress bool) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

// Explicitly the no-network lane: the git step asserts its typed error.
client, sb, err := harness.Claim(ctx, addr, token, template, sandbox.WithNetwork(sandbox.NetNone))
if err != nil {
return err
Expand Down Expand Up @@ -278,24 +277,24 @@ func smokeGit(ctx context.Context, sb *sandbox.Sandbox) error {
return fmt.Errorf("after checkout: current=%q, want feature", br.Current)
}

if err := sb.GitPush(ctx, "/work", ""); !isSilkdKind(err, wire.KindUnimplemented) {
return fmt.Errorf("push on none lane: %v, want unimplemented", err)
if err := sb.GitPush(ctx, "/work", ""); !isSilkdKind(err, wire.KindInternal) {
return fmt.Errorf("push without a remote: %v, want git's own error", err)
}
return nil
}

// smokeEgress pins that git runs on the egress lane, unlike the none-lane unimplemented guard.
// smokeEgress claims an egress-lane sandbox and reads a repository's status through it.
func smokeEgress(ctx context.Context, client *sandbox.Client, template string) error {
sb, err := client.New(ctx, template, sandbox.WithNetwork(sandbox.NetEgress))
if err != nil {
return fmt.Errorf("claim: %w", err)
}
defer func() { _ = sb.Close() }()
if _, err := sb.Exec(ctx, "git", "init", "-q", "-b", "main", "/tmp/r"); err != nil {
if _, err = sb.Exec(ctx, "git", "init", "-q", "/tmp/r"); err != nil {
return err
}
if err := sb.GitPush(ctx, "/tmp/r", ""); !isSilkdKind(err, wire.KindInternal) {
return fmt.Errorf("push on egress lane: %v, want internal git failure (lane misdetected?)", err)
if _, err = sb.GitStatus(ctx, "/tmp/r"); err != nil {
return fmt.Errorf("status on the egress lane: %w", err)
}
return nil
}
Expand Down
6 changes: 3 additions & 3 deletions protocol/wire/frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ type PortForward struct {

func (PortForward) Op() string { return "port_forward" }

// GitClone clones a repo; network-lane only; Auth rides an in-memory Authorization header, never guest disk.
// GitClone clones a repo; Auth rides an in-memory Authorization header, never guest disk.
type GitClone struct {
URL string `json:"url"`
Path string `json:"path"`
Expand Down Expand Up @@ -369,15 +369,15 @@ type GitCommit struct {

func (GitCommit) Op() string { return "git_commit" }

// GitPush pushes the current branch; network-lane only.
// GitPush pushes the current branch.
type GitPush struct {
Path string `json:"path"`
Auth string `json:"auth,omitempty"`
}

func (GitPush) Op() string { return "git_push" }

// GitPull pulls the current branch; network-lane only.
// GitPull pulls the current branch.
type GitPull struct {
Path string `json:"path"`
Auth string `json:"auth,omitempty"`
Expand Down
6 changes: 3 additions & 3 deletions sdk/go/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/cocoonstack/sandbox/protocol/wire"
)

// GitClone clones url into path, shallow when depth > 0; auth is an in-memory header token; needs the egress lane.
// GitClone clones url into path, shallow when depth > 0; auth is an in-memory header token.
func (s *Sandbox) GitClone(ctx context.Context, url, path, branch string, depth uint32, auth string) error {
return s.doneRPC(ctx, &wire.GitClone{URL: url, Path: path, Branch: branch, Depth: depth, Auth: auth})
}
Expand All @@ -30,12 +30,12 @@ func (s *Sandbox) GitAdd(ctx context.Context, path string, files ...string) erro
return s.doneRPC(ctx, &wire.GitAdd{Path: path, Files: files})
}

// GitPush pushes the current branch; on the no-network lane it fails with a typed error pointing at Push.
// GitPush pushes the current branch.
func (s *Sandbox) GitPush(ctx context.Context, path, auth string) error {
return s.doneRPC(ctx, &wire.GitPush{Path: path, Auth: auth})
}

// GitPull pulls the current branch; same lane rules as GitPush.
// GitPull pulls the current branch.
func (s *Sandbox) GitPull(ctx context.Context, path, auth string) error {
return s.doneRPC(ctx, &wire.GitPull{Path: path, Auth: auth})
}
Expand Down
2 changes: 1 addition & 1 deletion sdk/python/cocoonsandbox/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ def replace(self, files: list[str], pattern: str, replacement: str) -> list[dict
return [f for f in conn.recv_until("done") if f["type"] == "replaced"]

def git_clone(self, url: str, path: str, branch: str = "", depth: int = 0, auth: str = "") -> None:
"""Clones into path (egress lane only; the none lane answers a typed unimplemented error pointing at push)."""
"""Clones into path, shallow when depth is set; auth rides an in-memory header."""
self._done_rpc("git_clone", url=url, path=path, branch=branch or None, depth=depth or None, auth=auth or None)

def git_status(self, path: str) -> dict[str, Any]:
Expand Down
6 changes: 3 additions & 3 deletions sdk/python/e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ def _tree():
member = tr.extractfile("tree/t.txt")
assert member and member.read() == b"tar-marker"

@step("git-typed-error")
@step("git-unreachable-remote")
def _git():
try:
sb.git_clone("https://example.com/x.git", "/root/x")
raise AssertionError("clone on the none lane did not raise")
raise AssertionError("clone of a host the policy does not allow succeeded")
except SilkdError as exc:
assert exc.kind == "unimplemented", exc
assert exc.kind == "internal", exc

@step("hibernate-wake")
def _hibernate():
Expand Down
7 changes: 1 addition & 6 deletions silkd/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,11 @@ where
.stderr(Stdio::piped())
.process_group(0)
.kill_on_drop(!req.detach);
if let Some(cwd) = &req.cwd {
cmd.current_dir(cwd);
}
cmd.env_clear();
cmd.envs(sysutil::base_env().iter().copied());
cmd.envs(claim.iter().map(|(k, v)| (*k, &**v)));
cmd.envs(&req.env);
if let Some(user) = &req.user
&& let Err(e) = sysutil::apply_user(&mut cmd, user)
{
if let Err(e) = sysutil::apply_cwd_user(&mut cmd, req.cwd.as_deref(), req.user.as_deref()) {
return proto::error_frame(out, ErrorKind::BadRequest, e).await;
}

Expand Down
10 changes: 1 addition & 9 deletions silkd/src/git.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! git verbs wrap the guest git binary into structured results; the none lane fails clone/push/pull with a typed error.
//! git verbs wrap the guest git binary into structured results.

use std::process::Stdio;

Expand All @@ -12,8 +12,6 @@ use crate::sysutil;
const STATUS_FILES_BYTES: usize = proto::MAX_FRAME / 8;
const STATUS_ENTRY_OVERHEAD: usize = 40;

const NO_EGRESS: &str = "no network on this sandbox; use fs.push to move a repository in";

/// Clones `url` into `path` (optionally a branch, shallow depth), then Done.
pub async fn clone<W: AsyncWrite + Unpin>(
w: &mut W,
Expand All @@ -23,9 +21,6 @@ pub async fn clone<W: AsyncWrite + Unpin>(
depth: Option<u32>,
auth: Option<&str>,
) -> std::io::Result<()> {
if !crate::net::has_egress() {
return proto::error_frame(w, ErrorKind::Unimplemented, NO_EGRESS).await;
}
let depth_arg;
let mut args: Vec<&str> = vec!["clone"];
if let Some(b) = branch {
Expand Down Expand Up @@ -97,9 +92,6 @@ pub async fn net_verb<W: AsyncWrite + Unpin>(
auth: Option<&str>,
verb: &str,
) -> std::io::Result<()> {
if !crate::net::has_egress() {
return proto::error_frame(w, ErrorKind::Unimplemented, NO_EGRESS).await;
}
let out = git(path, auth, &[verb]).await?;
terminal(w, verb, &out).await
}
Expand Down
11 changes: 0 additions & 11 deletions silkd/src/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use std::sync::atomic::{AtomicI8, Ordering};
/// File the host writes with its lane verdict, `relay` or `direct`; on the root filesystem so a guest reboot keeps it.
const LANE_FILE: &str = "/etc/silkd-lane";

static LANE_OVERRIDE: AtomicI8 = AtomicI8::new(-1);
/// The lane file's verdict once read: -1 not yet present, 0 direct, 1 relay.
static LANE: AtomicI8 = AtomicI8::new(-1);
// probed once: the NIC set is fixed for the guest's life
Expand All @@ -27,11 +26,6 @@ static SILKD_NET: LazyLock<i8> = LazyLock::new(|| match std::env::var("SILKD_NET

/// Reports whether the guest can reach a network; only a `device`-backed interface counts, since the kernel auto-creates virtual tunnels.
pub fn has_egress() -> bool {
match LANE_OVERRIDE.load(Ordering::Relaxed) {
0 => return false,
1 => return true,
_ => {}
}
match *SILKD_NET {
0 => return false,
1 => return true,
Expand All @@ -55,11 +49,6 @@ pub fn routes_directly() -> bool {
!relay
}

/// Lane override for tests: set_var would race every concurrent getenv.
pub fn override_egress_for_tests(lane: Option<bool>) {
LANE_OVERRIDE.store(lane.map_or(-1, i8::from), Ordering::Relaxed);
}

/// Reads the lane file: `Some(true)` for relay, `Some(false)` for direct, `None` while the host has not written it.
fn lane_verdict(path: &str) -> Option<bool> {
let mut buf = [0u8; 8];
Expand Down
16 changes: 9 additions & 7 deletions silkd/src/net_egress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,15 @@ where

#[cfg(target_os = "linux")]
async fn dial_host_cloexec(port: u32) -> io::Result<tokio_vsock::VsockStream> {
let stream = tokio::task::spawn_blocking(move || {
let stream = vsock::VsockStream::connect_with_cid_port(libc::VMADDR_CID_HOST, port)?;
stream.set_nonblocking(true)?;
Ok::<_, io::Error>(stream)
})
.await
.map_err(io::Error::other)??;
let pending = crate::sysutil::watch_fd(crate::sysutil::vsock_connect_start(
libc::VMADDR_CID_HOST,
port,
)?)?;
drop(pending.writable().await?);
let stream = vsock::VsockStream::from(pending.into_inner());
if let Some(e) = stream.take_error()? {
return Err(e);
}
tokio_vsock::VsockStream::new(stream)
}

Expand Down
7 changes: 1 addition & 6 deletions silkd/src/pty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,10 @@ pub async fn open<W: AsyncWrite + Unpin>(
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
let mut cmd = Command::new(&shell);
cmd.arg("-i");
if let Some(dir) = &req.cwd {
cmd.current_dir(dir);
}
cmd.env_clear().envs(sysutil::base_env().iter().copied());
sysutil::apply_claim_env(&mut cmd);
cmd.envs(&req.env);
if let Some(user) = &req.user
&& let Err(e) = sysutil::apply_user(&mut cmd, user)
{
if let Err(e) = sysutil::apply_cwd_user(&mut cmd, req.cwd.as_deref(), req.user.as_deref()) {
return proto::error_frame(out, ErrorKind::BadRequest, e).await;
}
match (slave.try_clone(), slave.try_clone()) {
Expand Down
Loading
Loading