MorphOS — the modern PowerPC continuation of the Amiga operating system — ships an OpenSSH client, but no sshd, no telnetd, no way to reach a shell on the machine from another one. If you want to drive a MorphOS box remotely, you can dial out; nothing lets you dial in.
So I built morphos-telnetd: an authenticated telnet daemon, released under the MIT license. It is a small program, but it sits on top of a genuinely unusual problem, and the way that problem is not the Unix problem is the interesting part.
The problem: there are no pseudo-terminals
A Unix telnet daemon is mostly glue. The kernel already provides the hard parts: openpty() gives you a pseudo-terminal pair, fork() gives you a process, the line discipline gives you cooked-vs-raw input, and login handles authentication. telnetd opens a pty, forks, wires the shell’s stdin/stdout to the pty slave, and then spends the rest of its life copying bytes between the pty master and the socket. Everything difficult is borrowed.
None of that machinery exists in the Amiga lineage. There are no ptys. There is no fork(). There is no /bin/login. A straight port has nothing to port to.
What the Amiga lineage has instead is a different idea of what a terminal is. On Unix a console is a device node; you read() and write() file descriptors and the kernel routes the bytes. On AmigaDOS a console is not a device node — it is a message port that answers packets. When a program calls Read() on its console, dos.library turns the call into an ACTION_READ packet and posts it to whatever port that file handle names. Nothing requires that port to belong to the system’s console handler.
That last sentence is the whole design. The daemon spawns a Shell, points it at a message port the daemon itself owns, and then answers the shell’s console packets by hand:
| AmigaDOS packet | Unix equivalent |
|---|---|
ACTION_READ / ACTION_WRITE | read() / write() on the pty master |
ACTION_WAIT_CHAR | select() on the tty |
ACTION_SCREEN_MODE | ICANON — raw vs cooked |
ACTION_FINDINPUT / FINDOUTPUT | open() of the terminal |
ACTION_EXAMINE_FH | fstat() on the tty |
ACTION_CHANGE_SIGNAL | where SIGINT should be delivered |
That set of packets is a pty, expressed in messages instead of file descriptors. The daemon doesn’t attach a shell to a terminal; it becomes the terminal.
The consequence worth internalizing: on Unix the kernel sits between the shell and the daemon, so a mistake produces an EIO and a dead connection. Here there is nothing in between, and MorphOS has no memory protection. A mistake in packet accounting doesn’t return an error — it hangs a process, or takes down the machine.
One process, no fork
Because there is no fork(), the daemon cannot spawn a process per connection the way inetd-style Unix daemons do. And because SMP does not work on MorphOS — even a multiprocessor machine runs on one core — threads would buy nothing.
So the whole thing is one process serving every session, multiplexed with a single WaitSelect() — bsdsocket.library’s select(), plus an Exec signal mask as a sixth argument. One wait covers the listening socket, every session’s socket, and every session’s console message port at once. That single call is what makes the design possible, and it is a cleaner shape than the fork-per-connection model it replaces.
A few things that bit, and are worth knowing
Nothing is reclaimed at exit. This is the defining fact of the platform. A socket left open, a lock left held, a signal port left dangling — none of it comes back until the machine reboots. The daemon has to clean up every resource by hand, on every exit path, including the failure paths. A leaked listening socket means the port is simply gone until a reboot.
The daemon boots four seconds too early. Started from S:User-Startup, the daemon’s bind() failed with errno 49 — EADDRNOTAVAIL, “can’t assign requested address” — then succeeded four seconds later, on three consecutive boots. The network stack was loaded, but the interface had no address yet: User-Startup runs before the network chain. The fix was to start it from S:user-network-startup instead, which runs after the address is assigned. It binds first try now. A dependency that is always four seconds late looks exactly like one that is occasionally late — until something counts.
A telnet client can be conforming and still get stuck. The daemon originally ignored the DONT and WONT option-negotiation messages that RFC 854 requires it to answer. A well-behaved client that negotiated the wrong way would sit forever waiting for a reply that never came, with doubled characters and no way out from its end. That is worse than a missing feature — it is a daemon that can wedge a correct client — and it was the last thing fixed before release.
An undocumented packet with no name. Programs built against ixemul, MorphOS’s BSD compatibility layer, ask the console for something the native Shell never does: a private packet, ACTION_SESSION_MODE, defined in ixemul’s own source and in no SDK header, so it shows up in a trace as a bare number. It asks “are you a Linux-compatible console handler?” Answer it wrong and ixemul substitutes a control code for every newline the moment a program clears ONLCR — a failure that only appears in raw mode, which is the hardest case to notice by hand.
Authentication, and honesty about what telnet is
There is no /bin/login to hand off to, so the daemon authenticates in-process against the accounts in MorphOS Preferences → Users, via usergroup.library. An account with no password, or one locked, is refused rather than admitted — the out-of-the-box state is a blank password, and failing open would hand a shell to anyone who connected.
And telnet is plaintext. It authenticates and then carries every keystroke, password included, in the clear. That is fine on a trusted LAN behind a firewall and stated plainly in the security notes; it is not fine across the open internet. Which points at where this is really headed.
Why a telnet daemon is groundwork for an sshd
The RFC 854 telnet parser is deliberately the smallest, most replaceable part of the program. Everything hard — attaching a real Shell to a byte stream, answering console packets, tracking window size without a pty, surviving on a platform that reclaims nothing — lives in layers below it. An sshd for MorphOS would delete the telnet parser, keep those layers, and add crypto. openssl4.library already ships in the SDK.
So the telnet daemon is useful today for a platform that had nothing, and it is the proof that the hard half of a future sshd already works.
The source, build instructions, and a rather candid comparison to how a Unix telnetd differs are at github.com/bfeeny/morphos-telnetd.