Skip to content

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

Open
conallob wants to merge 3 commits into
jetkvm:devfrom
conallob:claude/lucid-ramanujan-mgonmf
Open

conallob wants to merge 3 commits into
jetkvm:devfrom
conallob:claude/lucid-ramanujan-mgonmf

Conversation

@conallob

Copy link
Copy Markdown

Closes #1520

Summary

With the Serial Console extension loaded, the extension port UART (/dev/ttyS3) can be reached over the network. A new Network Access setting offers Disabled (default), ser2net (TCP) or Web console. A JetKVM, or a rack of them, can then act as a serial console server without developer mode, SSH or an external daemon.

ser2net mode: a TCP listener built into the app. 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): telnet option negotiation, IAC escaping, and IAC BRK sends a real serial break (e.g. SysRq). RFC 2217 clients such as pyserial's rfc2217:// can change baud rate, data bits, parity and stop bits for their session; the configured settings come back when they disconnect.
  • There is no authentication, and the UI warns about this. The listener honours "loopback only" (same bind logic as the web server) and rejects ports 22, 80 and 443.

Web mode: GET /serial/ws uses the existing auth middleware and cookie, with the WebSocket library's default same-origin check, so another site can't use the cookie to reach the UART. Frames carry raw UART bytes in both directions. 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.

Design

  • One reader. Everything goes through the existing SerialMux, which stays the only reader and writer of the port. This fixes the problem described in the issue, where a second process (microcom) lost most reads to jetkvm_app. Network clients subscribe to that one reader, so each client gets every byte.
  • Raw bytes, not lines. Network clients read from SerialMux before ConsoleBroker, so they get no line splitting, RX:/TX: labels or control-character rewriting. Terminals, expect scripts and full-screen ANSI output work unchanged. The in-session WebRTC console is unaffected.
  • Slow clients can't stall the port. A hub fans out UART reads without ever blocking the reader; a client that falls 256 reads behind is disconnected.
  • Client limit. 1 by default, configurable up to 8 (e.g. an interactive session plus log capture). Extra clients are refused with a message (TCP) or close code 1013 (WebSocket).
  • Lifecycle. Changing mode, port or protocol, or unloading the extension, closes the listener and drops clients. Saving unchanged settings keeps existing sessions.
  • Settings live in serialSettings.json with the other Serial Console settings. Older files get the defaults, so the feature is off after upgrade.
  • Status RPC. getSerialNetworkStatus reports whether it is running, the listen address, any bind error and the connected clients. The UI polls it.

Prometheus metrics

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

Port and network metrics are computed when Prometheus scrapes. go.bug.st/serial can't report a port's current settings, so every open, close and mode change now goes through setSerialPortMode, which records them. The metrics show what 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.

Related fixes

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

Out of scope

  • The Aruba CX USB-C console from the issue discussion. That port is a USB-to-serial adapter on the switch side, so JetKVM would need to act as a USB host; better tracked separately.
  • mDNS/DNS-SD advertisement of the ser2net port for discovering units across a fleet.
  • RFC 2217 line-state and modem-state notifications. The extension port has no modem lines, so SET-CONTROL requests are acknowledged but have no effect.

Testing

  • serial_server_test.go and serial_metrics_test.go have no build tag, so they run in amd64 CI. They cover:

    • the telnet/RFC 2217 handling: IAC escaping, CR NUL, negotiation without loops, commands split across reads, line settings, rejected modes, BREAK
    • settings validation
    • disconnecting a client that falls behind
    • raw TCP in both directions, the client limit, stop, and saving unchanged settings
    • a full RFC 2217 session, including restoring line settings
    • the WebSocket in both directions, the client limit, and closing on a mode change
    • the metrics output for a closed port and for an open, reconfigured one, plus promlint

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

  • go vet and golangci-lint (repo config): 0 issues.

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

  • Tried /serial in Chromium against a mock backend: it connects, shows UART output, and typed input round-trips.

  • Not yet tested on hardware against a real /dev/ttyS3, and make test_e2e hasn't been run.

Checklist

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

@CLAassistant

CLAassistant commented Sep 27, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

conallob and others added 3 commits September 27, 2026 02:10
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
CI runs `go test ./...` on an amd64 runner. Every existing test in the
root package is tagged linux && arm, so the root package never had to
link there; the new untagged serial tests made it link against the
device-only libjknative and liblvgl, and the "Run tests" step failed:

    /usr/bin/ld: cannot find -ljknative
    FAIL github.com/jetkvm/kvm [build failed]

The telnet/RFC 2217 codec, the UART fan-out hub, the TCP and WebSocket
server and the metrics collector don't need the device, so they move
to internal/serialnet with their tests, which then run in CI like the
other internal packages. The root package keeps the device wiring:
settings, the SerialMux feed, port mode changes, RPCs, the /serial/ws
handler and collector registration. The server's hooks into the port
are passed in as serialnet.Deps. No behaviour change.

Refs jetkvm#1520

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3BNwaCJdCFHEuH7EZcR7j
@conallob
conallob force-pushed the claude/lucid-ramanujan-mgonmf branch from 2a794ad to 06f4026 Compare September 27, 2026 01:10
@justarandomgeek

Copy link
Copy Markdown

is this in a state you'd like someone to try it on real hardware? i'd be happy to give it a go if you're ready / if you've got instructions for that! :)

@conallob

conallob commented Sep 27, 2026 •

Copy link
Copy Markdown
Author

@justarandomgeek I believe it's ready to test on hardware. I need to set up one of my newer PoE KVMs for experimenting before I start testing on hardware though

Depending on which generation you have, #1536 may be a factor when testing though

@justarandomgeek

Copy link
Copy Markdown

yeah i have the PoE one, and i've already got the serial cable made up for it to the common adapter i use for everything, i just need to lay my hands on one more ethernet cable than i had, apparently...

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

3 participants