SSH

The ssh module (1.2.0) is a Geblang-native SSH client. It runs commands, streams I/O, transfers files via SFTP, and forwards ports - all on top of the stream protocol so SSH sessions behave the same as files, pipes, and TCP sockets.

Connect

import io;
import ssh;

let c = ssh.connect("alice@server.example.com", {
    "port": 22,
    "privateKeyFile": "~/.ssh/id_rsa",
    "knownHostsFile": "~/.ssh/known_hosts",
});
let r = c.exec("uname -a");
io.println(r.stdout);
c.close();

ssh.connect(target, opts = {}):

opts key Type Description
port int Default 22
timeoutMs int Connect timeout
password string Password auth
privateKey string PEM private key as a string
privateKeyFile string Path to a PEM private key
passphrase string Decrypts a passphrased key
agent bool Use $SSH_AUTH_SOCK
knownHostsFile string Path to a known_hosts file
insecureSkipHostKey bool Skip host-key verification (dev only)

target is "user@host" or just "host" (defaults to $USER). At least one auth method is required. At least one host-key check is required - explicit opt-in to insecure mode is safer than a silent default.

One-shot commands: exec

let r = c.exec("ls /srv");
io.println(r.stdout);    # captured output
io.println(r.stderr);    # captured error stream
io.println(r.exitCode);  # 0 on success

exec blocks until the command completes and returns an ExecResult. Use it for short commands with bounded output.

Streaming: spawn

let s = c.spawn("tail -f /var/log/app.log");
for (line in s.stdout) {
    io.println(line);
}
s.kill();

spawn returns an SSHSession with stdin / stdout / stderr as streams.IOStream values - the same shape as proc.Process. Use it for long-running commands or when you need to stream input through stdin.

Method Returns Description
wait() int Block until exit, return exit code
kill() void Send SIGKILL via the SSH channel
signal(name) void Send a named signal ("TERM", "HUP", ...)

SFTP

c.upload("./app.bin", "/srv/bin/app");
c.download("/srv/log/app.log", "./remote.log");
for (entry in c.sftpList("/srv")) {
    io.println(entry["name"] as string);
}
c.sftpMkdir("/srv/new", 0o755);
c.sftpRemove("/srv/old.tmp");

# Stream a remote file as a regular IOStream.
let remote = c.sftpOpen("/srv/log/app.log");
for (line in remote) {
    io.println(line);
}
remote.close();

sftpOpen returns a streams.IOStream, so you can pipe it through streams.copy(remote, localFile) or iterate line-by-line. The underlying SFTP client is lazy-initialised and cached on the SSH connection - the first SFTP call pays the setup cost; later calls reuse it.

Port forwarding

# Local: localhost:8080 -> internal-db:5432 (via the SSH server).
let tunnel = c.forwardLocal(8080, "internal-db:5432");

# ... use 127.0.0.1:8080 from this process ...

tunnel.close();
# Remote: <server>:9090 -> localhost:3000 (server initiates connections back).
let rev = c.forwardRemote(9090, "localhost:3000");
rev.close();

Tunnels run an accept-loop goroutine on the appropriate side. close() stops the loop and joins so the next call from the parent goroutine happens-after the last forwarded byte.

When to use ssh vs proc vs sockets

Use case Module
Run a local subprocess proc.spawn
Run a remote command via SSH ssh.spawn (or ssh.exec)
Transfer files between hosts ssh.upload / ssh.download
Talk to a non-SSH TCP service sockets.dial
Build a TCP server sockets.serve
Expose a non-SSH service through SSH ssh.forwardLocal

The Process, Socket, and SSHSession classes all expose the same streams.IOStream-shaped stdin / stdout / stderr surface, so piping data between them is uniform.