Skip to content

feat: add interactive tclient CLI and improve Telnet compatibility - #12

Merged
LeeEirc merged 4 commits into
masterfrom
feat/tclient-cli-terminal-compat
Nov 3, 2026
Merged

LeeEirc merged 4 commits into
masterfrom
feat/tclient-cli-terminal-compat

Conversation

@LeeEirc

@LeeEirc LeeEirc commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Add a supported interactive Telnet CLI that shares the library's transport and optional login path, and fix protocol behavior needed for reliable terminal sessions. This provides a standalone way to reproduce manual and automatic-login connections when troubleshooting library consumers such as Koko.

Changes

Interactive CLI

  • Add cmd/tclient with manual login by default and explicit --auto-login --user --ask-pass authentication. Support IPv4/IPv6, charset conversion, Enter mapping, negotiated local echo, window updates, and Ctrl+] commands.
  • Restore terminal settings and preserve original file flags on disconnect, errors, signals, and blocked or broken output pipes, including Go-managed nonblocking input/output. Keep local commands and echo on stderr so redirected stdout contains remote application data.
  • Keep local commands responsive during blocked network sends with an ordered 256 KiB input queue. Report an error and close the session on overflow; keep redirected input streaming with backpressure.
  • Send window updates through a serialized worker and refresh dimensions after automatic login, so peer backpressure cannot block quit or disconnect handling. Cancel blocked redirected pipe I/O on Windows as well as Unix.
  • After stdin EOF, drain final output with a deadline while keeping the write side available for Telnet replies. Provide --close-write for endpoints that require TCP EOF.
  • Create optional traces exclusively with mode 0600 and a sensitive-data warning. Reuse the CLI from _example/telnet_client.

Protocol handling

  • Decode receive-side NVT CR-NUL sequences in wire order, including fragmented input and BINARY transitions within one read.
  • Support SGA in both directions and serialize NAWS updates with negotiation so stale window packets cannot follow option shutdown.
  • Honor environment-variable requests, ordering, undefined values, and escaping. Export only the configured USER and retain legacy ENVIRON compatibility.
  • Process state-preserving, no-reply negotiation events without waiting for a blocked application write; retain write serialization for actual option changes and replies.

Compatibility

  • Client.Write continues to preserve application bytes and escape IAC. Existing automatic-login configuration and LineEnding behavior remain unchanged.
  • Observable receive change: Client.Read now removes the protocol NUL immediately following CR when receive BINARY mode is disabled. CRLF, standalone NUL, and binary data are preserved.
  • Add the read-only Client.TerminalMode() snapshot; terminal control remains in the CLI.
  • The old example now uses the CLI's flags. Replace -ip/-port with positional host [port], and -username/-password with --auto-login --user ... --ask-pass. See usage and migration details.

Validation and scope

Linux and Windows CI passed on Go 1.20.x and stable, including go test -race -timeout=3m ./..., go vet ./..., and both examples.

All 20 Linux PTY/TCP scenarios passed, covering echo, resize during automatic login, input fragmentation, EOF negotiation, final output, terminal restoration, blocked output, hidden Unicode password editing, and trace permissions. Targeted regression tests also cover blocked NAWS and input writes during local quit, EOF deadlines before queued sends complete, ordered data/commands/flush/half-close, bounded queue overflow, original file-flag preservation, and no-reply negotiations during blocked application writes.

Windows native tests cover synchronous and overlapped pipe I/O, cancellation before and during blocked operations, reuse of the original handles, and session drain timeouts. macOS arm64 cross-compilation passed. Interactive terminals currently target Unix; Windows supports redirected sessions and rejects TTY input before changing console settings.

Interaction is character-at-a-time. LINEMODE/local line editing and TCP urgent/SYNCH are outside this change. Koko integration and real vendor-device interoperability have not been tested in this PR.

Copilot AI balanced review requested due to automatic review settings November 3, 2026 14:32
@LeeEirc LeeEirc changed the title feat: add interactive tclient CLI and complete terminal protocol handling feat: add interactive tclient CLI and improve Telnet compatibility Nov 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Peer backpressure can deadlock negotiation reads and resize handling, while uncancellable non-Unix output writes can prevent session shutdown and cleanup.

4 open issues
What changed in this PR

Adds a supported interactive tclient CLI using the library’s existing transport and optional login flow, alongside improved terminal protocol handling.

Changes:

  • Decode receive-side NVT data in wire order, expose negotiated terminal modes, and expand environment negotiation.
  • Add interactive commands, charset conversion, terminal cleanup, EOF draining, and private tracing.
  • Reuse the CLI from the example entry point and add documentation, protocol tests, and PTY/TCP coverage.
File Description
transport.go Adds NVT decoding and serializes negotiation and window updates.
terminal_protocol_test.go Tests NVT handling, terminal modes, and concurrent writes.
terminal_mode.go Exposes a concurrency-safe terminal-mode snapshot.
scripts/​test_cli_pty.py Adds 17 black-box terminal and TCP scenarios.
README.md Introduces CLI usage and updated protocol behavior.
internal/​cli/​terminal_unix.go Adds polling-based Unix I/O and descriptor restoration.
internal/​cli/​terminal_other.go Provides non-Unix redirected-I/O support.
internal/​cli/​run.go Coordinates connection, login, session I/O, and cleanup.
internal/​cli/​run_test.go Tests session lifecycle, encoding, tracing, and failures.
internal/​cli/​options.go Defines and validates CLI options.
internal/​cli/​input.go Handles terminal input, local commands, and hidden passwords.
internal/​cli/​input_test.go Tests Enter mapping, echo, and local commands.
go.sum Records new dependency checksums.
go.mod Adds terminal, system, and encoding dependencies.
environment.go Implements bounded USER-only environment replies.
environment_test.go Tests request selection, escaping, and legacy compatibility.
docs/​cli.md Documents usage, platform support, and protocol limitations.
doc.go Clarifies optional login and terminal-mode reporting.
decoder.go Records event positions within decoded application data.
cmd/​tclient/​main.go Adds the supported CLI entry point.
client.go Documents optional login and application I/O semantics.
.github/​workflows/​test.yml Runs PTY/TCP scenarios in CI.
_example/​telnet_client/​main.go Delegates to the shared CLI implementation.
_example/​telnet_client/​go.sum Updates example dependency checksums.
_example/​telnet_client/​go.mod Aligns the example module for shared internal CLI access.

Review effort: Balanced

Comment thread internal/cli/run.go Resolved
Comment thread internal/cli/terminal_other.go Resolved
Comment thread transport.go Resolved
Comment thread internal/cli/run.go Resolved

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Synchronous network writes can block local quit handling, and descriptor access can change file flags that cleanup fails to restore.

No open issues 🎉

2 previously missed issues

In code that hasn't changed since last review

Medium severity Blocked network writes prevent processing local quit commands

internal/​cli/​input.go:68

If the peer stops reading and a paste fills the TCP send buffer, this synchronous write blocks the only stdin-reading goroutine, so subsequent Ctrl+] quit cannot be processed. Interactive writes have no deadline, and raw-mode Ctrl+C is sent remotely rather than interrupting the client. Keep local-command parsing independent of blocked network writes, with a cancellable send worker and a bounded queue policy that still allows local quit to close the connection.

Medium severity File.Fd changes descriptor mode and breaks flag restoration

internal/​cli/​terminal_unix.go:81

For a Go-managed nonblocking pipe such as os.Pipe, File.Fd() switches the descriptor to blocking mode before this flags snapshot, so cleanup restores the wrong flags. The input-side Fd() calls in run.go, input.go, and this file likewise change stdin without restoring its original flags, even if connecting fails. Use non-mutating descriptor access through SyscallConn.Control for both output snapshots and input/terminal operations.

4 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are reported for the new CLI and related Telnet transport changes.

No open issues 🎉

Review effort: Balanced

@LeeEirc
LeeEirc merged commit ffb709e into master Nov 3, 2026
9 checks passed
@LeeEirc
LeeEirc deleted the feat/tclient-cli-terminal-compat branch November 3, 2026 15:35
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.

2 participants