SSH client and server
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Bryan c25c4e0aa3 forms over ssh: a talc program on the other side shows its toolkit form in this terminal
The client under a talc terminal asks with the channel request
forms@talc; sshd gives the session's child a forms pipe as
TERMINAL_FORMS (inherited by that child alone), its forms sent back as
extended data of code 2 and written on the client's forms pipe. A server
that refuses the request is answered nothing more; -noforms asks
nothing. regress-live checks both servers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:05:24 +01:00
examples A packet built in one piece: one copy of the payload each way 2026-10-05 18:05:36 +02:00
lib/ssh ssh: the tree's own names 2026-10-31 18:07:21 +01:00
scripts forms over ssh: a talc program on the other side shows its toolkit form in this terminal 2026-11-01 13:05:24 +01:00
tools forms over ssh: a talc program on the other side shows its toolkit form in this terminal 2026-11-01 13:05:24 +01:00
.gitignore ssh: both sides, a shell on a pseudo-terminal, interoperating with OpenSSH 2026-10-04 21:43:08 +02:00
README forms over ssh: a talc program on the other side shows its toolkit form in this terminal 2026-11-01 13:05:24 +01:00
sshd.talc forms over ssh: a talc program on the other side shows its toolkit form in this terminal 2026-11-01 13:05:24 +01:00
VERSION ssh: both sides, a shell on a pseudo-terminal, interoperating with OpenSSH 2026-10-04 21:43:08 +02:00

ssh: an SSH server and client for the talc system, so that a machine can
be reached from any other -- a shell, a command, files both ways -- and
can reach any other the same way. It speaks SSH 2 exactly, so the other
end may be OpenSSH; the implementation is talc's own. One set of
algorithms and nothing to negotiate but agreeing to it: curve25519-sha256,
ssh-ed25519 keys, chacha20-poly1305@openssh.com, strict key exchange.
Keys only, never a password; no forwarding, no agent, no compression.
The names and the files are SSH's own, because people reach this from
other systems.

From ../terminal, `ssh host` runs with no terminal on the other side --
this side's input is a pipe -- so the lines typed go to a shell that
reads them one by one, its output and its error stream come back told
apart, and its exit status is the command's: clean, and no prompt. For
prompts from a bash host, `ssh -t -noecho host "bash --noediting -i"`:
a terminal on the other side told not to echo (ECHO off, ECHONL on in
the pty-req's modes), and a bash that lets it do the reading; a shell
with a line editor of its own (readline, zle) echoes and paints
regardless, which a terminal that is not a VT100 cannot use.

From ../terminal to another talc machine, a program there that shows
a toolkit form (tirc, an application of the terminal's) is shown in
this terminal, as if it ran here. The client, finding TERMINAL_FORMS in
its environment, asks with the channel request forms@talc; sshd gives
what runs in that session a forms pipe of its own as TERMINAL_FORMS,
whose forms travel back as extended data of code 2 (1 is the error
stream) and are written on the client's forms pipe, and the events the
terminal sends come back on standard input as lines, as typed lines do.
The pipe reaches that session's child alone. A server that does not
know the request (OpenSSH) refuses it and the session goes on without;
-noforms asks nothing.

  ssh -p 2222 root@127.0.0.1 '(run "/programs/tirc")'

  sshd.talc                   the server: lets a user named in the machine's
                              users file (../users) in on a key in
                              <home>/.ssh/authorized_keys, and gives them a
                              shell, runs a command, or serves files
  lib/ssh/wire.talc           (ssh wire): SSH's data types and message numbers
  lib/ssh/transport.talc      (ssh transport): the version exchange, the
                              binary packet protocol, the key exchange and
                              rekeying -- a state machine of bytes in,
                              actions out, the random bytes handed in
  lib/ssh/keys.talc           (ssh keys): ed25519 keys in OpenSSH's files
                              (openssh-key-v1, the public line,
                              authorized_keys, known_hosts), blobs,
                              signatures, fingerprints
  lib/ssh/authenticate.talc   (ssh authenticate): the publickey exchange
                              both ways; the host-key verdict
  lib/ssh/channels.talc       (ssh channels): channels and their windows
  lib/ssh/terminal.talc       (ssh terminal): the server's pseudo-terminal
                              (after ../terminal), the client's raw mode
  lib/ssh/client.talc         (ssh client): what a program imports to reach
                              another machine -- connect, a session, a
                              command or a subsystem, read and write
  lib/ssh/files.talc          (ssh files): SFTP version 3, the packets and
                              the client, with reads and writes kept in
                              flight for whole files
  lib/ssh/files/serve.talc    (ssh files serve): SFTP's server side, a
                              request in, the answers out
  lib/ssh/local-files.talc    (ssh local-files): what SFTP needs of the
                              file system that (system files) has not:
                              reading and writing at an offset, a file's
                              status, times, size, owner, links
  tools/ssh.talc              `ssh [user@]host [command]`
  tools/scp.talc              `scp`: files and trees both ways, over SFTP
  tools/sftp-server.talc      `sftp-server`: SFTP on its input and output,
                              what sshd runs for the sftp subsystem
  tools/ssh-keygen.talc       `ssh-keygen`: an ed25519 key pair
  tools/sftp.talc             `sftp`, the interactive client [milestone 4]
  examples/                   the libraries' tests: wire, keys (byte for byte
                              against a key ssh-keygen made), the exchange in
                              memory, channels and authentication, files (the
                              SFTP client and server joined in one program)
  scripts/build.sh            builds everything under tools/ and sshd
  scripts/regress.sh          the tests under examples/
  scripts/regress-live.sh     on this host, no root, against OpenSSH both
                              ways: commands, shells, files, rekeying
  scripts/vm-ssh.sh           the vm reached from this host over a port
                              forward, and reaching back

Build and check:

  scripts/build.sh
  scripts/regress.sh
  scripts/regress-live.sh      (needs OpenSSH: ssh, sshd, sftp, sftp-server)
  scripts/vm-ssh.sh            (needs OpenSSH, qemu and ../vm buildable)

which need the installed toolchain (compiler on PATH) and ../network,
../users and ../services built beside this. The live tests run
unprivileged on high ports and touch nothing the system's own sshd owns.

RUNNING IT. The server is started by the supervisor (../services) from
definitions/sshd.service, after the network; it makes its host key at
first start under /settings/ssh if there is none and prints the fingerprint to
its log, reads the users at /settings/users and each user's keys from
<home>/.ssh/authorized_keys, and listens on port 22. It must be
privileged to give a session another user's numbers; unprivileged, it
serves its own user alone (which is how the live tests run).

  sshd [-keys <dir>] [-users <path>] [-port n | -p n] [-address a]
       [-run <dir>] [-sftp-server <path>] [-within seconds] [-seconds n] [-trace]

What a session gets: a shell on a pseudo-terminal when the client asks
for one, or on pipes when it does not; a command; or the sftp subsystem
(sftp-server, beside sshd unless -sftp-server says, started as the user,
so every file access is the kernel's decision about that user). A command
is written to a file under -run (/running/sshd) and the user's shell runs it
as a script, `shell <file>`, the form every shell understands: the talc
shell has no -c. Its language is the talc shell's, so from another
machine:

  ssh -p 2222 root@localhost '(run "list" "-long")'

prints the listing and then the status, as the prompt would.

The clients find a machine's name through the network manager (NETWORK
in the environment), or take an address; they keep host keys in
~/.ssh/known_hosts and offer the key at ~/.ssh/id_ed25519 unless -i says
another.

  ssh [-p port] [-i key] [-l user] [-t | -T] [-strict] [-known path] [-v] [user@]host [command ...]
  scp [-P port] [-i key] [-r] [-p] [-q] [-v] [-strict] [-known path] source ... target
  ssh-keygen [-t ed25519] [-f path] [-N ""] [-C comment] | -l -f path | -y -f path
  sftp-server [-users <path>]

THE FILES ARE OPENSSH'S. A key made here is read by OpenSSH and the other
way round; `ssh-copy-id` from another machine appends to a user's
authorized_keys; a known_hosts or authorized_keys line pastes between
machines. The one thing not read is an encrypted private key: this system
has no passphrase ciphers, so `ssh-keygen -p -N ""` on the other side
takes the passphrase off first.

HOW FAST. About 255-345 MB a second for a 64 MB file on this host
against OpenSSH (talc scp to and from OpenSSH's sshd 255-345, OpenSSH's
scp to and from the talc sshd 255-290), and 280-295 talc to talc, where
OpenSSH to OpenSSH does 200-265 (the host varies).
scripts/regress-live.sh prints its rates too; they are lower, since it
rekeys every megabyte.

A NOTE ON THE WARNING. OpenSSH 10's client prints a line that the key
exchange is not post-quantum. It is not a refusal; the connection is made.
The cure, if it ever matters, is mlkem768x25519-sha256 (ML-KEM in talc),
which changes nothing else here.