Skip to content

feat(serial): serve the extension port UART over the network (ser2net or web console) - #1

Closed
conallob wants to merge 2 commits into
devfrom
claude/lucid-ramanujan-mgonmf
Closed

conallob wants to merge 2 commits into
devfrom
claude/lucid-ramanujan-mgonmf

Conversation

@conallob

@conallob conallob commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Closes jetkvm#1520

Summary

With the Serial Console extension loaded, the extension port UART (/dev/ttyS3) can now be reached over the network. A new Network Access setting has three options: Disabled (the default), ser2net (TCP) or Web console. This turns a JetKVM, or a rack of them, into a serial console server without SSH/developer mode or an external daemon.

ser2net mode: a native Go TCP listener. The port is configurable (default 2217), and so is the protocol:

  • Raw TCP: bytes pass straight through (socat -,raw,echo=0 tcp:<ip>:2217).
  • Telnet (RFC 2217): option negotiation (BINARY/ECHO/SGA/COM-PORT-OPTION), IAC escaping, and IAC BRK sends a real serial break (SysRq). RFC 2217 clients such as pyserial's rfc2217:// can set baud rate, data bits, parity and stop bits for the length of their session. The configured settings come back when they disconnect.
  • There is no authentication, and the UI warns about this. The listener honours the "loopback only" setting (same bind logic as the web server). Ports 22, 80 and 443 are rejected.

Web mode: GET /serial/ws sits behind the existing auth middleware and cookie. It uses the library's default same-origin check (no InsecureSkipVerify), so another site can't use the auth cookie to reach the UART. Frames carry raw UART bytes both ways. A new device-only route, /serial, is a full-page xterm console on top of it, so the UART works in a plain browser tab without a WebRTC KVM session. The extension panel has an "Open Web Console" button.

Shared design

  • Everything goes through the existing SerialMux, which stays the only reader and writer of the port. Network clients, the in-session WebRTC console and quick buttons don't race on the fd (this answers the concurrency question in the issue). Network input is echoed into the in-session console like typed input.
  • UART reads are fanned out by a hub that never blocks the reader. A client that falls 256 reads behind is disconnected, so it can't stall the other clients.
  • Concurrent clients are capped at 1 by default, configurable up to 8; for example, one interactive session plus log capture. Extra clients are refused with a message (TCP) or close code 1013 (WebSocket).
  • Changing the mode, port or protocol, or unloading the extension, closes the listener and drops the connected clients. Re-applying unchanged settings keeps the sessions.
  • A new RPC, getSerialNetworkStatus, reports whether it is running, the listen address, any bind error and the connected clients. The UI polls it.
  • The settings live in serialSettings.json with the other Serial Console settings. Older files get the defaults, so the feature stays disabled.

Prometheus metrics (on the existing /metrics endpoint)

Metric Type Labels
jetkvm_serial_port_open gauge 0/1 device
jetkvm_serial_port_info info (1) device, baud_rate, data_bits, parity, stop_bits, extension
jetkvm_serial_port_baud_rate gauge device
jetkvm_serial_network_info info (1) mode, protocol, listen_address
jetkvm_serial_network_up gauge 0/1 mode
jetkvm_serial_network_max_clients gauge none
jetkvm_serial_network_clients gauge transport
jetkvm_serial_network_bytes_total counter transport, direction

The port and network metrics come from a collector that runs at scrape time. go.bug.st/serial can't read back a port's mode, so every open, close and SetMode now goes through setSerialPortMode and a small tracker. The metrics then show the settings the port is actually running at, including a session-only RFC 2217 change, and the info series disappears rather than going stale when the port closes. Example fleet query: count by (mode) (jetkvm_serial_network_up == 1).

Small fixes this change needs

  • The stored Serial Console settings are now applied at boot when the extension is loaded. Before, they were only applied once the UI next opened the extension, so a network client after a reboot would have seen 115200 8N1.
  • A missing serialSettings.json no longer returns an error from getSerialSettings; the defaults apply.
  • SerialMux.Enqueue no longer blocks forever once the mux has been closed.
  • Setting the port mode is skipped when /dev/ttyS3 failed to open, instead of panicking. This also covers the ATX and DC extension mounts.

Not included (possible follow-ups): mDNS/DNS-SD advertisement of the ser2net port for fleet discovery, and RFC 2217 NOTIFY-LINESTATE/MODEMSTATE. The extension port has no modem lines, so SET-CONTROL requests are acknowledged but have no effect.

How this answers the issue discussion

  • Lost reads with microcom (@justarandomgeek): jetkvm_app already holds the fd and wins most read()s, so a second process only sees fragments. This PR doesn't add another reader; network clients subscribe to SerialMux's one reader, so every client gets every byte.
  • "ConsoleBroker seems to handle things in lines": correct. The network paths tap SerialMux before ConsoleBroker, so ser2net and web clients get the raw byte stream with no line splitting, RX:/TX: labels or control-character normalisation. Terminals, expect scripts and ANSI/curses output work unchanged. The broker still feeds the in-session WebRTC console as before.
  • "Hook into the existing auth/session model rather than a separate unauthenticated listener?": the web mode does exactly that (same auth cookie and middleware as the rest of the device API, same-origin WebSocket). The ser2net mode is for tooling that can only speak TCP/RFC 2217, so it's a separate, off-by-default choice with a warning in the UI, and it respects loopback-only.
  • Aruba CX USB-C console: out of scope. Those are device-mode USB-serial ports, so JetKVM would need to act as a USB host. Agreed in the issue to track separately; the network server here is transport-agnostic and could front such a port later.

Testing

  • serial_server_test.go and serial_metrics_test.go (no build tag, so they run on amd64 CI) cover:

    • the telnet/RFC 2217 codec: IAC escaping, CR NUL handling, negotiation without loops, commands split across reads, line settings and rejected modes, BREAK
    • settings validation
    • disconnecting a client that falls behind
    • raw TCP in both directions, the client limit, stop and no-op re-apply
    • a full RFC 2217 session, including restoring the line settings
    • the WebSocket in both directions, the client limit and closing on a mode change
    • the collector's exposition output, checked line by line against the expected text for a closed port and for an open, reconfigured one, plus promlint on the metric names and help text

    The server tests pass 30 runs in a row under -race.

  • go vet, golangci-lint (repo config) → 0 issues.

  • UI: tsc, oxlint (0 new warnings), oxfmt.

  • Smoke-tested /serial in Chromium against a mock backend: it connects, shows UART output, and typed input reaches the backend and is echoed back.

  • ⚠️ Not yet run on hardware: nothing has been tried against a real /dev/ttyS3, and make test_e2e hasn't been run.

Checklist

  • Ran make test_e2e locally and passed. Not run (needs a device).
  • Linked to issue(s) above by issue number
  • One problem per PR (the small fixes above are ones this feature depends on)
  • Lints pass; CI green (local lints pass; CI pending)
  • Tricky parts are commented in code

🤖 Generated with Claude Code

https://claude.ai/code/session_01C3BNwaCJdCFHEuH7EZcR7j

With the Serial Console extension loaded, the UART on the extension
port can now also be reached over the network, in one of two modes:

- ser2net: a TCP listener (default port 2217), either raw or telnet
  with RFC 2217 so clients such as pyserial's rfc2217:// can set the
  baud rate, data bits, parity and stop bits. A client's line
  settings last only for its session. IAC BREAK sends a serial break.
  There is no authentication, and the UI says so. The listener
  honours the loopback-only setting.
- web: an authenticated WebSocket at /serial/ws (the usual auth
  cookie, same-origin only) behind a standalone full-page console at
  /serial, so the UART is usable without a WebRTC KVM session.

Both modes go through the existing SerialMux, which stays the only
reader and writer of /dev/ttyS3. UART reads fan out to network
clients through a hub that never blocks the reader; a client that
falls behind by 256 reads is disconnected. Concurrent clients are
capped (1 by default, up to 8). getSerialNetworkStatus reports the
listener, errors and connected clients, and Prometheus exports client
counts and relayed bytes.

The settings live in serialSettings.json with the other Serial
Console settings; older files get the new defaults (disabled). Also:

- The stored Serial Console settings are now applied at boot when
  the extension is loaded, not when the UI next opens it.
- A missing serialSettings.json no longer fails getSerialSettings.
- SerialMux.Enqueue no longer blocks forever once the mux is closed.

Refs jetkvm#1520

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3BNwaCJdCFHEuH7EZcR7j
A collector reports the extension port UART's state at scrape time:

- jetkvm_serial_port_open{device}
- jetkvm_serial_port_info{device,baud_rate,data_bits,parity,stop_bits,extension}
- jetkvm_serial_port_baud_rate{device}
- jetkvm_serial_network_info{mode,protocol,listen_address}
- jetkvm_serial_network_up{mode}
- jetkvm_serial_network_max_clients

go.bug.st/serial can't read back the port's mode, so every open, close
and SetMode now goes through setSerialPortMode and a small tracker.
The metrics then show what the port is actually running at, including
settings an RFC 2217 client applied for its session, and the info
series is dropped rather than left stale when the port closes.
setSerialPortMode also checks for a port that failed to open, which
the ATX and DC extension mounts did not.

The collector test uses client_golang's testutil, which adds
kylelemons/godebug as an indirect module requirement (already in
go.sum).

Refs jetkvm#1520

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3BNwaCJdCFHEuH7EZcR7j
@conallob

Copy link
Copy Markdown
Owner Author

Superceded by jetkvm#1660 to upstream

@conallob conallob closed this Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extension port UART: support RFC2217/raw-TCP serial passthrough (ser2net-style) in addition to browser terminal

2 participants