Skip to content

V2? Indeed #597

Open
Open

Description

@puellanivis

I’ve finally gotten into the muck and got the work done to push out a v2 API.

This is right now just a preview, so we can look at things in a potentially breaking change manner, to see where we need to improve things, and invite discussion on the design.

The client copies much closer the os package, particularly with the OpenFile call, and reduces a lot of the repetitious code. It also uses generics, so we’re setting a new minimum version that can use our package. 🙈

The server on the other side is mimicking the gRPC framing for general instructions, but still remains the sort of os.File mimickry for Files. It does split Files and Directories, even though I’m not sure any implementation actually truly cares about this. But the design should allow one to force this distinction, if they want to make such a strict implementation. It is right now, strictly single threaded, but designed with an eye for how things could be spun off into concurrent tasks.

A local filesystem server is implemented as a subpackage, which is also useful as a demonstration of how to implement the new server interface. This removes the need for an in-memory filesystem, but that’s also something we could add back in, if there is significant demand.

Also, I took the time to read up every single supported Statfs implementation and figure out how that converts to that OS’s Statvfs. 🙃 That was fun. Also, we’re pulling in as much of the Stat_t as possible, which requires a pretty interesting array of OS-specific packages due to subtle naming differences.

Uh… yeah. Pile the bug tickets on, I want to know where I missed the mark, or messed up the implementation.

PS: Oh, I also want to add that all this work was possible because of the incredible work the people ahead of me did with this package. While I might not keep much of any of the original code, it taught me what works, and what was more kludgy than it was worth. So many decisions are made when we cannot know the ramifications later, like when the language puts out its own fs abstraction, and now we’re holding onto a deprecated design. But knowing now to not make those decisions now is worth more than gold! And I probably would have made the same choice, because it looked right at the time. We just cannot see the future. 🤷‍♀️

Activity

  1. christhomas commented on Oct 17, 2026

    @christhomas
    Contributor

    I've spent the last few days working through the v2 code (#683, #684, #685), and wanted to put together a picture of where the API stands and what still seems to be missing for a v2.0.0, as a starting point for discussion. It tries to follow the direction already in dev-v2 and in #407: model the client on os and io/fs, keep the server gRPC-shaped with optional extension interfaces, keep the protocol in encoding/ssh/filexfer, and keep the public surface small. None of it is meant as final.

    Where it stands

    Already in dev-v2 (or #649):

    • encoding/ssh/filexfer and openssh as the wire protocol packages, which Proposal: io/fs presents us an opportunity to rework the whole api into a v2 #407 suggested building first.
    • A client that follows os: OpenFile(name, flag, perm), Mkdir(name, perm), ReadDir returning []fs.DirEntry, a Dir type with ReadDir(n), errors as *fs.PathError/*fs.LinkError around an sshfx.Status whose Is matches fs.ErrNotExist, fs.ErrPermission and io.EOF (greatroar's suggestion in Proposal: io/fs presents us an opportunity to rework the whole api into a v2 #407), DirFS(dir) fs.FS, and a generic NewClient over small SSHClient/SSHSession interfaces, so the package doesn't import x/crypto.
    • A server with a gRPC-style ServerHandler, UnimplementedServerHandler to embed, extension interfaces (HardlinkServerHandler, POSIXRenameServerHandler, StatVFSServerHandler), FileHandler/DirHandler with optional SetStat/Truncate/Chmod/Chown/Chtimes/StatVFS interfaces, Hijack for raw packets, and localfs as a separate package.

    Client

    1. Walking and globbing. v1 has Walk, Glob and Match. In v2, fs.WalkDir(cl.DirFS("/"), …) and fs.Glob already work, but only for paths below the DirFS root, so paths relative to the login directory (what v1's Walk("dir") does) need a wrapper. Two filepath-style helpers would cover it: func (cl *Client) WalkDir(root string, fn fs.WalkDirFunc) error and func (cl *Client) Glob(pattern string) ([]string, error). Or we just document the DirFS route and leave them out.
    2. Contexts. Only ReadDirContext and ReaddirContext take a context today, though everything is context-aware internally. Either add …Context variants for all the request methods (the DialContext pattern), or only for the ones that can take a long time: ReadDir, RemoveAll, ReadFile/WriteFile, and WalkDir if it's added. I'd lean towards the second, to keep the surface small.
    3. Choosing what to start on the server. NewClient always requests the sftp subsystem. WithSubsystem(name string) and WithCommand(cmd string) options would cover Execute SUDO prior to transfer #12 (sudo /usr/lib/openssh/sftp-server, the same as sftp -s). WithCommand needs Start(cmd string) error in SSHSession, which *ssh.Session already has.
    4. Symlink variants. Lchmod, Lchown and Lchtimes through lsetstat@openssh.com ([v2] add client and server support for lsetstat@openssh.com #684).
    5. More OpenSSH extensions. OpenSSH's sftp-server also announces limits@openssh.com (lets the client size requests to what the server accepts instead of assuming 32 KiB), expand-path@openssh.com (~ expansion), copy-data (server-side copy), home-directory and users-groups-by-id@openssh.com (names for ls -l style listings). Each one is small and independent.
    6. Reconnecting (Way to detect underlying SSH connection has closed #144). I'd keep it out of the core API. [v1] add an example of a client that reconnects #690 adds an example for v1 that could be carried over.

    Server

    1. Serving an fs.FS. A read-only func FSHandler(fsys fs.FS) ServerHandler would make serving os.DirFS, an embed.FS or a testing/fstest.MapFS a one-liner. It also covers most of what v1's in-memory handler was used for in tests, without bringing a memfs back into the package.
    2. localfs on os.Root. dev-v2 already requires go1.25, so localfs could resolve paths through an os.Root opened on a configured directory. The OS then keeps every operation inside that directory, including through symlinks that point outside it, which WorkDir and path.Join can't do. That would give the chroot use case drakkan mentioned in Proposal: io/fs presents us an opportunity to rework the whole api into a v2 #407 without extra code in the server, and make localfs a safer starting point for people who copy it.
    3. Concurrency. In V2? Indeed #597 you mentioned the server is single-threaded for now, but designed so work could be spun off. Reads and writes on different handles, and keeping writes to one handle in order, are where it matters for throughput. This is probably the biggest open design question, and one where your plan should come first.
    4. Logging. Debug io.Writer could become a *slog.Logger, so it fits whatever logging the application already uses.

    Testing support

    An sftptest package along the lines of httptest (#274):

    // NewClient returns a client connected to an in-process Server that uses handler.
    // Both are closed through t.Cleanup.
    func NewClient(t testing.TB, handler sftp.ServerHandler, opts ...sftp.ClientOption) *sftp.Client

    With FSHandler and fstest.MapFS, a test against a fake remote filesystem would be about three lines. A second helper that starts a real SSH server on 127.0.0.1 with a generated host key would cover code that takes an *ssh.Client, but it needs x/crypto, so it would have to live in its own module, like examples does.

    A possible order

    If this looks like the right direction, each of these could be its own small issue and PR, which would also suit the project board idea from #648:

    1. FSHandler, then sftptest.NewClient on top of it
    2. WithSubsystem/WithCommand
    3. localfs on os.Root
    4. WalkDir/Glob, or documenting the DirFS route
    5. the OpenSSH extensions, one at a time
    6. server concurrency, once you've settled on an approach

    I'm happy to take on any of these. Let me know which you'd want, and what you'd change.

  2. puellanivis commented on Oct 19, 2026

    @puellanivis
    CollaboratorAuthor

    Client

    1. Approve on Walk, Glob, Match
    2. Approve second idea, providing context-aware alternatives only those where they are expected to take a long time. Where we don’t have to match any given interface, we could potentially provide first-class context-aware versions, that don’t have a ...Context suffix.
    3. WithSubsystem and WithCommand are reasonable features, but perhaps after release of v2.0.0 proper
    4. lsetstat alreadfy done and merged, (I think.) So, obviously, I agree with those.
    5. Good call on the additional extensions.
    6. Good idea on having an example for handling a reconnecting client. Since we removed the dependency on x/crypto it would be unfortunate to return it… while we could likely also just expand the SSH... interfaces, it doesn’t present something that people can see how it works. And they’ll probably want to add their own logging on that.

    Server

    1. FSHandler isn’t a bad idea.
    2. 🤔 I do like the idea of using an *os.Root for handling the WorkDir
    3. Fortunately, the original sftp-server is also single-threaded. The safest way to ensure compatibility with expectations, and coherence are to stay single-threaded. But yeah, like I said, this is something we should be able to do later. Probably through a mux of receivers based on filename/handle. 🤔 Better to leave for after v2.0.0 release.
    4. Switching to slog.Logger is also a good idea.

    Intended path:

    I want to ensure that we’re basically feature complete enough that we can port v1 to using v2 under the hood. And that we have addressed security, stability concerns, and API design choices. We need to consider that there are things that can be implemented after a proper release. Features and nice-to-haves are great, but we don’t need to block release of v2.0.0 behind implementing additional OpenSSH extensions.

    So, I will have main interests in focusing on reviewing and developing release blockers, rather than extra features.

  3. christhomas commented on Oct 19, 2026

    @christhomas
    Contributor

    Well, I'll see what I can do and you can tell me what you like or don't like and we'll go from there, I'll try to make the PRs not so crazy :P

  4. puellanivis commented on Oct 19, 2026

    @puellanivis
    CollaboratorAuthor

    You’re doing great so far!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions