Skip to the content.

_ssh — SSH client (libssh2 + axtls + TweetNaCl)

import _ssh

A real SSH-2 client. Backed by libssh2 on top of axtls (symmetric crypto, hashes) + TweetNaCl (Curve25519 KEX, Ed25519 host-key verify).

Supports:

RSA / DH-group KEX / ECDSA-NIST host keys are NOT supported (the axtls backend stubs return -1). Hosts that only offer those will fail the handshake.

Module-level

_ssh.version() — libssh2 version string

>>> _ssh.version()
'1.11.1'

_ssh.crypto_engine_name()'axtls'

>>> _ssh.crypto_engine_name()
'axtls'

_ssh.Session(sock) — the connection

Constructor takes a connected socket.socket. Performs the handshake immediately.

import socket, _ssh

s = socket.socket()
s.connect(socket.getaddrinfo('10.0.2.2', 22)[0][-1])
s.settimeout(0.1)                 # essential — see "Quirks"
sess = _ssh.Session(s)

.userauth_password(user, password)

Password auth. Raises OSError on failure.

sess.userauth_password('testuser', 'testpass')

.userauth_publickey(user, privkey_bytes, pubkey_bytes=None, passphrase=None)

Ed25519 publickey auth. privkey_bytes is the contents of an OpenSSH-format id_ed25519 file. pubkey_bytes is optional — libssh2 derives it from the private key if omitted.

with open('CLIENT.KEY', 'rb') as f:
    privkey = f.read()

sess.userauth_publickey('me', privkey)

(See open() hangs after import _ssh for a current open() gotcha that the bundled sftp.py etc. work around with build-time inlining.)

.exec(command) — run a command, return stdout

out = sess.exec('uname -a')
print(out.decode())             # bytes → str

Runs to completion, returns full stdout. Stderr / exit-status not captured.

.sftp() — open SFTP subsystem

Returns an SFTP object. See SFTP below.

.scp_recv(remote_path)bytes

Pulls a single file from the server, returns the bytes.

data = sess.scp_recv('/etc/motd')
print(data.decode())

.scp_send(remote_path, mode, data) → bytes written

Pushes a single file to the server.

sess.scp_send('/uploads/hello.txt', 0o644, b'hello from DOS\n')

.close()

Close the session.

SFTPsess.sftp()

.open(path, mode='r')SFTPFile

Modes: 'r', 'w'. Returns a file-like with .read(), .write(data), .close().

sftp = sess.sftp()
f = sftp.open('/etc/hostname', 'r')
print(f.read().decode())
f.close()

g = sftp.open('/tmp/from-dos.txt', 'w')
g.write(b'hello\n')
g.close()

.opendir(path)SFTPDir

.read() returns (name, attrs) tuples; None at end-of-dir.

d = sftp.opendir('/etc')
while True:
    entry = d.read()
    if entry is None: break
    name, attrs = entry
    mode, size, atime, mtime, uid, gid = attrs
    print(f'{mode:o} {size:>10} {name}')
d.close()

.stat(path) → 6-tuple

(mode, size, atime, mtime, uid, gid).

.realpath(path)str

Resolve to absolute canonical path.

.mkdir(path, mode=0o755) / .rmdir(path) / .unlink(path) / .rename(old, new)

Standard SFTP file ops.

.close()

Quirks

Where credentials live on DOS

DOS has no $HOME and no ~/.ssh/, so the usual OpenSSH paths do not exist. There is also no fallback location built into this port — nothing is searched automatically. You pass paths explicitly, and the conventions below are what the examples and docs assume.

Recommended layout. Put credentials in an \SSH\ directory at the root of whatever drive you run from:

C:\SSH\ID_ED.KEY      private key   (OpenSSH format)
C:\SSH\ID_ED.PUB      public key
C:\SSH\KNOWN_H.TXT    known hosts

8.3 names are mandatory. FAT gives you eight characters plus a three-character extension, uppercased. The OpenSSH names do not survive:

OpenSSH On DOS
id_ed25519 ID_ED.KEY
id_ed25519.pub ID_ED.PUB
known_hosts KNOWN_H.TXT
authorized_keys AUTH_KEY.TXT

Pick the names you like — nothing in the port depends on these — but write them the way DOS will actually store them. A file copied in as id_ed25519 becomes something like ID_ED25.519, which is not what your script will be asking for.

Pointing the examples somewhere else. examples/scp.py and examples/sftp.py take the path as an argument; there is no default and no search path. Give a full path including the drive letter if you are not running from the same drive.

Current status — read this before planning around it. Only password auth is wired up. Public-key auth needs the RSA / key- parse implementations in port/libssh2_axtls.c, which are still stubs returning -1 (docs/WIP.md item 1), and userauth_publickey_fromfile is not bound (item 3). So the table above is the convention to write to, not something the port reads today.

Interim recipe: inline the key at build time. Until key loading lands, the working pattern — the one rigs/ssh-rig uses and passes with — is to substitute the key bytes into the script before it reaches DOS, rather than reading a file at runtime:

# SSHTEST.PY carries a placeholder that the rig replaces:
PRIVKEY = __CLIENT_KEY_BYTES__       # b"-----BEGIN OPENSSH..."
session.userauth_publickey(user, PRIVKEY)

The rig renders that placeholder from the real key file on the build host, then copies the rendered script onto the disk image. See rigs/ssh-rig/run-ssh-rig.sh.

Note this workaround exists because key parsing is missing, not because file reading is broken. Reading a key file works wherever disk I/O works — verified under DOSBox-X. On QEMU + FreeDOS it will hit the disk wedge in docs/WIP.md item 2.

See also


Credit: API shape original; libssh2 protocol terminology from libssh2 docs.