• wcsshd/wcrlogind -- who wantsto test

    From Robert Wolfe@1:261/20 to All on Sun Oct 18 17:42:12 2026
    ========================================================
    ======================
    wcsshd / wcrlogind -- ARCHITECTURE DESIGN (pre-implementation)
    Two daemons, one project: an SSH server and an rlogin server for Wildcat!
    Interactive Net Server, both acting as GATEWAYS into Wildcat!'s existing
    Telnet-based terminal/login service rather than reimplementing it. ========================================================
    ======================

    WHY "GATEWAY", NOT "REIMPLEMENT"
    ----------------------------------
    Wildcat! already has a complete interactive terminal service -- login
    prompt, main menu, message areas, doors, everything -- reachable today
    over plain Telnet. Neither an SSH daemon nor an rlogin daemon needs to
    know anything about THAT (no new RPC surface, no menu code, nothing wcimaprpc.pas/wcserverrpc.pas-shaped). Both new daemons instead:

    1. Accept the outer connection (SSH or rlogin) and speak that
    protocol's own handshake/framing.
    2. Open a SECOND, plain TCP connection, from wcsshd/wcrlogind itself
    to Wildcat!'s existing Telnet listener (localhost by default).
    3. From then on, act as a byte-shovel between the two: bytes from the
    SSH channel / rlogin socket go to the inner Telnet connection and
    back, in both directions, for the life of the session.

    This is the same "reuse the existing pipeline, only build the front
    door" philosophy wcsmtpd already applies to the hook chain/prespool
    queue -- there, wcsmtpd.exe replaces wcsmtp.exe's LISTENING role only
    and hands off to Wildcat!'s existing downstream machinery; here, wcsshd.exe/wcrlogind.exe replace nothing about Wildcat!'s terminal
    service, they just put an SSH- or rlogin-shaped front door in front of
    the SAME Telnet listener that's already there. Wildcat!'s own login
    prompt, account lockout, security level checks, everything -- all still
    apply, unmodified, inside the gateway.

    ONE CONSEQUENCE WORTH STATING UP FRONT: because Telnet's own IAC
    (Interpret As Command) negotiation bytes only exist on the INNER
    connection (between the gateway and Wildcat!), and neither SSH nor
    rlogin clients understand IAC at all, the gateway must be a competent
    enough Telnet CLIENT itself to keep that inner connection sane (answer Wildcat!'s option negotiations sensibly, never forward a raw IAC byte
    out to the SSH/rlogin side) -- see section 2.


    1. RESEARCH: WHAT'S ACTUALLY AVAILABLE, AND THE CRYPTO DECISION ------------------------------------------------------------
    SSH is not TLS -- OpenSSL implements TLS/SSL, not the SSH transport/ auth/connection protocols (RFC 4251-4254), so wctls.pas/openssl.pas
    (already built for wcsmtpd/wcimapd) are NOT reusable here for the SSH
    protocol itself, even though both ultimately rest on similar symmetric- cipher/MAC primitives under the hood. Implementing SSH's key exchange,
    ciphers, MACs, and framing correctly BY HAND, from scratch, in Pascal,
    for something that will authenticate real BBS accounts over the open
    Internet, is not a responsible use of a from-scratch implementation --
    subtle mistakes in hand-rolled crypto are exactly the kind of thing
    that turns into a real vulnerability, and this was flagged to and
    confirmed with the project owner directly rather than assumed silently.

    DECISION (confirmed): wcsshd.exe dynamically loads libssh (a mature,
    actively maintained, security-audited C SSH library) exactly the way
    wctls.pas dynamically loads OpenSSL and wcserverrpc.pas/wcimaprpc.pas dynamically load wcsrv2(.x64).dll -- LoadLibrary/GetProcAddress, a function-pointer table per needed export, fail-soft (wcsshd.exe still
    starts and logs a clear reason if libssh can't be loaded, same pattern
    as "TLS not configured" in wcsmtpd/wcimapd).

    WHAT'S RESEARCHED VS. ASSUMED (read before implementing wclibssh.pas):
    - libssh's server API shape (ssh_bind_new/ssh_bind_options_set/
    ssh_bind_listen/ssh_bind_accept, ssh_new+ssh_handle_key_exchange
    per accepted connection, ssh_set_server_callbacks with an
    auth_password_function/channel_open_request_session_function/
    pty_request_function/shell_request_function callback struct,
    ssh_event_new+ssh_event_add_session+ssh_event_dopoll as the poll
    loop that drives those callbacks, ssh_channel_read/ssh_channel_
    write for the actual data path) reflects libssh's long-stable,
    well-documented public C API (api.libssh.org) -- confirmed via
    libssh's own published server API reference and its
    examples/ssh_server_fork.c sample during this design pass.
    - A PREBUILT libssh.dll for Windows is NOT something libssh's own
    project ships directly (libssh.org's own downloads are source
    tarballs) -- the practical way to get one is vcpkg
    ("vcpkg install libssh") or a MinGW/MSYS2 build, both of which
    produce a libssh.dll wcsshd.exe can dynamically load the same way
    it would load any other DLL. This is a REAL extra install step for
    whoever runs wcsshd.exe with TLS enabled -- exactly like OpenSSL
    being a separate install for wcsmtpd/wcimapd's TLS support, just a
    less common one, and it will be documented just as plainly in
    wcsshd-INSTALL.txt (including the vcpkg command) rather than
    assumed present.
    - NOT verified: the EXACT export names/ordinals/calling convention
    of a specific libssh.dll build (vcpkg's vs. a hand-built MinGW one
    can differ slightly in packaging, e.g. static vs. import-lib
    naming) -- wclibssh.pas's GetProcAddress calls are written against
    the documented C API names, and like every other dynamic binding
    in this project, missing exports are detected and reported by name
    rather than assumed present (same pattern as wcserverrpc.pas's
    DLL-export sanity check). This gets exercised for real only once
    tested against an actual libssh.dll on the target Windows machine
    -- flagged honestly in wcsshd-RELEASE-NOTES.txt's CURRENT STATUS,
    not glossed over.


    2. THE SHARED TELNET GATEWAY (wctelnetgw.pas) ------------------------------------------------------------
    One unit, used by both daemons, doing three things:

    a) OPEN the inner connection: a plain TCP connect (fpSocket/fpConnect,
    same Sockets-unit primitives as every other daemon in this project)
    to Cfg.TelnetHost:Cfg.TelnetPort (default 127.0.0.1:23 -- i.e.
    Wildcat!'s own Telnet listener, assumed running on the SAME
    machine as wcsshd/wcrlogind; a different host is supported via ini
    but not the common case).

    b) SPEAK JUST ENOUGH TELNET on that inner connection to keep it
    working without dragging the outer client into it at all:
    - Any IAC (byte 255) sequence arriving FROM Wildcat! is parsed
    (IAC + one of WILL/WONT/DO/DONT + one option byte, or IAC SB
    ... IAC SE for subnegotiation) and stripped out of what gets
    forwarded to the outer client -- the outer SSH/rlogin session
    only ever sees the actual terminal text/data bytes.
    - The gateway answers a SMALL, fixed set of option negotiations
    itself rather than implementing full RFC 854/855-1091 option
    negotiation generally: DO ECHO -> WONT (the gateway does not
    want Wildcat! also echoing on top of whatever the outer
    client/terminal already does -- avoids double-echo, a classic
    Telnet-gateway bug), DO/WILL SUPPRESS-GO-AHEAD -> agree (WILL/
    DO respectively -- harmless, commonly expected), everything
    else -> WONT/DONT (decline) -- Wildcat!'s Telnet listener is
    expected to fall back to sane line-based behavior for anything
    declined, same as talking to a very old/plain Telnet client.
    - A NAWS-equivalent (terminal window size) is NOT negotiated in
    v1 -- see KNOWN LIMITATIONS; Wildcat! gets whatever its own
    default assumption is, same as a client that never sends NAWS
    today. Terminal TYPE (Telnet TTYPE option) similarly not
    forwarded in v1 -- SSH's own pty-req terminal type / rlogin's
    own terminal-type field (see sections 3-4) are captured by the
    gateway but not yet threaded through as a Telnet TTYPE answer;
    documented as a roadmap item, not silently dropped.
    - IAC 255 255 (a literal 0xFF data byte, escaped per Telnet's
    own rules) arriving from Wildcat! is un-escaped back to one
    0xFF data byte before forwarding outward, and -- if the OUTER
    client ever sends a raw 0xFF byte inward (SSH: basically never,
    since SSH channel data isn't Telnet-framed at all; rlogin:
    possible, it's raw bytes) -- it gets escaped TO IAC 255 255
    before being sent to Wildcat!, so Wildcat!'s own Telnet parser
    never misreads a legitimate data byte as the start of an IAC
    sequence.

    c) BRIDGE bytes bidirectionally for the life of the session: two
    directions, driven the same way regardless of which outer protocol
    is in front -- inner-to-outer bytes go through the IAC filter in
    (b) then out through whatever the caller's "send to outer client"
    callback is (an SSH channel write, or a raw rlogin socket send);
    outer-to-inner bytes go through the escape step in (b) then a
    plain send on the inner Telnet socket. wcsshd.exe drives this loop
    from inside libssh's own event loop (ssh_event_dopoll, section 5);
    wcrlogind.exe drives it with a plain two-thread-or-select loop
    matching wcsmtpd's existing blocking-I/O-per-thread style (each
    rlogin session already gets its own OS thread, same as an SMTP/
    IMAP session, so a second inner reader thread per session is a
    small, well-precedented addition, not a new concurrency model).

    OPTIONAL CREDENTIAL AUTO-FEED (used by wcsshd only, see section 5):
    wctelnetgw.pas accepts an optional (Username, Password: string) pair
    at bridge-start time. If given, the FIRST bytes written to the inner
    connection, before the bidirectional bridge loop starts, are
    Username + CRLF, then Password + CRLF -- i.e. the gateway TYPES the
    already-SSH-authenticated credentials into Wildcat!'s own login
    prompt on the user's behalf, so an SSH user isn't asked to log in
    twice. THIS IS A DOCUMENTED ASSUMPTION, not a verified fact: it
    assumes Wildcat!'s Telnet listener's login sequence is exactly
    "username prompt, then password prompt, in that order, each ended by
    the user's Enter key" with nothing else required first (no "Press
    any key", no ANSI-detection prompt, etc. -- real Wildcat! installs
    commonly have one or more of those). wcsshd-TROUBLESHOOTING.txt will
    need to say plainly that this may need a small adjustable "pre-login
    skip" (a configurable number of lines/prompts to wait through, or a
    configurable expected-prompt substring to wait for before typing the
    username) once tested against a real system, and ships with that as
    a documented, ini-adjustable value rather than a hardcoded assumption
    -- see section 6, [Gateway] LoginPromptWait.
    wcrlogind does NOT get this treatment (see section 4) -- rlogin
    cannot carry a password at all, so there is nothing valid to
    auto-feed beyond the username, and even that is UNAUTHENTICATED
    client-supplied data (see section 4's security note) -- auto-feeding
    an unverified username is far lower-stakes than a password would be,
    but it's still worth being honest that "auto-feed" here means
    something weaker for rlogin than for SSH.


    3. wcsshd.exe -- SSH DAEMON ------------------------------------------------------------
    Listener architecture: IDENTICAL in shape to wcsmtpd.pas's Alpha-4
    dual-stack design -- TListenerThread/TListenerFamily ported over
    unchanged, IPv6-preferred/IPv4-fallback, both on by default, each
    listener's bind failure independent/fail-soft. Cfg.ListenPort default
    2222 (NOT 22 -- same "safe by default, non-standard until you've
    tested" rule as every other daemon in this project's family of ports:
    wcsmtpd 2525, wcimapd 1430). ONE listener per address family in v1 (no
    separate "implicit" vs upgraded mode the way SMTP/IMAP have -- SSH is
    encrypted from the very first byte from the client's point of view, so
    there's only one kind of SSH listener to run, not two).

    Per-connection flow (TSshSession = class(TThread), same base shape as TSmtpSession/a hypothetical TImapSession):
    1. Accept -> ssh_new (libssh session object) bound to the accepted
    socket's fd, ssh_handle_key_exchange (the SSH transport-layer
    handshake -- version exchange, KEX, host key, encryption/MAC
    algorithm negotiation -- all handled INSIDE libssh, wcsshd.pas
    never touches raw KEX/cipher bytes itself, which is the entire
    point of using the library).
    2. Host key: wcsshd.exe needs its own SSH host key (RSA or ed25519,
    a PEM file, generated once with ssh-keygen or libssh's own
    ssh_pki_generate -- documented in wcsshd-INSTALL.txt) -- loaded
    via ssh_bind_options_set(SSH_BIND_OPTIONS_HOSTKEY, path) at
    ssh_bind_new time, once per process, same "load once, use for
    every accepted connection" lifetime as wctls.pas's shared SSL_CTX.
    3. Authentication: libssh's server callback mechanism
    (ssh_server_callbacks_struct.auth_password_function) is invoked
    BY libssh, inside ssh_event_dopoll, whenever the connecting client
    attempts password auth. wcsshd's callback does exactly what
    wcimaprpc.pas's WcRpcLogin already does for wcimapd -- validate
    (username, password) against the real Wildcat! user database. To
    avoid a cross-project unit dependency, wcsshd's project gets its
    OWN small wcauthrpc.pas: a copy of just the auth-relevant slice of
    wcimaprpc.pas (WcRpcInit/Shutdown, WcRpcCreateContext/
    DeleteContext, WcRpcLogin/Logout, TUser/TUserInfo types) -- none
    of the conference/message-store API wcimapd needed, since wcsshd
    never touches mail. Same non-Windows-fails-soft-but-still-compiles
    structure wcimaprpc.pas now has (see wcimapd's own recent
    refactor), for the same Linux-dev-and-test reason.
    Returns SSH_AUTH_SUCCESS only on a real WcRpcLogin success; every
    other case (bad credentials, RPC unavailable) returns
    SSH_AUTH_DENIED with a generic reason, same non-distinguishing-
    error-message discipline as every other login path in this
    project. MaxLoginAttempts-style throttling: libssh itself already
    limits auth tries per connection (AuthTries option); wcsshd sets
    that rather than reinventing it.
    4. Channel + pty + shell: once authenticated, libssh's
    channel_open_request_session_function fires when the client opens
    its session channel; pty_request_function captures the client's
    requested terminal type/dimensions (kept for a future NAWS/TTYPE
    forward, see section 2); shell_request_function (or exec_function,
    if a client asks to run a command instead of a shell -- v1 treats
    ANY exec request the same as a shell request, ignoring the
    specific command string, since there's nothing here for it to
    mean other than "give me the Wildcat! terminal") is where wcsshd
    opens the inner Telnet connection (section 2) and starts the
    bridge, feeding the SAME (username, password) that just passed
    WcRpcLogin as the auto-feed credentials.
    5. Bridge runs (section 2c) until either side closes; wcsshd then
    ssh_channel_close/ssh_disconnect/ssh_free/closes the inner Telnet
    socket, in that order, all best-effort (never raise during
    cleanup, same rule as every Free/Shutdown path elsewhere in this
    project).

    Idle timeout / watchdog: same TWatchdog force-close-the-socket-from- another-thread pattern as wcsmtpd, applied to the OUTER accepted
    socket (closing it unblocks whatever libssh call is currently pending
    on that fd the same way it unblocks a plain fpRecv elsewhere).

    Public-key authentication: OUT OF SCOPE for v1 (see section 6) --
    Wildcat!'s own account model is username+password, with no existing
    concept of "this SSH public key maps to this BBS account" to check a
    key against, so there is nothing correct for wcsshd to do with a
    pubkey auth attempt beyond declining it and telling the client to use
    password auth instead (ssh_bind_options_set constrains offered auth
    methods so libssh only advertises password to begin with, rather than
    accepting pubkey attempts and always failing them one at a time).


    4. wcrlogind.exe -- RLOGIN DAEMON ------------------------------------------------------------
    rlogin (RFC 1282) predates any real security model -- on the wire it
    is: client connects (traditionally to port 513, privileged-source-port
    by convention only, not enforced by this implementation), server
    expects four NUL-terminated fields with no framing beyond the NULs:
    client-user-name \0 server-user-name \0 terminal-type/speed \0
    then (v0-era) waits for a single 0-byte ack from the server before the
    session becomes a raw bidirectional byte stream, or (v1282-clarified)
    proceeds directly, plus an OOB "window-size" control sequence
    (0xFF 0xFF "ss" + a struct with rows/cols) the client MAY send later
    over the socket's out-of-band/urgent channel for terminal resizing --
    v1 of wcrlogind reads and discards that OOB sequence if present
    (acknowledging it exists so it doesn't corrupt the data stream) but
    does not forward it to Wildcat! (same NAWS gap noted in section 2).

    THERE IS NO PASSWORD FIELD IN THE RLOGIN PROTOCOL AT ALL. Its original
    security model was host-based trust (a server-side .rhosts file
    saying "this client host + this client username is allowed in as this
    server username, no password needed") -- something Wildcat! has no
    equivalent of and wcrlogind does NOT attempt to implement (implementing .rhosts-style trust would mean SKIPPING Wildcat!'s own password check
    entirely, which is a much worse security posture than requiring it).
    So wcrlogind.exe's rlogin handling is: parse the three NUL-terminated
    fields, open the Telnet gateway (section 2), auto-feed ONLY the
    client-supplied username (never a password -- there isn't one) as a
    convenience so the user doesn't have to retype it, and let Wildcat!'s
    own Telnet login prompt ask for the actual password INTERACTIVELY over
    the (still cleartext, since rlogin itself is never encrypted) bridged connection, exactly as a plain Telnet client would experience.

    SECURITY POSTURE, STATED PLAINLY (goes in wcrlogind-README.txt's own
    "read this first," not buried): wcrlogind.exe provides ZERO
    confidentiality and ZERO real authentication of its own -- it is
    exactly as secure as plain Telnet (because that's what it's a thin
    front-end for), with the one difference that the connecting client's
    claimed username is taken on faith and pre-filled rather than typed.
    It exists purely for legacy rlogin-client compatibility, not as a security-hardened option -- wcsshd.exe is the one to point actual
    "I want this encrypted" traffic at. Given that, wcrlogind's [Listen]
    Address in the shipped ini defaults to 127.0.0.1 (loopback only, NOT 0.0.0.0/::) -- the one deliberate exception to every other daemon in
    this project defaulting to all-interfaces -- so a fresh install can't accidentally expose a cleartext-password login path to the open
    Internet; an admin has to explicitly widen it, with a loud comment in wcrlogind.ini explaining why.

    Listener architecture: same dual-stack TListenerThread/TListenerFamily
    shape as wcsmtpd/wcsshd, port default 5130 (non-standard, vs. the
    conventional 513, same reasoning as every other port default in this
    project -- doubly so here given the loopback-default point above).


    5. EVENT LOOP / THREADING NOTE (wcsshd specifically) ------------------------------------------------------------
    libssh's server callback model is normally driven by ssh_event_dopoll
    in a loop on ONE session at a time (or via ssh_event_add_session for a
    few sessions multiplexed on one poll loop) -- for wcsshd, the simplest
    correct mapping onto this project's established "one OS thread per
    connection" model (used by every other daemon here) is: each
    TSshSession thread creates its OWN ssh_event, adds only its own
    ssh_session to it, and loops ssh_event_dopoll(event, timeout_ms) with
    a short timeout (so the loop can also check the watchdog-driven
    Terminated flag between polls) until the channel/bridge is done or the connection drops -- i.e. one event loop per thread per connection, NOT
    one shared event loop multiplexing every connection (which would be
    more "libssh-idiomatic" for a single-threaded event-driven server, but
    would mean rewriting this project's entire concurrency model just for
    this one daemon). This keeps wcsshd.pas's shape recognizable next to
    every other daemon here (wcsmtpd/wcimapd), at the cost of one extra
    OS thread's worth of overhead per SSH connection, which is the same
    tradeoff this project already made for wcsmtpd/wcimapd over a
    single-threaded select() server.


    6. CONFIG SCHEMA
    ------------------------------------------------------------
    wcsshd.ini
    [Listen] Address/EnableIPv4/Address6/EnableIPv6/Hostname/Port
    (Port default 2222, see section 3)
    [SSH] HostKeyFile (PEM, required -- no default, wcsshd logs
    "TLS"-style "SSH not configured" and refuses to start
    any listener without one, since there is no sane
    "plaintext fallback" for an SSH daemon the way TLS-
    optional made sense for SMTP/IMAP), MaxAuthTries
    (default 3, passed straight to libssh), LibSshDllName
    (override, same idea as openssl.pas's DLL-name
    fallback list but user-settable here since libssh's
    own Windows packaging isn't as standardized as
    OpenSSL's -- see section 1).
    [Gateway] TelnetHost/TelnetPort (default 127.0.0.1:23),
    LoginPromptWait (section 2's documented-assumption
    escape hatch -- default 0, meaning "type the username
    immediately"; settable to a small delay in ms, or a
    future version could match a configurable prompt
    substring instead of a blind delay -- v1 ships the
    simple delay-based version and documents the
    limitation).
    [Limits] SessionTimeoutSecs, MaxConnections -- same meaning as
    every other daemon.
    [Options] ConnectionLog -- same structured-log convention.

    wcrlogind.ini
    [Listen] Same fields, Address defaults to 127.0.0.1 (see
    section 4), Port default 5130.
    [Gateway] TelnetHost/TelnetPort -- same as wcsshd's.
    [Limits] / [Options] Same shape as wcsshd's.


    7. CONNECTION LOGGING ------------------------------------------------------------
    Same two-file convention as wcsmtpd/wcimapd (free-text wcsshd<date>.log
    / wcrlogind<date>.log plus a structured wcsshd-conn<date>.log / wcrlogind-conn<date>.log), same [cid=N ip=...] tagging. Structured
    fields, beyond the usual cid/family/peer/listener/duration_ms:
    wcsshd: auth_result (success/denied/error), username (only once
    auth SUCCEEDED -- never logged for a failed/unknown attempt,
    so the log itself can't be used to enumerate valid
    usernames the way a bad error message could), cipher/kex
    (whatever libssh negotiated, via ssh_get_cipher_in/
    ssh_get_kex_algo-equivalent queries once available),
    bytes_in/bytes_out (both directions of the bridge).
    wcrlogind: claimed_username (the client-supplied, UNVERIFIED rlogin
    username -- explicitly labeled "claimed" in the field name
    itself so nobody mistakes it for an authenticated identity
    when reading the log), bytes_in/bytes_out.
    "Detailed connection logging", as asked for, means this structured log
    plus the free-text log's own connect/auth-attempt/disconnect narrative
    -- not packet-level tracing of the bridged Telnet session's actual
    terminal content, which is exactly the kind of thing a BBS user would
    NOT expect logged (their session transcript, potentially including
    whatever they type at Wildcat!'s own password prompt on the inner
    connection) -- wcsshd/wcrlogind log connection METADATA, never the
    bridged data stream's contents. This boundary gets stated explicitly
    in both README.txt files.


    8. WHAT'S DELIBERATELY OUT OF SCOPE FOR v1 ------------------------------------------------------------
    - SSH public-key authentication (section 3) -- password only.
    - NAWS / Telnet TTYPE forwarding through the gateway (section 2) --
    Wildcat! sees whatever its own no-negotiation default is.
    - rlogin's OOB window-size resize sequence is drained, not acted on.
    - Any host-based trust model for rlogin (section 4) -- deliberately
    never implemented, not a gap.
    - SSH host-key rotation/multiple host keys (RSA + ed25519 offered
    together) -- v1 loads exactly one HostKeyFile.
    - A configurable per-daemon Wildcat! "which Telnet node/port" beyond
    a single TelnetHost:TelnetPort pair -- no multi-node load
    balancing across several Telnet listeners.


    ========================================================
    ======================

    --- Platinum Xpress/Win/WINServer v10.0gr19.1
    * Origin: Over the Brink : Grand Island, NY - brinkbbs.org (1:261/20)
  • From Robert Wolfe@1:261/20 to Robert Wolfe on Sun Oct 18 17:52:46 2026
    c:\wcat>wcsshd.exe
    wcSSHD for Wildcat! Interactive Net Server.
    Wildcat! and its SDK are (Copyright) Santronics Software
    Programming in FreePascal by Robert Wolfe <rwolfe@twc.com>
    Version 1.0/Alpha-1
    Ctrl+C to stop.

    ! config file not found: c:\wcat\wcsshd.ini -- using built-in defaults (note: [SSH]
    HostKeyFile has no default and MUST be set, see wcsshd-INSTALL.txt)
    $ wcsshd v1.0/Alpha-1 starting -- IPv4 on (:2222), IPv6 on ([]:2222), hostname=localhost, gateway=127.0.0.1:23, MaxAuthTries=3, ConnectionLog=true
    ! FATAL: [SSH] HostKeyFile is not set in c:\wcat\wcsshd.ini -- wcsshd cannot start
    without an SSH host key. See wcsshd-INSTALL.txt for how to generate one (ssh-keygen
    or libssh's own ssh_pki_generate).
    * shutdown requested -- stopping listener(s)
    * all listener(s) stopped

    c:\wcat>

    ----

    c:\wcat>wcrlogind.exe
    wcRLOGIND for Wildcat! Interactive Net Server.
    Wildcat! and its SDK are (Copyright) Santronics Software
    Programming in FreePascal by Robert Wolfe <rwolfe@twc.com>
    Version 1.0/Alpha-1
    Ctrl+C to stop.

    $ wcrlogind v1.0/Alpha-1 starting -- IPv4 on (10.8.0.3:513), IPv6 on ([::1]:513),
    hostname=localhost, gateway=127.0.0.1:23, ConnectionLog=true
    ! [Listen] Address is not loopback-only -- rlogin has NO encryption and NO real
    authentication of its own (see wcrlogind-README.txt); this is your explicit choice to
    widen it, not a default.
    * listening on [::1]:513 (IPv6, preferred)
    * listening on 10.8.0.3:513 (IPv4, dual-stack)

    On 10/18/2026 5:42 PM, Robert Wolfe wrote to all:


    ======================================================
    ==
    ======================
    wcsshd / wcrlogind -- ARCHITECTURE DESIGN (pre-implementation)
    Two daemons, one project: an SSH server and an rlogin server for Wildcat!
    Interactive Net Server, both acting as GATEWAYS into Wildcat!'s existing
    Telnet-based terminal/login service rather than reimplementing it.

    ======================================================
    ==
    ======================

    WHY "GATEWAY", NOT "REIMPLEMENT"
    ----------------------------------
    Wildcat! already has a complete interactive terminal service -- login prompt, main menu, message areas, doors, everything -- reachable today
    over plain Telnet. Neither an SSH daemon nor an rlogin daemon needs to
    know anything about THAT (no new RPC surface, no menu code, nothing wcimaprpc.pas/wcserverrpc.pas-shaped). Both new daemons instead:

    1. Accept the outer connection (SSH or rlogin) and speak that
    protocol's own handshake/framing.
    2. Open a SECOND, plain TCP connection, from wcsshd/wcrlogind itself
    to Wildcat!'s existing Telnet listener (localhost by default).
    3. From then on, act as a byte-shovel between the two: bytes from the
    SSH channel / rlogin socket go to the inner Telnet connection and
    back, in both directions, for the life of the session.

    This is the same "reuse the existing pipeline, only build the front
    door" philosophy wcsmtpd already applies to the hook chain/prespool
    queue -- there, wcsmtpd.exe replaces wcsmtp.exe's LISTENING role only
    and hands off to Wildcat!'s existing downstream machinery; here, wcsshd.exe/wcrlogind.exe replace nothing about Wildcat!'s terminal
    service, they just put an SSH- or rlogin-shaped front door in front of
    the SAME Telnet listener that's already there. Wildcat!'s own login
    prompt, account lockout, security level checks, everything -- all still apply, unmodified, inside the gateway.

    ONE CONSEQUENCE WORTH STATING UP FRONT: because Telnet's own IAC
    (Interpret As Command) negotiation bytes only exist on the INNER
    connection (between the gateway and Wildcat!), and neither SSH nor
    rlogin clients understand IAC at all, the gateway must be a competent
    enough Telnet CLIENT itself to keep that inner connection sane (answer Wildcat!'s option negotiations sensibly, never forward a raw IAC byte
    out to the SSH/rlogin side) -- see section 2.


    1. RESEARCH: WHAT'S ACTUALLY AVAILABLE, AND THE CRYPTO DECISION ------------------------------------------------------------
    SSH is not TLS -- OpenSSL implements TLS/SSL, not the SSH transport/ auth/connection protocols (RFC 4251-4254), so wctls.pas/openssl.pas
    (already built for wcsmtpd/wcimapd) are NOT reusable here for the SSH protocol itself, even though both ultimately rest on similar symmetric- cipher/MAC primitives under the hood. Implementing SSH's key exchange, ciphers, MACs, and framing correctly BY HAND, from scratch, in Pascal,
    for something that will authenticate real BBS accounts over the open Internet, is not a responsible use of a from-scratch implementation -- subtle mistakes in hand-rolled crypto are exactly the kind of thing
    that turns into a real vulnerability, and this was flagged to and
    confirmed with the project owner directly rather than assumed silently.

    DECISION (confirmed): wcsshd.exe dynamically loads libssh (a mature, actively maintained, security-audited C SSH library) exactly the way wctls.pas dynamically loads OpenSSL and wcserverrpc.pas/wcimaprpc.pas dynamically load wcsrv2(.x64).dll -- LoadLibrary/GetProcAddress, a function-pointer table per needed export, fail-soft (wcsshd.exe still
    starts and logs a clear reason if libssh can't be loaded, same pattern
    as "TLS not configured" in wcsmtpd/wcimapd).

    WHAT'S RESEARCHED VS. ASSUMED (read before implementing wclibssh.pas):
    - libssh's server API shape (ssh_bind_new/ssh_bind_options_set/
    ssh_bind_listen/ssh_bind_accept, ssh_new+ssh_handle_key_exchange
    per accepted connection, ssh_set_server_callbacks with an
    auth_password_function/channel_open_request_session_function/
    pty_request_function/shell_request_function callback struct,
    ssh_event_new+ssh_event_add_session+ssh_event_dopoll as the poll
    loop that drives those callbacks, ssh_channel_read/ssh_channel_
    write for the actual data path) reflects libssh's long-stable,
    well-documented public C API (api.libssh.org) -- confirmed via
    libssh's own published server API reference and its
    examples/ssh_server_fork.c sample during this design pass.
    - A PREBUILT libssh.dll for Windows is NOT something libssh's own
    project ships directly (libssh.org's own downloads are source
    tarballs) -- the practical way to get one is vcpkg
    ("vcpkg install libssh") or a MinGW/MSYS2 build, both of which
    produce a libssh.dll wcsshd.exe can dynamically load the same way
    it would load any other DLL. This is a REAL extra install step for
    whoever runs wcsshd.exe with TLS enabled -- exactly like OpenSSL
    being a separate install for wcsmtpd/wcimapd's TLS support, just a
    less common one, and it will be documented just as plainly in
    wcsshd-INSTALL.txt (including the vcpkg command) rather than
    assumed present.
    - NOT verified: the EXACT export names/ordinals/calling convention
    of a specific libssh.dll build (vcpkg's vs. a hand-built MinGW one
    can differ slightly in packaging, e.g. static vs. import-lib
    naming) -- wclibssh.pas's GetProcAddress calls are written against
    the documented C API names, and like every other dynamic binding
    in this project, missing exports are detected and reported by name
    rather than assumed present (same pattern as wcserverrpc.pas's
    DLL-export sanity check). This gets exercised for real only once
    tested against an actual libssh.dll on the target Windows machine
    -- flagged honestly in wcsshd-RELEASE-NOTES.txt's CURRENT STATUS,
    not glossed over.


    2. THE SHARED TELNET GATEWAY (wctelnetgw.pas) ------------------------------------------------------------
    One unit, used by both daemons, doing three things:

    a) OPEN the inner connection: a plain TCP connect (fpSocket/fpConnect,
    same Sockets-unit primitives as every other daemon in this project)
    to Cfg.TelnetHost:Cfg.TelnetPort (default 127.0.0.1:23 -- i.e.
    Wildcat!'s own Telnet listener, assumed running on the SAME
    machine as wcsshd/wcrlogind; a different host is supported via ini
    but not the common case).

    b) SPEAK JUST ENOUGH TELNET on that inner connection to keep it
    working without dragging the outer client into it at all:
    - Any IAC (byte 255) sequence arriving FROM Wildcat! is parsed
    (IAC + one of WILL/WONT/DO/DONT + one option byte, or IAC SB
    ... IAC SE for subnegotiation) and stripped out of what gets
    forwarded to the outer client -- the outer SSH/rlogin session
    only ever sees the actual terminal text/data bytes.
    - The gateway answers a SMALL, fixed set of option negotiations
    itself rather than implementing full RFC 854/855-1091 option
    negotiation generally: DO ECHO -> WONT (the gateway does not
    want Wildcat! also echoing on top of whatever the outer
    client/terminal already does -- avoids double-echo, a classic
    Telnet-gateway bug), DO/WILL SUPPRESS-GO-AHEAD -> agree (WILL/
    DO respectively -- harmless, commonly expected), everything
    else -> WONT/DONT (decline) -- Wildcat!'s Telnet listener is
    expected to fall back to sane line-based behavior for anything
    declined, same as talking to a very old/plain Telnet client.
    - A NAWS-equivalent (terminal window size) is NOT negotiated in
    v1 -- see KNOWN LIMITATIONS; Wildcat! gets whatever its own
    default assumption is, same as a client that never sends NAWS
    today. Terminal TYPE (Telnet TTYPE option) similarly not
    forwarded in v1 -- SSH's own pty-req terminal type / rlogin's
    own terminal-type field (see sections 3-4) are captured by the
    gateway but not yet threaded through as a Telnet TTYPE answer;
    documented as a roadmap item, not silently dropped.
    - IAC 255 255 (a literal 0xFF data byte, escaped per Telnet's
    own rules) arriving from Wildcat! is un-escaped back to one
    0xFF data byte before forwarding outward, and -- if the OUTER
    client ever sends a raw 0xFF byte inward (SSH: basically never,
    since SSH channel data isn't Telnet-framed at all; rlogin:
    possible, it's raw bytes) -- it gets escaped TO IAC 255 255
    before being sent to Wildcat!, so Wildcat!'s own Telnet parser
    never misreads a legitimate data byte as the start of an IAC
    sequence.

    c) BRIDGE bytes bidirectionally for the life of the session: two
    directions, driven the same way regardless of which outer protocol
    is in front -- inner-to-outer bytes go through the IAC filter in
    (b) then out through whatever the caller's "send to outer client"
    callback is (an SSH channel write, or a raw rlogin socket send);
    outer-to-inner bytes go through the escape step in (b) then a
    plain send on the inner Telnet socket. wcsshd.exe drives this loop
    from inside libssh's own event loop (ssh_event_dopoll, section 5);
    wcrlogind.exe drives it with a plain two-thread-or-select loop
    matching wcsmtpd's existing blocking-I/O-per-thread style (each
    rlogin session already gets its own OS thread, same as an SMTP/
    IMAP session, so a second inner reader thread per session is a
    small, well-precedented addition, not a new concurrency model).

    OPTIONAL CREDENTIAL AUTO-FEED (used by wcsshd only, see section 5):
    wctelnetgw.pas accepts an optional (Username, Password: string) pair
    at bridge-start time. If given, the FIRST bytes written to the inner
    connection, before the bidirectional bridge loop starts, are
    Username + CRLF, then Password + CRLF -- i.e. the gateway TYPES the
    already-SSH-authenticated credentials into Wildcat!'s own login
    prompt on the user's behalf, so an SSH user isn't asked to log in
    twice. THIS IS A DOCUMENTED ASSUMPTION, not a verified fact: it
    assumes Wildcat!'s Telnet listener's login sequence is exactly
    "username prompt, then password prompt, in that order, each ended by
    the user's Enter key" with nothing else required first (no "Press
    any key", no ANSI-detection prompt, etc. -- real Wildcat! installs
    commonly have one or more of those). wcsshd-TROUBLESHOOTING.txt will
    need to say plainly that this may need a small adjustable "pre-login
    skip" (a configurable number of lines/prompts to wait through, or a
    configurable expected-prompt substring to wait for before typing the
    username) once tested against a real system, and ships with that as
    a documented, ini-adjustable value rather than a hardcoded assumption
    -- see section 6, [Gateway] LoginPromptWait.
    wcrlogind does NOT get this treatment (see section 4) -- rlogin
    cannot carry a password at all, so there is nothing valid to
    auto-feed beyond the username, and even that is UNAUTHENTICATED
    client-supplied data (see section 4's security note) -- auto-feeding
    an unverified username is far lower-stakes than a password would be,
    but it's still worth being honest that "auto-feed" here means
    something weaker for rlogin than for SSH.


    3. wcsshd.exe -- SSH DAEMON ------------------------------------------------------------
    Listener architecture: IDENTICAL in shape to wcsmtpd.pas's Alpha-4 dual-stack design -- TListenerThread/TListenerFamily ported over
    unchanged, IPv6-preferred/IPv4-fallback, both on by default, each
    listener's bind failure independent/fail-soft. Cfg.ListenPort default
    2222 (NOT 22 -- same "safe by default, non-standard until you've
    tested" rule as every other daemon in this project's family of ports: wcsmtpd 2525, wcimapd 1430). ONE listener per address family in v1 (no separate "implicit" vs upgraded mode the way SMTP/IMAP have -- SSH is encrypted from the very first byte from the client's point of view, so there's only one kind of SSH listener to run, not two).

    Per-connection flow (TSshSession = class(TThread), same base shape as TSmtpSession/a hypothetical TImapSession):
    1. Accept -> ssh_new (libssh session object) bound to the accepted
    socket's fd, ssh_handle_key_exchange (the SSH transport-layer
    handshake -- version exchange, KEX, host key, encryption/MAC
    algorithm negotiation -- all handled INSIDE libssh, wcsshd.pas
    never touches raw KEX/cipher bytes itself, which is the entire
    point of using the library).
    2. Host key: wcsshd.exe needs its own SSH host key (RSA or ed25519,
    a PEM file, generated once with ssh-keygen or libssh's own
    ssh_pki_generate -- documented in wcsshd-INSTALL.txt) -- loaded
    via ssh_bind_options_set(SSH_BIND_OPTIONS_HOSTKEY, path) at
    ssh_bind_new time, once per process, same "load once, use for
    every accepted connection" lifetime as wctls.pas's shared SSL_CTX.
    3. Authentication: libssh's server callback mechanism
    (ssh_server_callbacks_struct.auth_password_function) is invoked
    BY libssh, inside ssh_event_dopoll, whenever the connecting client
    attempts password auth. wcsshd's callback does exactly what
    wcimaprpc.pas's WcRpcLogin already does for wcimapd -- validate
    (username, password) against the real Wildcat! user database. To
    avoid a cross-project unit dependency, wcsshd's project gets its
    OWN small wcauthrpc.pas: a copy of just the auth-relevant slice of
    wcimaprpc.pas (WcRpcInit/Shutdown, WcRpcCreateContext/
    DeleteContext, WcRpcLogin/Logout, TUser/TUserInfo types) -- none
    of the conference/message-store API wcimapd needed, since wcsshd
    never touches mail. Same non-Windows-fails-soft-but-still-compiles
    structure wcimaprpc.pas now has (see wcimapd's own recent
    refactor), for the same Linux-dev-and-test reason.
    Returns SSH_AUTH_SUCCESS only on a real WcRpcLogin success; every
    other case (bad credentials, RPC unavailable) returns
    SSH_AUTH_DENIED with a generic reason, same non-distinguishing-
    error-message discipline as every other login path in this
    project. MaxLoginAttempts-style throttling: libssh itself already
    limits auth tries per connection (AuthTries option); wcsshd sets
    that rather than reinventing it.
    4. Channel + pty + shell: once authenticated, libssh's
    channel_open_request_session_function fires when the client opens
    its session channel; pty_request_function captures the client's
    requested terminal type/dimensions (kept for a future NAWS/TTYPE
    forward, see section 2); shell_request_function (or exec_function,
    if a client asks to run a command instead of a shell -- v1 treats
    ANY exec request the same as a shell request, ignoring the
    specific command string, since there's nothing here for it to
    mean other than "give me the Wildcat! terminal") is where wcsshd
    opens the inner Telnet connection (section 2) and starts the
    bridge, feeding the SAME (username, password) that just passed
    WcRpcLogin as the auto-feed credentials.
    5. Bridge runs (section 2c) until either side closes; wcsshd then
    ssh_channel_close/ssh_disconnect/ssh_free/closes the inner Telnet
    socket, in that order, all best-effort (never raise during
    cleanup, same rule as every Free/Shutdown path elsewhere in this
    project).

    Idle timeout / watchdog: same TWatchdog force-close-the-socket-from- another-thread pattern as wcsmtpd, applied to the OUTER accepted
    socket (closing it unblocks whatever libssh call is currently pending
    on that fd the same way it unblocks a plain fpRecv elsewhere).

    Public-key authentication: OUT OF SCOPE for v1 (see section 6) --
    Wildcat!'s own account model is username+password, with no existing
    concept of "this SSH public key maps to this BBS account" to check a
    key against, so there is nothing correct for wcsshd to do with a
    pubkey auth attempt beyond declining it and telling the client to use password auth instead (ssh_bind_options_set constrains offered auth
    methods so libssh only advertises password to begin with, rather than accepting pubkey attempts and always failing them one at a time).


    4. wcrlogind.exe -- RLOGIN DAEMON ------------------------------------------------------------
    rlogin (RFC 1282) predates any real security model -- on the wire it
    is: client connects (traditionally to port 513, privileged-source-port
    by convention only, not enforced by this implementation), server
    expects four NUL-terminated fields with no framing beyond the NULs:
    client-user-name \0 server-user-name \0 terminal-type/speed \0
    then (v0-era) waits for a single 0-byte ack from the server before the session becomes a raw bidirectional byte stream, or (v1282-clarified) proceeds directly, plus an OOB "window-size" control sequence
    (0xFF 0xFF "ss" + a struct with rows/cols) the client MAY send later
    over the socket's out-of-band/urgent channel for terminal resizing --
    v1 of wcrlogind reads and discards that OOB sequence if present (acknowledging it exists so it doesn't corrupt the data stream) but
    does not forward it to Wildcat! (same NAWS gap noted in section 2).

    THERE IS NO PASSWORD FIELD IN THE RLOGIN PROTOCOL AT ALL. Its original security model was host-based trust (a server-side .rhosts file
    saying "this client host + this client username is allowed in as this
    server username, no password needed") -- something Wildcat! has no equivalent of and wcrlogind does NOT attempt to implement (implementing .rhosts-style trust would mean SKIPPING Wildcat!'s own password check entirely, which is a much worse security posture than requiring it).
    So wcrlogind.exe's rlogin handling is: parse the three NUL-terminated fields, open the Telnet gateway (section 2), auto-feed ONLY the client-supplied username (never a password -- there isn't one) as a convenience so the user doesn't have to retype it, and let Wildcat!'s
    own Telnet login prompt ask for the actual password INTERACTIVELY over
    the (still cleartext, since rlogin itself is never encrypted) bridged connection, exactly as a plain Telnet client would experience.

    SECURITY POSTURE, STATED PLAINLY (goes in wcrlogind-README.txt's own
    "read this first," not buried): wcrlogind.exe provides ZERO
    confidentiality and ZERO real authentication of its own -- it is
    exactly as secure as plain Telnet (because that's what it's a thin
    front-end for), with the one difference that the connecting client's
    claimed username is taken on faith and pre-filled rather than typed.
    It exists purely for legacy rlogin-client compatibility, not as a security-hardened option -- wcsshd.exe is the one to point actual
    "I want this encrypted" traffic at. Given that, wcrlogind's [Listen]
    Address in the shipped ini defaults to 127.0.0.1 (loopback only, NOT 0.0.0.0/::) -- the one deliberate exception to every other daemon in
    this project defaulting to all-interfaces -- so a fresh install can't accidentally expose a cleartext-password login path to the open
    Internet; an admin has to explicitly widen it, with a loud comment in wcrlogind.ini explaining why.

    Listener architecture: same dual-stack TListenerThread/TListenerFamily
    shape as wcsmtpd/wcsshd, port default 5130 (non-standard, vs. the conventional 513, same reasoning as every other port default in this
    project -- doubly so here given the loopback-default point above).


    5. EVENT LOOP / THREADING NOTE (wcsshd specifically) ------------------------------------------------------------
    libssh's server callback model is normally driven by ssh_event_dopoll
    in a loop on ONE session at a time (or via ssh_event_add_session for a
    few sessions multiplexed on one poll loop) -- for wcsshd, the simplest correct mapping onto this project's established "one OS thread per connection" model (used by every other daemon here) is: each
    TSshSession thread creates its OWN ssh_event, adds only its own
    ssh_session to it, and loops ssh_event_dopoll(event, timeout_ms) with
    a short timeout (so the loop can also check the watchdog-driven
    Terminated flag between polls) until the channel/bridge is done or the connection drops -- i.e. one event loop per thread per connection, NOT
    one shared event loop multiplexing every connection (which would be
    more "libssh-idiomatic" for a single-threaded event-driven server, but
    would mean rewriting this project's entire concurrency model just for
    this one daemon). This keeps wcsshd.pas's shape recognizable next to
    every other daemon here (wcsmtpd/wcimapd), at the cost of one extra
    OS thread's worth of overhead per SSH connection, which is the same
    tradeoff this project already made for wcsmtpd/wcimapd over a single-threaded select() server.


    6. CONFIG SCHEMA ------------------------------------------------------------
    wcsshd.ini
    [Listen] Address/EnableIPv4/Address6/EnableIPv6/Hostname/Port
    (Port default 2222, see section 3)
    [SSH] HostKeyFile (PEM, required -- no default, wcsshd logs
    "TLS"-style "SSH not configured" and refuses to start
    any listener without one, since there is no sane
    "plaintext fallback" for an SSH daemon the way TLS-
    optional made sense for SMTP/IMAP), MaxAuthTries
    (default 3, passed straight to libssh), LibSshDllName
    (override, same idea as openssl.pas's DLL-name
    fallback list but user-settable here since libssh's
    own Windows packaging isn't as standardized as
    OpenSSL's -- see section 1).
    [Gateway] TelnetHost/TelnetPort (default 127.0.0.1:23),
    LoginPromptWait (section 2's documented-assumption
    escape hatch -- default 0, meaning "type the username
    immediately"; settable to a small delay in ms, or a
    future version could match a configurable prompt
    substring instead of a blind delay -- v1 ships the
    simple delay-based version and documents the
    limitation).
    [Limits] SessionTimeoutSecs, MaxConnections -- same meaning as
    every other daemon.
    [Options] ConnectionLog -- same structured-log convention.

    wcrlogind.ini
    [Listen] Same fields, Address defaults to 127.0.0.1 (see
    section 4), Port default 5130.
    [Gateway] TelnetHost/TelnetPort -- same as wcsshd's.
    [Limits] / [Options] Same shape as wcsshd's.


    7. CONNECTION LOGGING ------------------------------------------------------------
    Same two-file convention as wcsmtpd/wcimapd (free-text wcsshd<date>.log
    / wcrlogind<date>.log plus a structured wcsshd-conn<date>.log / wcrlogind-conn<date>.log), same [cid=N ip=...] tagging. Structured
    fields, beyond the usual cid/family/peer/listener/duration_ms:
    wcsshd: auth_result (success/denied/error), username (only once
    auth SUCCEEDED -- never logged for a failed/unknown attempt,
    so the log itself can't be used to enumerate valid
    usernames the way a bad error message could), cipher/kex
    (whatever libssh negotiated, via ssh_get_cipher_in/
    ssh_get_kex_algo-equivalent queries once available),
    bytes_in/bytes_out (both directions of the bridge).
    wcrlogind: claimed_username (the client-supplied, UNVERIFIED rlogin
    username -- explicitly labeled "claimed" in the field name
    itself so nobody mistakes it for an authenticated identity
    when reading the log), bytes_in/bytes_out.
    "Detailed connection logging", as asked for, means this structured log
    plus the free-text log's own connect/auth-attempt/disconnect narrative
    -- not packet-level tracing of the bridged Telnet session's actual
    terminal content, which is exactly the kind of thing a BBS user would
    NOT expect logged (their session transcript, potentially including
    whatever they type at Wildcat!'s own password prompt on the inner connection) -- wcsshd/wcrlogind log connection METADATA, never the
    bridged data stream's contents. This boundary gets stated explicitly
    in both README.txt files.


    8. WHAT'S DELIBERATELY OUT OF SCOPE FOR v1 ------------------------------------------------------------
    - SSH public-key authentication (section 3) -- password only.
    - NAWS / Telnet TTYPE forwarding through the gateway (section 2) --
    Wildcat! sees whatever its own no-negotiation default is.
    - rlogin's OOB window-size resize sequence is drained, not acted on.
    - Any host-based trust model for rlogin (section 4) -- deliberately
    never implemented, not a gap.
    - SSH host-key rotation/multiple host keys (RSA + ed25519 offered
    together) -- v1 loads exactly one HostKeyFile.
    - A configurable per-daemon Wildcat! "which Telnet node/port" beyond
    a single TelnetHost:TelnetPort pair -- no multi-node load
    balancing across several Telnet listeners.



    ======================================================
    ==
    ======================


    * Origin: Over the Brink : Grand Island, NY - brinkbbs.org (1:261/20)