Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zig-sftp

An SFTP (SSH File Transfer Protocol) server library and CLI in Zig.

Implements version 3 of the protocol (draft-ietf-secsh-filexfer-02), which is what OpenSSH speaks and therefore what interoperability means in practice.

Design

The protocol codec and the session logic know nothing about SSH. SFTP is length-prefixed packets over a reliable byte stream, so keeping the codec free of transport types means it can be driven from an SSH subsystem, a socket, or a test buffer, and tested directly rather than through a live connection.

  • protocol.zig - packet framing, wire types, encode/decode
  • server.zig - session state, handle table, path confinement

Usage

const sftp = @import("sftp");

var session = sftp.Session.init(allocator, "/srv/data");
defer session.deinit();

// Refuses anything that leaves the root.
const path = try session.resolve("docs/report.pdf");
defer allocator.free(path);

Parsing a stream:

while (try sftp.protocol.parse(buffer)) |packet| {
    // packet.body borrows from buffer
    buffer = buffer[packet.consumed..];
}

parse returns null when the buffer holds less than a whole packet. That is the normal case on a stream and is deliberately distinct from an error - a caller that treats "need more bytes" as a failure drops valid traffic.

CLI

zig-sftp version
zig-sftp check-path --root /srv/data docs/a.txt
zig-sftp serve --root /srv/data

serve speaks SFTP on stdin/stdout, which is how an SSH daemon invokes a subsystem:

Subsystem sftp /path/to/zig-sftp serve --root /srv/data

check-path is exposed as a command because path confinement is the part of a file server most worth being able to check from the outside:

$ zig-sftp check-path --root /srv/data ../etc/passwd
refused: ../etc/passwd (Escapes)

Install a prebuilt binary

Every v* tag publishes release binaries. Each asset is a gzipped tarball holding a single zig-sftp executable, named after its target:

Asset Runs on
zig-sftp-aarch64-linux-musl.tar.gz 64-bit ARM Linux: Raspberry Pi 3/4/5 on a 64-bit OS, AWS Graviton
zig-sftp-x86_64-linux-musl.tar.gz 64-bit x86 Linux
zig-sftp-aarch64-macos.tar.gz Apple Silicon macOS
zig-sftp-x86_64-macos.tar.gz Intel macOS

A checksums.txt (sha256, one line per asset) is attached alongside them.

tag=v0.1.0
target=aarch64-linux-musl
base="https://github.com/zig-utils/zig-sftp/releases/download/$tag"
curl -fsSLO "$base/zig-sftp-$target.tar.gz"
curl -fsSLO "$base/checksums.txt"
sha256sum --check --ignore-missing checksums.txt
tar -xzf "zig-sftp-$target.tar.gz"
sudo install -m 0755 zig-sftp /usr/local/bin/zig-sftp

The Linux builds link musl statically, so the binary has no runtime dependencies and runs on any distro, Raspberry Pi OS included. A 32-bit Pi OS cannot run the aarch64 build; use a 64-bit image. The macOS builds link libSystem, which every macOS ships.

zig build cross produces the same layout locally, one binary per target under zig-out/cross/<target>/zig-sftp.

Path confinement

This is the whole security boundary, so it is worth stating what it refuses. Three distinct escapes are handled, all in resolveWithin:

  • .. traversal, including forms that only escape after normalisation
  • absolute paths - a leading / means the top of the served root, not the host's filesystem root, which is what SFTP clients intend
  • a sibling whose name merely shares the root's prefix (/srv/data-evil is not under /srv/data), which a naive startsWith check gets wrong

Normalisation is lexical and never touches the filesystem, because resolving against the filesystem would follow symlinks - and a symlink is exactly how a client escapes a root that only checks the textual path.

Status

Implemented and tested: the wire codec (framing, primitives, attributes, status replies), session and handle lifecycle, path confinement, error-to-status mapping, and version negotiation.

Not yet implemented: the filesystem request handlers (open/read/write/ readdir and friends) and the SSH transport. SFTP normally runs as an SSH subsystem, and hosting it means an SSH server - a separate concern this library deliberately does not take on. Point sshd at the CLI instead.

Testing

zig build test

Requires Zig 0.17.0-dev. CI and releases build with the exact nightly pinned in build.zig.zon (minimum_zig_version) and .github/workflows/.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages