Background
Haven (an Android SSH/SFTP client) currently uses JSch for SFTP and I'd like to contribute SFTP support to ssh-proto. Per our discussion in GlassHaven/Haven#58, opening this issue first to discuss architecture before starting work.
Proposed architecture
Layer 1: Subsystem channel request
Add requestSubsystem(name: String): Boolean to SshSession, following the same pattern as requestShell() / requestExec(). The Kaitai definition (channel_request_subsystem.ksy) already exists. This is a small, independent piece that could be a separate PR.
Layer 2: SFTP state machine
An SftpClient class that drives the SFTP protocol over an SSH session channel:
SshSession (channel with "sftp" subsystem)
└── SftpClient (state machine, packet framing, request/response correlation)
├── SftpPacketIO (reads/writes SFTP packets: length-prefixed type+id+payload)
└── Request/response correlation (maps request IDs to pending completions)
Key design decisions:
-
Packet framing: SFTP messages are length-prefixed (uint32 length + type + request_id + payload). This sits on top of SshSession.write()/read() — the SFTP client reassembles SSH channel data into complete SFTP packets.
-
Request/response correlation: SFTP is multiplexed — multiple requests can be in flight. Each request carries a unique id, and the response echoes it. A ConcurrentHashMap<UInt, CompletableDeferred<SftpResponse>> or similar structure maps request IDs to pending completions.
-
State machine: Minimal states — Init (version negotiation), Ready (accepting requests), Closed. Version negotiation happens on SSH_FXP_INIT / SSH_FXP_VERSION exchange.
-
Version support: Start with SFTPv3 (RFC draft-ietf-secsh-filexfer-02) as the baseline — it's what OpenSSH implements. Version negotiation picks min(client_version, server_version). I'd structure the code so v4/v5/v6 extensions can be added later without breaking the v3 API.
Layer 3: High-level API
interface SftpClient : AutoCloseable {
val protocolVersion: Int
suspend fun stat(path: String): SftpFileAttributes
suspend fun lstat(path: String): SftpFileAttributes
suspend fun fstat(handle: SftpHandle): SftpFileAttributes
suspend fun opendir(path: String): SftpHandle
suspend fun readdir(handle: SftpHandle): List<SftpDirEntry>?
suspend fun listdir(path: String): List<SftpDirEntry> // convenience
suspend fun open(path: String, flags: Int, attrs: SftpFileAttributes = SftpFileAttributes.EMPTY): SftpHandle
suspend fun read(handle: SftpHandle, offset: Long, length: Int): ByteArray?
suspend fun write(handle: SftpHandle, offset: Long, data: ByteArray)
suspend fun close(handle: SftpHandle)
suspend fun remove(path: String)
suspend fun mkdir(path: String, attrs: SftpFileAttributes = SftpFileAttributes.EMPTY)
suspend fun rmdir(path: String)
suspend fun rename(oldPath: String, newPath: String)
suspend fun realpath(path: String): String
suspend fun readlink(path: String): String
suspend fun symlink(linkPath: String, targetPath: String)
suspend fun setstat(path: String, attrs: SftpFileAttributes)
suspend fun fsetstat(handle: SftpHandle, attrs: SftpFileAttributes)
}
Protocol definitions
I'd add Kaitai .ksy files for the SFTP packet types (init, version, open, close, read, write, stat, etc.) following the existing pattern. These would live in the protocol module.
Module placement
SFTP could either:
- A) Live in the
sshlib module alongside the SSH channel code (since it's tightly coupled to SshSession)
- B) Be a separate
sftp module that depends on sshlib
I'd lean toward (A) for simplicity, unless you prefer the separation.
Implementation plan
- PR: subsystem channel request —
requestSubsystem() on SshSession
- PR: SFTP packet framing + version negotiation —
SftpPacketIO, init/version exchange
- PR: SFTP operations — file operations, directory listing, stat, etc.
Happy to adjust any of this based on your preferences. What are your thoughts on the state machine approach and module placement?
Background
Haven (an Android SSH/SFTP client) currently uses JSch for SFTP and I'd like to contribute SFTP support to ssh-proto. Per our discussion in GlassHaven/Haven#58, opening this issue first to discuss architecture before starting work.
Proposed architecture
Layer 1: Subsystem channel request
Add
requestSubsystem(name: String): BooleantoSshSession, following the same pattern asrequestShell()/requestExec(). The Kaitai definition (channel_request_subsystem.ksy) already exists. This is a small, independent piece that could be a separate PR.Layer 2: SFTP state machine
An
SftpClientclass that drives the SFTP protocol over an SSH session channel:Key design decisions:
Packet framing: SFTP messages are length-prefixed (
uint32 length + type + request_id + payload). This sits on top ofSshSession.write()/read()— the SFTP client reassembles SSH channel data into complete SFTP packets.Request/response correlation: SFTP is multiplexed — multiple requests can be in flight. Each request carries a unique
id, and the response echoes it. AConcurrentHashMap<UInt, CompletableDeferred<SftpResponse>>or similar structure maps request IDs to pending completions.State machine: Minimal states —
Init(version negotiation),Ready(accepting requests),Closed. Version negotiation happens onSSH_FXP_INIT/SSH_FXP_VERSIONexchange.Version support: Start with SFTPv3 (RFC draft-ietf-secsh-filexfer-02) as the baseline — it's what OpenSSH implements. Version negotiation picks
min(client_version, server_version). I'd structure the code so v4/v5/v6 extensions can be added later without breaking the v3 API.Layer 3: High-level API
Protocol definitions
I'd add Kaitai
.ksyfiles for the SFTP packet types (init, version, open, close, read, write, stat, etc.) following the existing pattern. These would live in theprotocolmodule.Module placement
SFTP could either:
sshlibmodule alongside the SSH channel code (since it's tightly coupled toSshSession)sftpmodule that depends onsshlibI'd lean toward (A) for simplicity, unless you prefer the separation.
Implementation plan
requestSubsystem()onSshSessionSftpPacketIO, init/version exchangeHappy to adjust any of this based on your preferences. What are your thoughts on the state machine approach and module placement?