Skip to content

ethernetgateway v0.6.4

Choose a tag to compare

@github-actions github-actions released this 14 Jul 18:56

Added

  • Serial ports gain a third mode: Kermit Server. Alongside Modem
    (AT Command) Mode
    and Telnet-Serial Mode, each serial port (A/B) can
    now run as an always-on Kermit server: as soon as the port is enabled it
    listens for Kermit packets directly on the wire — no AT commands, no
    dialing, no menu. It is the same server ATDT KERMIT reaches from the
    modem emulator, but always on and with no AT layer; received files land
    in transfer_dir, and it re-arms after every FINISH/BYE so the wire stays
    a live server. The port reopens automatically if the device disappears
    (matching modem mode). Selectable from the GUI Mode dropdown, the web
    config's per-port "More…" popup, and the telnet per-port T toggle
    (which now cycles Modem → Console → Kermit). Persists as
    serial_a_mode / serial_b_mode = kermit. Auth and the telnet menu are
    bypassed by design — enable only on trusted serial lines (same posture as
    allow_atdt_kermit).
  • YMODEM receives multi-file batches (sb file1 file2 …). The receiver
    previously ran the end-of-batch handshake right after the first file's EOT,
    so a batch sender lost every file after the first (and could hang waiting for
    the receiver). It now reads the next block 0 at each EOT — a named block 0
    starts the next file, the null block 0 ends the batch — and returns every
    file. The first file keeps the user-entered name; files 2..N use the sender's
    (sanitized) block-0 name, saved atomically like ZMODEM/Kermit batches. A
    corrupt inter-file block 0 is NAK-retried (bounded), the batch is capped at
    1000 files, and a non-UTF-8 file name is received under a generated name
    rather than truncating the batch.
  • Weather works worldwide, not just US zip codes. The Weather menu now
    accepts any city name or postal code (London, SW1A 1AA, Zürich,
    62051), percent-encoding the query so spaces and non-ASCII are safe. A
    City, Country or City, Region qualifier disambiguates common names
    (London, GB vs London, Ontario; Paris, France vs Paris, Texas), and
    the matched country is shown. A new weather_units setting — auto
    (default: Fahrenheit/mph for the US, Celsius/km/h elsewhere), us, or
    metric — controls display units; press U on the weather screen to cycle
    them in place (no re-fetch). Wired into the telnet Other-Settings menu, the
    web config page, and the GUI. The config key weather_zip is renamed to
    weather_location; an existing weather_zip value migrates automatically
    on first load, and any saved location persists across sessions as before.
  • Configurable desktop GUI display scale (gui_zoom). The console window
    now honors a gui_zoom setting: auto (default) follows the monitor's own
    scale factor as before, while a number (e.g. 1.0, 1.25, 0.8) pins the
    window's pixels-per-point absolutely so a display that reports an inflated
    DPI no longer renders the GUI oversized. Selectable as "Display scale" from
    the GUI's Server → More panel and the web config's Server → More page
    (Auto / 75% / 100% / 125% / 150% / 200%), and clamped to 0.5–3.0.
  • Show the file being downloaded on the SELECT PROTOCOL screen. The
    download protocol picker now displays the file name (truncated to the
    terminal width) and byte size above the protocol list, so the user can
    confirm the right file before choosing a protocol.
  • Make directories from the telnet file-transfer menu. A new M option
    creates a subdirectory inside the current transfer working directory (the
    name is validated like a filename — a single component, no .. or /), then
    asks whether to make it the working directory.
  • Weather falls back to MET Norway when Open-Meteo is unreachable. If the
    Open-Meteo forecast host can't be reached, the Weather menu now automatically
    retries the forecast against MET Norway (api.met.no Locationforecast 2.0 —
    free, no API key, independent infrastructure), reusing the coordinates
    already geocoded via Open-Meteo (worldwide coverage, so the fallback works for
    any location). MET's data is kept in metric and converted to your chosen units
    at display time, and its symbol codes mapped to descriptions; you only see an
    error if both providers fail.
  • Wait for the receiver before starting a Kermit download
    (kermit_wait_for_receiver, default on).
    A Kermit transfer is
    receiver-driven at the start — the receiving side sends a NAK to solicit the
    sender's Send-Init (Frank da Cruz, Kermit Protocol Manual §4). The gateway
    now holds its Send-Init until that poke arrives and then sends exactly one, on
    both interactive downloads and Kermit server GET responses. A client that
    never pokes (e.g. C-Kermit) falls through a short bounded wait and gets an
    unprompted Send-Init as before. Wired into the telnet Kermit-settings menu
    (G), the web config page, and the GUI.
  • Verbose Kermit receive-path logging. With verbose = true, the Kermit
    upload/receive path now emits periodic per-packet progress plus a
    per-file summary (bytes, packets, block-check type), matching the diagnostic
    style of the XMODEM/YMODEM/ZMODEM paths. Off by default.

Changed

  • Warning popups are now dark red (GUI and web). Security/confirmation
    warnings previously looked identical to ordinary popups, so it wasn't obvious
    the modal was blocking the next click and had to be acknowledged. The GUI's
    four warning popups (ATDT-KERMIT, Kermit-server, disable-IP-safety,
    master-needs-SSH) now use a dark-red panel + red border. On the web, the
    native confirm()/alert() warnings (disable web server, change web port,
    master-needs-SSH) are replaced with matching dark-red modal dialogs whose
    overlay blocks the form until the operator chooses Continue/Cancel. The web
    also gains the enable-guard warnings it was missing versus the GUI —
    Disable IP Safety, Kermit Server, and Allow ATDT KERMIT now raise
    the same red confirmation before they take effect.
  • ZMODEM batch receive is capped at 1000 files (MAX_BATCH_FILES, matching
    YMODEM and Kermit), so a peer that streams endless files can't grow the
    in-memory batch without bound; the receiver cancels and errors past the cap.
  • Config UI: tidier frames via "More" popups. The web config page now keeps
    the Master/Slave relay settings under the Server frame's More… popup
    (they were a separate card), matching the GUI and returning the page to six
    frames. The AI Chat, Browser, and Weather frame (both web and GUI) is now
    three rows — API key and homepage on the frame, with a More… button that
    opens the weather location and units.
  • Weather fetch fails fast with a clearer message. The Open-Meteo request
    now uses a 5 s connect timeout (was a single 15 s global) and retries once on
    a transient transport failure, so an unreachable/blocked forecast host no
    longer hangs the Weather menu for 15 s. Errors are distinguished:
    "Not found - try 'City, Country'." (no geocoder match) vs "Weather service
    unreachable. Try again later." (network/host down) vs "Weather service
    returned bad data." (parse).

Fixed

  • Serial Kermit Server Mode transfers at full speed. Kermit Server Mode was
    far slower than the same server launched from the File Transfer menu. The
    bridge that pumps bytes between the wire and the Kermit server
    (run_console_bridge) drained its outbound queue only between wire reads,
    and Kermit is stop-and-wait — so while the gateway composed each reply the
    wire sat idle and the bridge blocked out the full wire-read timeout before
    writing that reply, adding a fixed delay to every gateway-originated packet
    (each ACK when receiving, each DATA when sending — hence the slowdown in both
    directions). The menu/ATDT KERMIT server never had this because its pump
    produces and flushes the reply in the same iteration that consumed the
    request. Server Mode now uses that same inline pump (run_kermit_bridge_inline,
    modeled on the modem online-mode path) instead of the decoupled two-thread
    bridge, so a reply leaves the wire the moment it's produced. (The interactive
    Serial Gateway console still uses run_console_bridge, whose backpressure
    design is intentional there.)
  • Kermit server uploads no longer drop a file on a name collision. When an
    uploaded file's name already exists in transfer_dir, every Kermit-server
    receive path (the telnet-menu server, the standalone TCP listener, and the
    new serial Kermit Server Mode / ATDT KERMIT) now renames the incoming file
    DOS/CP-M-Kermit style instead of skipping it — the base name is numbered
    within 8 characters the way CP/M Kermit clients (e.g. kercpm3) do on a
    download collision: abcdefgh.txtabcdefg0.txtabcdefg9.txt
    abcdef10.txt, and a shorter name such as hi.txthi0.txt. The original
    file is never overwritten, and a verified resume still replaces its own
    partial in place. (The pre-existing telnet-menu and TCP-listener paths
    previously skipped such a collision with an "already exists" note.)
  • ATDT KERMIT uploads now actually save to disk. The serial Kermit-server
    dial path passed a no-op file-commit hook to the server, but the Kermit
    receiver only buffers uploaded files in memory and relies on that hook to
    persist them — so a client send/put over ATDT KERMIT completed on the
    wire but left nothing in transfer_dir (downloads/get, which read from
    disk, were unaffected). Both the always-on serial Kermit Server Mode (new,
    above) and the ATDT KERMIT dial now commit each received file with the same
    filename / subdir path-safety validation as the telnet and TCP-listener
    Kermit server paths.
  • Kermit server GET is now case-insensitive. A client requesting a file in
    a different case than it is stored on disk — CP/M clients such as kercpm3
    uppercase filenames — no longer fails "File not found" and burns a retry
    re-requesting under another case. The server prefers an exact match, then
    falls back to a case-insensitive match among the transfer directory's direct
    entries, so the path-traversal protection is unchanged.
  • Kermit server downloads no longer provoke spurious retransmissions on
    vintage receivers.
    The server was sending its Send-Init unsolicited; a
    receiver-driven client (e.g. kercpm3 on CP/M) pokes with a NAK to solicit
    it, that poke crossed our Send-Init on the wire, and we resent it — delivering
    a duplicate the client tallied as a retry. The server now waits for the poke
    and answers with a single Send-Init. Combined with the case-insensitive fix
    above, this cuts the retry count such clients report on a clean download from
    2–3 down to the single, unavoidable initiating NAK that the Kermit
    receiver-driven start requires (uploads read 0 — there the client is the
    sender and never pokes). Documented in usermanual.html and kermit.html.
  • Kermit sender no longer cascades retransmits on a duplicate ACK. On a
    serial download to a hardware CP/M client (kercpm3 / Kermit-80), the
    receiver-driven start makes the gateway send its Send-Init twice, so the
    client's first ACK arrives duplicated — and the sender treated that stale,
    already-satisfied ACK by retransmitting the current packet, whose re-ACK
    became the next stale ACK: a self-perpetuating cascade that showed as a burst
    of dozens of "retries" before the transfer settled and ran clean. A
    stale/duplicate ACK is now discarded without retransmitting (the sender keeps
    reading for the ACK that advances the window); a retransmit still fires only
    on a NAK for the current sequence or a read timeout.
  • Kermit remote dir / remote help replies no longer staircase on CP/M
    clients.
    The server built those listings with bare-LF line endings;
    C-Kermit on Unix masks this via the tty's ONLCR, but a hardware CP/M client
    (Kermit-80) does no translation, so each line stepped down without returning
    to column 0. Both bodies are now CRLF-encoded before transfer (existing CRLFs
    left intact; TYPE's verbatim file bytes are deliberately untouched).
  • Web browser surfaces the real error when the AI chat API rejects a
    request.
    The Groq client treated every non-2xx response as an opaque
    transport error (http status: 401) and discarded the JSON body, so its
    code to extract Groq's descriptive error.message (e.g. "Invalid API Key",
    rate-limit text) never ran. It now reads the body on error responses and
    reports Groq's own message.
  • ZMODEM downloads are no longer throttled to ~5 KB/s on fast links. When
    reading a hex header the receiver drained up to three trailing bytes (CR, LF,
    and an optional XON), but Forsberg's zsendhdr omits the XON for ZACK and
    ZFIN frames — so on those the drain blocked the full 200 ms per frame
    waiting for an XON that never comes. Because our sender ACK-gates every
    1 KB subpacket (ZCRCQ → read ZACK), that phantom wait capped throughput
    near one subpacket per 200 ms regardless of link speed. The receiver now
    waits for the third trailing byte only for frame types that actually carry
    it. No wire bytes change; slow retro links are unaffected (a subpacket's own
    transmission already dwarfs 200 ms there).
  • Plain XMODEM sends no longer report a false failure at xmodem_max_retries = 1. A Forsberg-compliant receiver NAKs the first EOT to verify
    end-of-file and ACKs only the resent one (our own receiver does this), so
    completing the handshake requires at least two EOT attempts. The send-side
    EOT loop was bounded by xmodem_max_retries, so at the minimum setting of
    1 it sent a single EOT, took the expected verification NAK as failure,
    and reported an error on a transfer that had actually succeeded. The EOT
    budget now floors at 2; a receiver that ACKs the first EOT still returns on
    the first pass, so the common case is unchanged.
  • Serial dial-out stays responsive to shutdown and config restarts. When
    an ATDT target resolved to several unreachable addresses, the modem tried
    each in turn and could block the serial thread for (address count × the S7
    timeout) — during which a server shutdown or a per-port config restart was
    stalled. The dial loop now checks the shutdown/restart flags between address
    attempts and bails with NO CARRIER. The peer-dial answer-wait is likewise
    clamped to the same 60 s ceiling ATDT uses, so a large S7 can't pin the
    caller's port for up to 255 s while a local peer rings.
  • SIGHUP reloads instead of shutting the service down. SIGHUP was wired
    to the same shutdown flag as SIGINT/SIGTERM, so systemctl reload cleanly
    stopped the gateway — and because the exit was clean (code 0), Restart=on-failure
    did not bring it back, leaving the service down. SIGHUP now arms the
    restart/reload path (re-reading config) instead of exiting, matching the
    shipped systemd unit's ExecReload.
  • Kermit CAPAS long-packet / sliding-window bits corrected. The capability
    mask had LONGPKT and SLIDING transposed versus the canonical layout
    (C-Kermit ckcmai.c: long = 0x02, sliding = 0x04). Gateway↔gateway sessions
    and the test suite were self-consistent and unaffected, but a third-party peer
    advertising one capability without the other (e.g. G-Kermit, MS-DOS Kermit —
    long packets, no windows) was misread, desyncing the rest of the Send-Init.
    Now fixed and pinned against the C-Kermit source.
  • Serial console/modem no longer busy-loops at 100% CPU on a port EOF
    (macOS).
    run_console_bridge and command_mode_tick treated a
    zero-length read (Ok(0)) as "no data" and re-polled immediately. The port
    is opened with a read timeout, so an idle read is Err(TimedOut)Ok(0)
    actually means the device closed (e.g. a PTY master after its slave exits,
    where loss surfaces as EOF rather than the Err(EIO) a real ttyUSB gives).
    Both now treat it as a disconnect (reopening in modem mode), matching the
    online-path readers. Inert on Linux.
  • ZMODEM receiver no longer emits the sender's OO trailer. Per Forsberg
    §8.4 the receiver replies ZFIN and then reads the sender's OO; emitting
    our own was a role inversion (harmless in practice — the peer had already
    sent its own and exited).

Security

  • SSH server refuses to overwrite an unreadable host key. If the host-key
    file existed but failed to parse (e.g. truncated by a full disk), the server
    silently generated a new key and wrote it over the old one — changing the
    server's SSH identity and tripping every client's "REMOTE HOST
    IDENTIFICATION HAS CHANGED" warning (and potentially clobbering a merely
    truncated, recoverable key). It now refuses to start the SSH server in that
    case, leaving the file untouched for the operator to restore or remove, the
    way sshd treats a bad host key. A missing key file is still generated
    normally on first run.
  • Punter receive can no longer be hung by a flood of empty blocks. A peer
    that streamed valid-checksum, non-final, zero-payload blocks would spin the
    receive loop forever: an empty block never grows the output (so the file-size
    cap never trips) and passes the checksum (so the bad-block cap never trips).
    A conformant C1 sender emits exactly one header-only block per phase (block 0,
    which only announces block 1's size), so the receiver now bounds the number
    of accepted empty non-final blocks and gives up on a peer that exceeds it.
  • Text-mode web browser can no longer be crashed by a deeply-nested page.
    A page whose HTML nested tags tens of thousands deep (e.g. unclosed <div>s,
    well under the 1 MB body cap) parsed into a DOM so deep that the browser's
    recursive title/form extractors overflowed the worker-thread stack and
    aborted the entire gateway process (all telnet/SSH sessions), a
    remotely-content-triggered denial of service. The browser now rejects a
    document nested deeper than 512 element levels ("Page is too deeply nested to
    render.") before those walks run.
  • Refreshed dependencies to clear RustSec advisories. cargo update
    moved aes (yanked) → 0.9.1, memmap2 (RUSTSEC-2026-0186 unsound) → 0.9.11,
    dropped anyhow (RUSTSEC-2026-0190 unsound), and bumped the egui/eframe stack
    to 0.34.3 and russh to 0.60.3. The two quick-xml DoS advisories
    (RUSTSEC-2026-0194/0195) are waived in .cargo/audit.toml: quick-xml is a
    build-time proc-macro dependency (wayland-scanner) that parses trusted
    Wayland protocol XML at compile time — it is not in the shipped binary and the
    gateway does no runtime XML parsing, so neither DoS path is reachable.
  • Web config UI: enabling login no longer widens IP exposure. The
    private-IP allowlist now applies whenever disable_ip_safety is off,
    regardless of whether "Require Login" is on. Previously, enabling security
    dropped the allowlist — accepting any source IP, gated only by
    cleartext-HTTP Basic auth on a page that renders the login password and Groq
    API key into form fields. Login-gated access from arbitrary IPs is now an
    explicit disable_ip_safety = true opt-in. (The telnet listener is
    unchanged and intentionally still opens to any IP under security_enabled
    it echoes no secrets and is the retro-hardware access path.)
  • Relay onward-dial now requires the master's allow_peer_dial. A slave's
    Model-B onward-dial — asking the master to open an outbound TCP connection to
    an arbitrary external host:port — was gated only by gateway_role = master
    • master_accept_relays. It now also requires the master's allow_peer_dial
      (the same opt-in that already governs peer-dial), closing an authenticated
      SSRF/pivot/port-scan primitive available to any holder of the shared
      credentials.
  • Text-mode web browser no longer re-sends a form POST over cleartext. On a
    TLS error an HTTPS form submission was transparently retried over http://,
    re-sending the form fields (possibly credentials) in the clear before the
    downgrade notice was shown — an active MITM could force a TLS error to strip
    encryption and capture the body. A POST is now refused on a TLS error;
    idempotent GET page loads still downgrade with a warning banner.
  • Web-browser page text is sanitized before it reaches the terminal. Remote
    content (HTML, text/plain, and gopher) now passes through the same
    sanitize_for_terminal filter as the AI-chat path, stripping ANSI/CSI/OSC
    escape sequences a malicious or MITM'd page could use to manipulate a retro
    terminal. Link-number sentinels are preserved. Coverage also includes the
    page URL (a gopher selector can carry escapes into the status line) and all
    rendered form text — form/field labels, Select option text (sanitized in
    place), and displayed field values (sanitized at display time so the
    submitted value stays byte-exact) — which the form view/edit UI prints.
  • An unreadable existing config is no longer reset to insecure defaults. If
    egateway.conf is present but can't be read (non-UTF-8, corruption, or a
    permission/I/O error), the gateway now refuses to start rather than
    overwriting it with security_enabled = false / password changeme. Config
    and dial-map saves also fsync before the atomic rename, so a crash or power
    loss between write and rename can't publish a truncated file (which would
    then trip the new fail-loud guard on the next start). An existing file that
    parses to no recognized settings (empty, whitespace-only, or comments-only
    — e.g. an external truncation to zero bytes) is likewise treated as
    unreadable rather than as "all defaults," so it can't silently downgrade the
    gateway either.
  • Startup warns on the wide-open combination. disable_ip_safety = true
    together with security_enabled = false — an unauthenticated gateway
    reachable from any IP — now emits a startup warning, matching the guard the
    GUI/telnet toggle popups already apply.
  • ZMODEM: bound control-frame floods that make no forward progress. The
    45 s negotiation deadline and per-read timeout bound silence, not a peer
    that streams valid control frames. The receiver now bounds progressless
    control frames (ZRQINIT/ZSINIT/ZFREECNT/ZSTDERR/unknown), reset by a real
    ZFILE, and the sender bounds stale-ZRINIT drains per ZFILE attempt, so a
    chatty-but-progressless peer can no longer keep a session alive indefinitely.
  • Telnet: the session subnegotiation reader is now slowloris-bounded.
    read_subneg_payload bounds each read with SB_DRAIN_TIMEOUT, so a peer that
    sends IAC SB then stalls without IAC SE can no longer pin the session and
    its max_sessions slot when idle_timeout_secs = 0. This matches the two
    gateway-path SB readers, which were already bounded.
  • Serial: the direct peer-dial ring is now shutdown/restart-aware. While an
    ATD <Port>@<ip> to a local modem port was ringing unanswered, the caller's
    serial thread parked in a blocking wait and ignored shutdown/restart for up to
    the clamped S7 window. The ring now races a shutdown/restart poll (the same
    idiom the modem-port announcer uses), so a config restart or shutdown is
    responsive within ~100 ms.