Skip to content

feat: add remote-exec#2

Open
aeroxy wants to merge 22 commits into
mainfrom
dev
Open

feat: add remote-exec#2
aeroxy wants to merge 22 commits into
mainfrom
dev

Conversation

@aeroxy
Copy link
Copy Markdown
Owner

@aeroxy aeroxy commented Jun 1, 2026

Summary by CodeRabbit

  • New Features

    • Unix-only remote command execution with interactive PTY support and client-side RemoteExec subcommand.
    • Opt-in server setting/CLI flag to enable remote exec (disabled by default).
    • Terminal resize updates and remote exit-status reporting.
  • Bug Fixes / Improvements

    • Improved session shutdown, exit-code handling, and relay behavior during disconnects.
    • Faster connection readiness signaling to avoid send stalls.
  • Documentation

    • Expanded config and wiki guidance on PTY behavior and security trade-offs.
  • Tests

    • Added protocol round-trip tests for exec-related messages.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1aee38e0-142f-4c7f-bcc1-937bdc042d0b

📥 Commits

Reviewing files that changed from the base of the PR and between cb4884f and 8810dfa.

📒 Files selected for processing (1)
  • src/server.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/server.rs

📝 Walkthrough

Walkthrough

Adds Unix PTY-backed remote exec: dependencies and server config, protocol variants and tests, tunnel readiness/Exit handling, server PTY relay, client terminal/raw-mode I/O, CLI RemoteExec wiring and hot-reload, plus supporting docs.

Changes

Remote Exec Feature

Layer / File(s) Summary
Dependencies and server config
Cargo.toml, config.example.toml, src/config.rs
Adds shlex and Unix-only PTY deps (portable-pty, nix, libc) and a new server.allow_exec: bool documented default false.
CLI integration (RemoteExec)
src/main.rs
Adds exec module (unix-gated), RemoteExec subcommand and args, --allow-exec server flag with CLI override plumbing, logging override for RemoteExec, and dispatch that connects and invokes exec::run.
Client exec flow
src/exec.rs
Client probes terminal size, enters raw mode via RawGuard, sends Exec, forwards stdin↔tunnel (sends EOF payload on stdin EOF), handles SIGWINCH resize, awaits TunnelEvent::Exit, sends Close, and returns exit code.
Protocol messages and tests
src/protocol.rs
Adds Message::Exec, Message::Resize, Message::ExitStatus and tests that round-trip these variants (plus Reset test).
Tunnel SSE & Exit events
src/tunnel.rs
Adds TunnelEvent::Exit(i32), changes SSE readiness to sse_ready: Arc<Notify>, delays readiness until first qualifying SSE payload on initial connect, adjusts reconnect behavior, and maps ExitStatus to TunnelEvent::Exit. Includes unit tests.
Relay writer handling
src/relay.rs
Relay writer loop handles TunnelEvent::Exit(_) by terminating the writer task.
Server-side PTY execution & relay
src/server.rs
Adds per-connection PTY resize channels; Message::Exec opens a PTY and spawns shell/command on Unix, relays PTY↔SSE with retry and shutdown bounds, sends ExitStatus then Close, and cleans up. Adds encrypted_response and forward_pty_chunk.
Reload & overrides
src/reload.rs
Adds CliOverrides.server_allow_exec, HotServerConfig.allow_exec, documents intentional invalid-certs usage comment, and computes effective allow_exec with CLI precedence during reloads.
Docs: security trade-offs
wiki/enriched-context.md
Documents PTY canonical-mode behavior and intentional trade-offs: disabled TLS cert validation in specific client builder and opt-in tunnix server --allow-exec behavior with startup warning.

Sequence Diagram

sequenceDiagram
    participant User as User (Client)
    participant ClientExec as exec::run()
    participant Tunnel as Tunnel (SSE)
    participant Server as server handler
    participant PTY as PTY & Child

    User->>ClientExec: start remote exec (tunnel, cmd)
    ClientExec->>Tunnel: Message::Exec(cmd, cols, rows)
    Tunnel->>Server: HTTP POST (encrypted)
    Server->>Server: check hot.allow_exec
    Server->>PTY: create pty & spawn child
    Server-->>Tunnel: encrypted ACK
    Tunnel-->>ClientExec: ACK
    ClientExec->>ClientExec: enter raw mode
    User->>ClientExec: stdin bytes
    ClientExec->>Tunnel: Message::Data(conn_id, bytes)
    Tunnel->>Server: forward Data
    Server->>PTY: write to child stdin
    PTY-->>Server: child stdout bytes
    Server->>Tunnel: Message::Data(conn_id, bytes)
    Tunnel->>ClientExec: Data -> stdout
    ClientExec->>User: display output
    User->>ClientExec: resize signal
    ClientExec->>Tunnel: Message::Resize(conn_id, cols, rows)
    Tunnel->>Server: apply resize
    PTY->>Server: child exits(code)
    Server->>Tunnel: Message::ExitStatus(conn_id, code)
    Tunnel->>ClientExec: TunnelEvent::Exit(code)
    ClientExec->>ClientExec: restore terminal, send Close
    ClientExec->>User: exit(code)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I poke the tunnel, keys aglow,
PTYs hum where quiet bytes flow,
Resize and raw mode, shells comply,
Exit codes bounce back through the sky,
The rabbit bows—then hops goodbye.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add remote-exec' directly and accurately reflects the main feature being introduced across the changeset: remote command execution capability via PTY support.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a remote command execution feature (tunnix remote-exec) using PTYs, adding a new CLI command, configuration options, and PTY-based data relaying on the server. The review feedback highlights several critical issues: potential compilation failures on Windows due to Unix-specific APIs and commands that require conditional compilation, a potential deadlock in the PTY relay if background processes keep the slave PTY open, a connection registration leak on early errors in exec::run, high lock contention from locking the session mutex on every data chunk, and a lack of Windows shell fallbacks for command execution.

Comment thread src/server.rs Outdated
Comment thread src/main.rs
Comment thread src/main.rs
Comment thread src/main.rs
Comment thread src/main.rs
Comment thread src/main.rs
Comment thread src/server.rs Outdated
Comment thread src/server.rs
Comment thread src/exec.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/reload.rs (1)

143-159: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve --allow-exec across hot reloads.

Line 158 always reapplies the file value, so a server started with --allow-exec will silently revert on the next reload. This should follow the other CLI override semantics and ignore file changes when the flag was supplied.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/reload.rs` around lines 143 - 159, The reload logic currently always
applies the file's sc.allow_exec value, reverting any CLI-provided flag; update
the logic so CLI overrides take precedence: when comparing sc.allow_exec !=
current.allow_exec, if the CLI supplied the flag (i.e., current.allow_exec
reflects a CLI override) do not push "allow_exec" into changed and do not use
sc.allow_exec when constructing the new HotServerConfig; instead set allow_exec
in the HotServerConfig to current.allow_exec (preserving the CLI choice). Ensure
the check around changed.push("allow_exec") and the HotServerConfig field
assignment use this rule so file changes are ignored when the flag was supplied.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/exec.rs`:
- Line 80: The code currently initializes let mut exit_code = 0 and returns it
even when no ExitStatus was received, turning transport/server failures into
success; change this by introducing a sentinel (e.g., Option<i32> observed_exit)
or a boolean (e.g., saw_exit_status) alongside exit_code in the same scope, set
it only when you actually observe an ExitStatus in the loop that handles process
events, and replace any early/closure/error return paths (the branches that
return exit_code when the tunnel closes or errors before an ExitStatus) to
instead return an Err or a non-zero fallback when observed is None/false; update
all places that currently return exit_code (including the tunnel-closed and
error branches) to check the new observed flag and convert to a proper error or
a defined non-zero code if no real exit was seen.

In `@src/main.rs`:
- Around line 330-333: The code builds cmd by flattening ra.cmd with a raw space
join (let cmd = if ra.cmd.is_empty() { None } else { Some(ra.cmd.join(" ")) }),
which loses argument boundaries and will break commands with spaces or
metacharacters when executed via /bin/sh -c; fix it by either 1) properly
shell-escaping each element of ra.cmd before joining (use a robust escape
function that wraps/escapes items for POSIX sh) so the resulting String
preserves arguments when passed to /bin/sh -c, or 2) better, change the
protocol/handler to transmit ra.cmd as a Vec<String> and spawn the process
directly without /bin/sh -c (preserve argv and use
Command::new(&cmd0).args(&argv[1..]) or equivalent). Update the code that
constructs and uses the cmd variable and the execution path that invokes /bin/sh
-c to use one of these approaches.
- Line 3: The build exposes a Unix-only module and subcommand unconditionally:
remove or gate the unconditional mod exec; and Command::RemoteExec export and
instead add cfg gating or a non-Unix stub to avoid failing on non-Unix targets.
Specifically, wrap the module declaration and any uses/variants of
Command::RemoteExec with #[cfg(unix)] (and #[cfg(not(unix))] for a stub
implementation in src/exec.rs), or provide a platform-specific stub module that
implements the same public API but returns a clear error on non-Unix; update
references to exec::RemoteExec / Command::RemoteExec so they compile under both
cfg branches.

In `@src/server.rs`:
- Around line 664-679: The PTY child can outlive crashed clients because the
current tokio::select only kills the child when write_rx actually closes
(write_blocking path), but a crashed client can leave its sender parked in
sess.tcp_writers so write_rx never closes; update the shutdown logic to detect
abandoned sessions — for example, wrap the child wait/select with a timeout
(tokio::time::timeout) or add a heartbeat/inactivity check that observes
sess.tcp_writers and forces killer.kill() when the writer has been idle or
removed; specifically modify the code around child.clone_killer, wait_handle,
and the write_blocking/select block to fall back to a forced kill when the
timeout/heartbeat indicates the client is gone.
- Line 391: The current info! log prints the full user-supplied command via cmd
which may expose secrets; change the info! call in server.rs to only log conn_id
and whether it was a shell or command (e.g., use cmd.is_some() to emit "cmd" vs
"<shell>") along with cols and rows, and move the full command string into a
debug! log (or redact it) using cmd.as_deref() only at debug level so sensitive
data is not present in normal logs; update the log invocation that currently
uses info!("[{}] EXEC {} ({}x{})", conn_id, cmd.as_deref().unwrap_or("<shell>"),
cols, rows) accordingly.

---

Outside diff comments:
In `@src/reload.rs`:
- Around line 143-159: The reload logic currently always applies the file's
sc.allow_exec value, reverting any CLI-provided flag; update the logic so CLI
overrides take precedence: when comparing sc.allow_exec != current.allow_exec,
if the CLI supplied the flag (i.e., current.allow_exec reflects a CLI override)
do not push "allow_exec" into changed and do not use sc.allow_exec when
constructing the new HotServerConfig; instead set allow_exec in the
HotServerConfig to current.allow_exec (preserving the CLI choice). Ensure the
check around changed.push("allow_exec") and the HotServerConfig field assignment
use this rule so file changes are ignored when the flag was supplied.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cee0e9ea-4ae0-4df7-b7de-ea2fed4d7ce8

📥 Commits

Reviewing files that changed from the base of the PR and between fa0a219 and e741c7e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml
  • config.example.toml
  • src/config.rs
  • src/exec.rs
  • src/main.rs
  • src/protocol.rs
  • src/relay.rs
  • src/reload.rs
  • src/server.rs
  • src/tunnel.rs

Comment thread src/exec.rs Outdated
Comment thread src/main.rs
Comment thread src/main.rs Outdated
Comment thread src/server.rs Outdated
Comment thread src/server.rs
@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 1, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0fc6e2c26b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tunnel.rs Outdated
Comment thread src/exec.rs Outdated
@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 1, 2026

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a remote command execution feature (tunnix remote-exec) that allows running commands or interactive shells on the server via a PTY. It includes configuration options to enable this feature, terminal raw mode management, resize handling, and improved client-side SSE connection synchronization. The review feedback highlights two key improvements: implementing a retry mechanism when sending PTY data over a temporarily disconnected SSE stream to prevent data loss, and querying stdout/stderr as fallbacks in terminal_size() to correctly detect terminal dimensions when stdin is redirected.

Comment thread src/server.rs
Comment thread src/exec.rs
… SSE output reliability during client reconnects
@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 1, 2026

@codex review

@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 1, 2026

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a remote command execution feature (tunnix remote-exec) using a PTY, allowing users to run interactive shells or commands on the server. Feedback on these changes highlights three key issues: abrupt client disconnections can cause remote processes and PTY relay tasks to leak and hang on the server; joining command arguments with a space loses argument boundaries and should instead use a shell-quoting library like shlex; and restricting the EOF character transmission to when input has been sent causes commands with empty stdin to hang indefinitely.

Comment thread src/server.rs
Comment thread src/main.rs
Comment thread src/exec.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/exec.rs`:
- Around line 99-115: The current EOF handling in the stdin read branch skips
sending an EOF if sent_input is false, which causes remote commands reading
until EOF to hang; update the match arm inside the tokio::select! branch (the
stdin.read(&mut stdin_buf) handling) so that when Ok(0) is returned you always
send a terminal EOF/close signal to the remote (e.g. reuse Message::Data {
conn_id, data: vec![0x04] } or a dedicated close/EOF message) via
tunnel.send_message(&eof).await even if sent_input is false, or alternatively
send a specific stdin-close message if the protocol supports it; ensure
sent_input is not used to suppress the EOF/close signal in that branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a55f014-bf61-4dea-be5a-c25e5b295c1e

📥 Commits

Reviewing files that changed from the base of the PR and between 0fc6e2c and 82e36e0.

📒 Files selected for processing (3)
  • src/exec.rs
  • src/server.rs
  • src/tunnel.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/tunnel.rs
  • src/server.rs

Comment thread src/exec.rs Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 82e36e0703

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/exec.rs Outdated
Comment thread src/tunnel.rs Outdated
- server: add an SSE-dead watchdog to relay_pty_connection so an abrupt
  client disconnect no longer orphans the child process. The watchdog
  polls Session::sse_tx.is_closed() every 2s; once the SSE receiver is
  gone (network drop, process killed, tunnel gone) it kills the child
  and joins the wait handle. Previously write_blocking would block on
  write_rx forever and the child would run indefinitely.
- server: tunnel sse_ready now also fires on the first event during an
  existing-session reconnect (not just on a data: frame). The first
  event on a reconnect is typically the keepalive comment, so
  send_message's retry path was burning the full 10s RECONNECT_WAIT
  before retrying the POST. Initial connect still gates on the first
  data: frame so a new-session Reset is processed before
  register_connection lands.
- exec: wrap session() body in an inner async block so Message::Close
  is sent to the server on every exit path (including ? early returns
  on stdin send failure and TunnelEvent::Error). Without this, a
  tunnel error during the session would leave the server's PTY and
  child process running.
- exec: always send the terminal EOF byte (0x04) on local stdin EOF,
  even when no input was forwarded. Gating on sent_input made
  'tunnix remote-exec cat </dev/null' and similar commands hang
  forever waiting for input to close.
- main: use shlex::try_join to forward remote-exec arguments so spaces
  and quotes inside args survive the trip through the server's
  'sh -c'. Plain join(" ") re-tokenized args, so 'tunnix remote-exec
  echo "a b" "c"' would reach the server as four args.
- reload: annotate danger_accept_invalid_certs(true) as intentional
  (corporate TLS-inspecting proxy deployments rely on it). Wiki
  enriched-context gains an 'Intentional security trade-offs' section
  to keep future maintainers from 'fixing' it in a security pass.
@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 1, 2026

/gemini review

@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 1, 2026

@codex review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a remote command execution and interactive shell feature (tunnix remote-exec) using PTY allocation. Key changes include adding the RemoteExec subcommand, introducing new protocol messages (Exec, Resize, ExitStatus), implementing server-side PTY spawning and relaying, and adding client-side terminal raw mode handling. The review feedback highlights three main areas for improvement: making the nix dependency platform-specific in Cargo.toml to prevent breaking Windows compilation, adding platform-specific shell support (e.g., cmd.exe on Windows) in the server's execution logic, and decoupling stdin reading from network sending in src/exec.rs using a background task to prevent terminal lag and freezing.

Comment thread Cargo.toml Outdated
Comment thread src/server.rs
Comment thread src/exec.rs
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97e9bb06a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main.rs
Comment thread src/exec.rs Outdated
…ing)

- main: pass single-arg `cmd` verbatim to the server's `sh -c`. shlex
  would otherwise wrap strings like `echo $HOME && id` in single
  quotes, making the shell look for a literal command named that.

- exec: track whether the last forwarded stdin byte was '\n'. On local
  EOF, send '\n' + 0x04 (not just 0x04) when the line is unterminated,
  so canonical-mode consumers (cat, read, ...) see real EOF and
  `printf abc | remote-exec cat` no longer hangs.

- exec: move all outbound messages (stdin data, EOF, resize) through a
  background sender task fed by an mpsc. The main select! only
  enqueues, so a slow POST /send can no longer stall stdout writes or
  SIGWINCH handling. The channel is dropped and the sender joined
  before sending Close, so the final Close is always the last message
  on the wire.

- server: gate `Message::Exec` / `Message::Resize`, the
  `pty_masters` field, the `portable_pty` import, the
  `relay_pty_connection` fn, and the new `forward_pty_chunk` helper
  behind `#[cfg(unix)]`. On non-unix, Exec returns an encrypted
  `Error` and Resize is a no-op — PTY support is intentionally
  Unix-only.

- server: on child exit, signal the forward task via `Notify` so it
  does a final `try_recv` flush of the PTY output channel and exits,
  rather than waiting forever for the channel to close (it never does
  when a backgrounded process inherited the slave PTY). The 2s drain
  budget is preserved; the encrypt+send retry path is extracted into
  `forward_pty_chunk` and shared by the live and drain paths.

- cargo: move `portable-pty` and `nix` under
  `[target.'cfg(unix)'.dependencies]`. `libc` stays top-level.
@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 2, 2026

/gemini review

@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 2, 2026

@codex review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces remote command execution and interactive shell support (tunnix remote-exec) over the tunnel using a PTY on Unix-based servers. Key changes include the addition of a client-side execution module (src/exec.rs), server-side PTY spawning and relaying logic (src/server.rs), and configuration options to safely enable or disable this feature. Feedback on these changes highlights two main issues: first, a potential thread leak and blocking thread pool exhaustion in the PTY reader if a background process inherits the slave PTY, which can be resolved using tokio::io::unix::AsyncFd; second, a conflict where the watchdog immediately kills the child process during transient disconnects, bypassing the intended five-second reconnect tolerance.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/server.rs Outdated
Comment thread src/server.rs
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3a2c22cb92

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/exec.rs
@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 2, 2026

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

✅ Actions performed

Reviews resumed.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
Cargo.toml (1)

64-64: <!-- [scratchpad_start]
[task review_file_1/1]
[observations]

  • Cargo.toml: line 64 has shlex = "1.3".

  • Cargo.toml: line 66-68 comment says "Remote exec (PTY) — Unix only", but libc = "0.2" is in root [dependencies] (line 68).

  • Cargo.toml: line 70 starts [target.'cfg(unix)'.dependencies] with portable-pty = "0.9" and nix = { version = "0.29", features = ["term"] }.

  • Cargo.lock: package shlex resolves to version = "1.3.0" with checksum.

  • GitHub advisory query for shlex returns LOW severity issues with vulnerableVersionRange: "< 1.3.0" and firstPatchedVersion.identifier: "1.3.0".

  • src/main.rs: mod exec; and RemoteExec are gated behind #[cfg(unix)].

  • src/exec.rs: contains libc::STDIN_FILENO/libc::winsize/libc::ioctl usages (no further cfg on those lines).
    [analysis]

  • shlex = "1.3" is safe relative to the reported advisory because Cargo.lock pins to 1.3.0, which is the first patched version for the < 1.3.0 range.

  • The libc placement isn’t a functional gating bug (libc is only used from the #[cfg(unix)] exec module), but it does contradict the manifest comment and prevents matching the "Unix only" intent at the dependency level.
    [done]
    [ scratchpad_end ] -->
    Align Unix-only dependency intent

  • shlex = "1.3" resolves to shlex 1.3.0 in Cargo.lock; the reported shlex security advisory affects shlex < 1.3.0 and lists 1.3.0 as the first patched version (LOW), so the pinned version is covered.

  • Cargo.toml keeps libc = "0.2" in root [dependencies] even though it’s described as “Unix only”; since libc is only used from src/exec.rs which is compiled under #[cfg(unix)], this is mostly a manifest/intent mismatch—move libc into [target.'cfg(unix)'.dependencies] if you want to avoid pulling it for non-Unix builds and match the comment.

  • Consider checking advisories for portable-pty/nix as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Cargo.toml` at line 64, Cargo.toml currently declares libc in the root
[dependencies] even though it’s only used by the Unix-gated exec module (mod
exec / RemoteExec); move the libc = "0.2" entry into the existing
[target.'cfg(unix)'.dependencies] section alongside portable-pty and nix so the
dependency is only pulled for Unix builds, then run a full cargo build/test to
verify src/exec.rs (which references libc::STDIN_FILENO, libc::winsize,
libc::ioctl) still compiles under cfg(unix); also re-check advisories for
portable-pty and nix while you’re updating the manifest.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Cargo.toml`:
- Around line 66-68: The dependency "libc" is currently in the root
[dependencies] section but must be compiled only on Unix; remove the line `libc
= "0.2"` from the main dependencies and add it under the Unix-specific Cargo
section by placing it in [target.'cfg(unix)'.dependencies] so the crate is only
enabled on Unix targets (reference the dependency name "libc" and the target
section "[target.'cfg(unix)'.dependencies]").

---

Nitpick comments:
In `@Cargo.toml`:
- Line 64: Cargo.toml currently declares libc in the root [dependencies] even
though it’s only used by the Unix-gated exec module (mod exec / RemoteExec);
move the libc = "0.2" entry into the existing [target.'cfg(unix)'.dependencies]
section alongside portable-pty and nix so the dependency is only pulled for Unix
builds, then run a full cargo build/test to verify src/exec.rs (which references
libc::STDIN_FILENO, libc::winsize, libc::ioctl) still compiles under cfg(unix);
also re-check advisories for portable-pty and nix while you’re updating the
manifest.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 98bd859e-5158-4420-8067-74b598f1673c

📥 Commits

Reviewing files that changed from the base of the PR and between 82e36e0 and 3a2c22c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • src/exec.rs
  • src/main.rs
  • src/reload.rs
  • src/server.rs
  • src/tunnel.rs
  • wiki/enriched-context.md
✅ Files skipped from review due to trivial changes (1)
  • wiki/enriched-context.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/main.rs
  • src/reload.rs
  • src/exec.rs
  • src/tunnel.rs
  • src/server.rs

Comment thread Cargo.toml
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a remote command execution feature (remote-exec) to tunnix, allowing users to run commands or interactive shells over the tunnel. It adds PTY allocation and management on Unix platforms, introduces new protocol messages (Exec, Resize, ExitStatus), and updates the server to handle these requests when explicitly enabled. The code review feedback highlights a potential memory exhaustion (OOM) risk in src/exec.rs due to the use of an unbounded channel for stdin streaming without backpressure, recommending a bounded channel and a separate task instead. Additionally, reviewers noted that a failure in try_clone_reader within src/server.rs could cause the client to hang indefinitely, suggesting that an error message should be sent to the client before returning.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/exec.rs
Comment thread src/server.rs Outdated
Comment thread src/exec.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server.rs (1)

640-654: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Fix: spawn_blocking makes your 2s EOF-timeout cleanup ineffective (can hang)

Moving the PTY reader to tokio::task::spawn_blocking fixes the WouldBlock busy-spin, but it breaks the timeout cleanup: JoinHandle::abort() does not cancel a spawn_blocking closure that’s already running, and read_task.await will wait until the blocking reader.read returns rather than returning a cancelled JoinError.

So the scenario your comment claims to handle (child keeps the PTY slave open → reader.read blocks forever) is not actually handled:

  • read_task.abort() won’t unblock or stop the blocking read.
  • The dup’d fd won’t be closed as part of cancellation, and the blocking thread can remain stuck.
  • Cleanup after the 2s timeout can hang, preventing ExitStatus/Close and leaving session state entries around.

The read loop (Ok(0)/Ok(n)/Errbreak + blocking_send) is fine; the cleanup contract needs to actively unblock reader.read (e.g., close the underlying dup’d fd on timeout, or use a nonblocking/AsyncFd-driven read that can be cancelled by closing/unregistering).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.rs` around lines 640 - 654, The blocking PTY read spawned with
tokio::task::spawn_blocking (the closure using reader.read and pty_out_tx,
assigned to read_task) cannot be cancelled by read_task.abort(), so the 2s
timeout cleanup can hang; fix by making the reader cancelable: either switch to
a non-blocking/AIO approach (use tokio::io::unix::AsyncFd or set O_NONBLOCK on
the dup'd FD and use async reads so the task can be aborted) and perform reads
on the async handle instead of spawn_blocking, or, if you must use
spawn_blocking, ensure the timeout path actively closes the dup'd file
descriptor (the underlying FD held by reader) before awaiting/aborting read_task
so reader.read returns and the thread exits; update the code paths around
spawn_blocking, reader, read_task, and the timeout/cleanup logic to implement
one of these two approaches.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/server.rs`:
- Around line 640-654: The blocking PTY read spawned with
tokio::task::spawn_blocking (the closure using reader.read and pty_out_tx,
assigned to read_task) cannot be cancelled by read_task.abort(), so the 2s
timeout cleanup can hang; fix by making the reader cancelable: either switch to
a non-blocking/AIO approach (use tokio::io::unix::AsyncFd or set O_NONBLOCK on
the dup'd FD and use async reads so the task can be aborted) and perform reads
on the async handle instead of spawn_blocking, or, if you must use
spawn_blocking, ensure the timeout path actively closes the dup'd file
descriptor (the underlying FD held by reader) before awaiting/aborting read_task
so reader.read returns and the thread exits; update the code paths around
spawn_blocking, reader, read_task, and the timeout/cleanup logic to implement
one of these two approaches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 18a27b03-a4f0-4c8c-8849-a8feab498ed3

📥 Commits

Reviewing files that changed from the base of the PR and between df4f75e and 2f5d542.

📒 Files selected for processing (2)
  • src/server.rs
  • src/tunnel.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tunnel.rs

@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 3, 2026

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a remote command execution (remote-exec) feature to tunnix, allowing users to run commands or open interactive shells on the server via a pseudo-terminal (PTY) on Unix platforms. Key feedback highlights a cancel-safety issue in src/exec.rs when reading directly from tokio::io::Stdin within a select! block, which can be resolved by forwarding input through an mpsc channel. Additionally, a thread leak risk was identified in src/server.rs because read_task.abort() does not terminate blocking spawn_blocking tasks, and a defensive check was recommended to handle an empty SHELL environment variable by defaulting to /bin/sh.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/exec.rs
Comment thread src/server.rs Outdated
Comment thread src/server.rs Outdated
@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 3, 2026

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a remote command execution (remote-exec) feature to tunnix, allowing users to run commands or open interactive shells on a server via a pseudo-terminal (PTY). The changes include new CLI subcommands, configuration options to safely opt-in to this feature, protocol updates, and PTY stream relaying logic. The review comments identify two critical issues: first, a lack of backpressure on stdin in src/exec.rs due to an unbounded channel, which could lead to high memory usage; second, a concurrency risk in src/server.rs where a MutexGuard is held across an .await point during error handling, potentially causing deadlocks or starvation.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/exec.rs
Comment on lines +95 to +183
let (msg_tx, mut msg_rx) = tokio::sync::mpsc::unbounded_channel::<Message>();
let sender_tunnel = tunnel.clone();
let sender_task = tokio::spawn(async move {
while let Some(msg) = msg_rx.recv().await {
if let Err(e) = sender_tunnel.send_message(&msg).await {
debug!("send_message failed: {}", e);
break;
}
}
});

let mut stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
let mut stdin_buf = vec![0u8; 8192];
let mut stdin_open = true;
let mut winch = signal(SignalKind::window_change())?;
let mut exit_code = 0;
// Only a real ExitStatus from the server makes the command's exit code
// meaningful. If the tunnel drops or errors first, we must not report 0.
let mut saw_exit = false;
// Track whether the last forwarded byte was '\n'. On local stdin EOF,
// canonical-mode consumers (cat, read, ...) need a real EOF, not just a
// VEOF flush of an unterminated pending line. If the line is still
// buffered (no trailing newline), one Ctrl-D only delivers the partial
// line; the consumer then waits for more input forever. We send
// '\n' + 0x04 in that case so the line is delivered AND the next read
// sees EOF.
let mut last_was_newline = true;

loop {
tokio::select! {
n = stdin.read(&mut stdin_buf), if stdin_open => {
match n {
Ok(0) => {
// Local stdin hit EOF. Send a real EOF to the
// remote PTY so canonical-mode consumers terminate.
let mut eof_data = Vec::with_capacity(2);
if !last_was_newline {
// Terminate the pending line so the VEOF below
// can actually signal EOF on an empty buffer.
eof_data.push(b'\n');
}
eof_data.push(0x04);
let _ = msg_tx.send(Message::Data { conn_id, data: eof_data });
stdin_open = false;
}
Ok(n) => {
last_was_newline = stdin_buf[n - 1] == b'\n';
let msg = Message::Data { conn_id, data: stdin_buf[..n].to_vec() };
if msg_tx.send(msg).is_err() {
anyhow::bail!("sender channel closed");
}
}
Err(e) => {
debug!("stdin read error: {}", e);
stdin_open = false;
}
}
}
event = event_rx.recv() => {
match event {
Some(TunnelEvent::Data(data)) => {
if stdout.write_all(&data).await.is_err() {
break;
}
let _ = stdout.flush().await;
}
Some(TunnelEvent::Exit(code)) => {
exit_code = code;
saw_exit = true;
}
Some(TunnelEvent::Error(msg)) => {
anyhow::bail!("remote error: {}", msg);
}
Some(TunnelEvent::Close) | None => break,
}
}
_ = winch.recv() => {
let (cols, rows) = terminal_size();
let _ = msg_tx.send(Message::Resize { conn_id, cols, rows });
}
}
}

// Close the sender channel and wait for it to flush all queued
// messages. This guarantees the final Message::Close below is the
// last thing sent on the wire for this conn_id.
drop(msg_tx);
let _ = sender_task.await;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Unbounded Channel and Lack of Backpressure on Stdin

Using an unbounded channel (unbounded_channel) for forwarding stdin data to the network sender task can lead to unbounded memory growth and potential Out-Of-Memory (OOM) crashes. If a user pipes a large file or a continuous stream into tunnix remote-exec (e.g., cat large_file | tunnix remote-exec ...), stdin.read will read the data as fast as possible and buffer it in the unbounded channel, while the network sender task is bottlenecked by the slower HTTP POST requests.

To implement proper backpressure, we can:

  1. Use a bounded channel (e.g., mpsc::channel(100)).
  2. Spawn a separate async task to read from stdin and forward to the channel. This naturally suspends reading from stdin when the channel is full, propagating backpressure.
  3. Abort the stdin task when the main loop exits to ensure clean shutdown.
        let (msg_tx, mut msg_rx) = tokio::sync::mpsc::channel::<Message>(100);
        let sender_tunnel = tunnel.clone();
        let sender_task = tokio::spawn(async move {
            while let Some(msg) = msg_rx.recv().await {
                if let Err(e) = sender_tunnel.send_message(&msg).await {
                    debug!("send_message failed: {}", e);
                    break;
                }
            }
        });

        let stdin_sender = msg_tx.clone();
        let stdin_task = tokio::spawn(async move {
            let mut stdin = tokio::io::stdin();
            let mut stdin_buf = vec![0u8; 8192];
            let mut last_was_newline = true;
            loop {
                match stdin.read(&mut stdin_buf).await {
                    Ok(0) => {
                        let mut eof_data = Vec::with_capacity(2);
                        if !last_was_newline {
                            eof_data.push(b'\n');
                        }
                        eof_data.push(0x04);
                        let _ = stdin_sender.send(Message::Data { conn_id, data: eof_data }).await;
                        break;
                    }
                    Ok(n) => {
                        last_was_newline = stdin_buf[n - 1] == b'\n';
                        if stdin_sender.send(Message::Data { conn_id, data: stdin_buf[..n].to_vec() }).await.is_err() {
                            break;
                        }
                    }
                    Err(_) => break,
                }
            }
        });

        let mut stdout = tokio::io::stdout();
        let mut winch = signal(SignalKind::window_change())?;
        let mut exit_code = 0;
        let mut saw_exit = false;

        loop {
            tokio::select! {
                event = event_rx.recv() => {
                    match event {
                        Some(TunnelEvent::Data(data)) => {
                            if stdout.write_all(&data).await.is_err() {
                                break;
                            }
                            let _ = stdout.flush().await;
                        }
                        Some(TunnelEvent::Exit(code)) => {
                            exit_code = code;
                            saw_exit = true;
                        }
                        Some(TunnelEvent::Error(msg)) => {
                            anyhow::bail!("remote error: {}", msg);
                        }
                        Some(TunnelEvent::Close) | None => break,
                    }
                }
                _ = winch.recv() => {
                    let (cols, rows) = terminal_size();
                    let _ = msg_tx.try_send(Message::Resize { conn_id, cols, rows });
                }
            }
        }

        stdin_task.abort();
        let _ = stdin_task.await;
        drop(msg_tx);
        let _ = sender_task.await;

Comment thread src/server.rs Outdated
@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 3, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

✅ Action performed

Full review finished.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/reload.rs`:
- Around line 43-47: The comment next to .danger_accept_invalid_certs(true)
understates the scope: that call disables certificate validation for all TLS
connections created by this client (a global TLS validation bypass), and
hostname verification is a separate option controlled by
danger_accept_invalid_hostnames(true); update the comment to explicitly state it
disables certificate chain validation globally and the security implications,
and either gate this call behind an explicit opt-in config/feature flag or
change behavior to only disable hostname checks if that was intended (reference
.danger_accept_invalid_certs(true) and .danger_accept_invalid_hostnames(true) to
locate the client builder).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a5485f0-726a-4fac-9169-eaa989f99091

📥 Commits

Reviewing files that changed from the base of the PR and between fa0a219 and 3296ed0.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • Cargo.toml
  • config.example.toml
  • src/config.rs
  • src/exec.rs
  • src/main.rs
  • src/protocol.rs
  • src/relay.rs
  • src/reload.rs
  • src/server.rs
  • src/tunnel.rs
  • wiki/enriched-context.md

Comment thread src/reload.rs
@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 3, 2026

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements a remote command execution (remote-exec) feature for Unix platforms, allowing clients to run commands or open interactive shells on the server via a pseudo-terminal (PTY). The changes introduce new protocol messages (Exec, Resize, ExitStatus), configuration options to toggle execution permissions, and the necessary client and server relay logic. The review feedback highlights critical improvement opportunities, including a thread leak risk in src/server.rs when aborting spawn_blocking tasks, a cancellation leak risk in src/exec.rs that could bypass connection unregistration, and a potential spawn failure if the SHELL environment variable is set but empty.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/server.rs
Comment thread src/exec.rs
Comment thread src/server.rs
@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 3, 2026

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a remote command execution feature (remote-exec) to tunnix, allowing users to run commands or open interactive shells on a remote server over the tunnel using a PTY. The feedback suggests monitoring the background sender task in exec.rs to prevent client hangs if it exits prematurely. Additionally, the reviewer recommends dynamically forwarding the client's local TERM environment variable to the server instead of hardcoding it, which involves updating the Message::Exec protocol, client-side execution, server-side PTY configuration, and unit tests.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/exec.rs
break;
}
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Pin the sender_task so that we can safely poll it in the select! loop. This allows us to detect immediately if the background sender task fails, preventing the client from hanging in a half-dead state.

        });
        tokio::pin!(sender_task);

Comment thread src/exec.rs
Comment on lines +124 to +125
loop {
tokio::select! {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Add the sender_task to the select! loop to immediately detect if the background sender task exits prematurely (e.g., due to a persistent network failure or tunnel error).

        loop {
            tokio::select! {
                res = &mut sender_task => {
                    anyhow::bail!("sender task exited prematurely");
                }

Comment thread src/protocol.rs
Comment thread src/protocol.rs Outdated
Comment thread src/protocol.rs Outdated
Comment thread src/exec.rs Outdated
Comment thread src/server.rs Outdated
Comment thread src/server.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server.rs (1)

443-450: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Kill the PTY child on post-spawn setup failures.

spawn_command has already succeeded before both of these branches. If either one returns early, the request fails but the child is never explicitly terminated, so a command that ignores PTY hangup can keep running on the server.

🛠️ Minimal fix
             let writer = match master.take_writer() {
                 Ok(w) => w,
                 Err(e) => {
+                    let mut killer = child.clone_killer();
+                    let _ = killer.kill();
                     error!("[{}] take writer failed: {}", conn_id, e);
                     return encrypted_response(
                         &hot.crypto,
@@
     let (read_afd, write_afd) = match setup {
         Ok(pair) => pair,
         Err(e) => {
+            let mut killer = child.clone_killer();
+            let _ = killer.kill();
             error!("[{}] PTY async setup failed: {}", conn_id, e);
             let sse_tx = {
                 let mut sess = session.lock().await;

Also applies to: 668-685

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.rs` around lines 443 - 450, The spawned PTY child (returned from
spawn_command and stored in child) is not being terminated when
master.take_writer or other post-spawn setup fails, allowing the child to keep
running; before returning the encrypted_response in these error branches (e.g.,
the master.take_writer Err branch and the similar branch at lines 668-685),
explicitly terminate the child by calling its kill routine and awaiting/handling
its exit (e.g., call child.kill() and then wait on the child/handle to reap it)
so the process is not left running; apply the same change in both error branches
and reference the spawn_command result (child) and the encrypted_response return
sites when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server.rs`:
- Around line 668-685: The PTY setup failure path currently only sends
Message::Error and returns, leaving the client waiting after the earlier exec
ACK; update the Err(e) branch to also send the terminal shutdown sequence by
creating and sending a Message::ExitStatus (with the conn_id and appropriate
exit code) and a Message::Close (or Close equivalent) after sending
Message::Error, using the same crypto.encrypt(...) and sse_tx.send(...) flow
(reusing conn_id, sse_tx and msg.to_bytes()) so the client receives the
ExitStatus/Close pair and the local remote-exec session can clean up; ensure you
still perform the existing session.lock().await cleanup
(sess.tcp_writers.remove, sess.pty_resize.remove) before sending these final
messages.

---

Outside diff comments:
In `@src/server.rs`:
- Around line 443-450: The spawned PTY child (returned from spawn_command and
stored in child) is not being terminated when master.take_writer or other
post-spawn setup fails, allowing the child to keep running; before returning the
encrypted_response in these error branches (e.g., the master.take_writer Err
branch and the similar branch at lines 668-685), explicitly terminate the child
by calling its kill routine and awaiting/handling its exit (e.g., call
child.kill() and then wait on the child/handle to reap it) so the process is not
left running; apply the same change in both error branches and reference the
spawn_command result (child) and the encrypted_response return sites when making
the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 976dd2cb-3866-4506-b0c5-39c5f43226ed

📥 Commits

Reviewing files that changed from the base of the PR and between 5ec849a and b7aaf67.

📒 Files selected for processing (1)
  • src/server.rs

Comment thread src/server.rs
Comment on lines +668 to +685
Err(e) => {
error!("[{}] PTY async setup failed: {}", conn_id, e);
let sse_tx = {
let mut sess = session.lock().await;
sess.tcp_writers.remove(&conn_id);
sess.pty_resize.remove(&conn_id);
sess.sse_tx.clone()
};
let msg = Message::Error {
conn_id: Some(conn_id),
message: format!("PTY setup failed: {}", e),
};
if let Ok(bytes) = msg.to_bytes() {
if let Ok(encrypted) = crypto.encrypt(&bytes) {
let _ = sse_tx.send(encrypted).await;
}
}
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Emit ExitStatus/Close when PTY setup fails after the exec ACK.

Line 467 already ACKs the exec as established. This failure path only sends Message::Error and returns, so the client never gets the ExitStatus/Close pair that the exec flow waits on. That can leave the local remote-exec session hanging with a stale conn_id.

🛠️ Suggested follow-up
         Err(e) => {
             error!("[{}] PTY async setup failed: {}", conn_id, e);
             let sse_tx = {
                 let mut sess = session.lock().await;
                 sess.tcp_writers.remove(&conn_id);
                 sess.pty_resize.remove(&conn_id);
                 sess.sse_tx.clone()
             };
-            let msg = Message::Error {
-                conn_id: Some(conn_id),
-                message: format!("PTY setup failed: {}", e),
-            };
-            if let Ok(bytes) = msg.to_bytes() {
-                if let Ok(encrypted) = crypto.encrypt(&bytes) {
-                    let _ = sse_tx.send(encrypted).await;
-                }
-            }
+            for msg in [
+                Message::Error {
+                    conn_id: Some(conn_id),
+                    message: format!("PTY setup failed: {}", e),
+                },
+                Message::ExitStatus { conn_id, code: -1 },
+                Message::Close { conn_id },
+            ] {
+                if let Ok(bytes) = msg.to_bytes() {
+                    if let Ok(encrypted) = crypto.encrypt(&bytes) {
+                        let _ = sse_tx.send(encrypted).await;
+                    }
+                }
+            }
             return;
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.rs` around lines 668 - 685, The PTY setup failure path currently
only sends Message::Error and returns, leaving the client waiting after the
earlier exec ACK; update the Err(e) branch to also send the terminal shutdown
sequence by creating and sending a Message::ExitStatus (with the conn_id and
appropriate exit code) and a Message::Close (or Close equivalent) after sending
Message::Error, using the same crypto.encrypt(...) and sse_tx.send(...) flow
(reusing conn_id, sse_tx and msg.to_bytes()) so the client receives the
ExitStatus/Close pair and the local remote-exec session can clean up; ensure you
still perform the existing session.lock().await cleanup
(sess.tcp_writers.remove, sess.pty_resize.remove) before sending these final
messages.

@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 3, 2026

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a remote-exec feature, enabling users to run commands or interactive shells on the server through a PTY. The changes include updates to the protocol, configuration, and server/client logic, alongside documentation regarding security trade-offs. I have provided feedback on optimizing performance by replacing heap allocations with stack-based buffers and using stack pinning for futures.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/server.rs Outdated
Comment thread src/exec.rs
@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 4, 2026

/gemini review

@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 4, 2026

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

✅ Action performed

Reviews resumed.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a remote command execution (remote-exec) feature to Tunnix, allowing users to run commands or open interactive shells over the tunnel using a pseudo-terminal (PTY) on Unix systems. It adds client-side terminal handling in src/exec.rs, server-side PTY spawning and relaying in src/server.rs, and configuration/CLI support. The review feedback suggests a performance optimization during PTY session teardown: introducing a client_gone flag to skip or minimize the 50-retry loop when sending final shutdown messages to an already disconnected client, avoiding up to 10 seconds of unnecessary delays.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/server.rs
Comment thread src/server.rs
@aeroxy
Copy link
Copy Markdown
Owner Author

aeroxy commented Jun 4, 2026

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a remote command execution (remote-exec) feature to tunnix on Unix platforms, allowing users to run commands or open interactive shells over the tunnel. It adds a new exec module, updates the protocol with Exec, Resize, and ExitStatus messages, and implements PTY allocation and asynchronous relaying on the server. The review feedback highlights a critical compilation error in relay_pty_connection where .ok_or_else() is incorrectly called on a primitive RawFd returned by as_raw_fd(). Additionally, it is recommended to validate or restrict the SHELL environment variable when spawning the default shell on the server to prevent potential security or execution issues.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/server.rs
Comment on lines +644 to +665
let setup = (|| -> std::io::Result<(AsyncFd<OwnedFd>, AsyncFd<OwnedFd>)> {
let master_fd = master
.as_raw_fd()
.ok_or_else(|| std::io::Error::other("PTY master has no fd"))?;
unsafe {
let flags = libc::fcntl(master_fd, libc::F_GETFL);
if flags < 0 {
return Err(std::io::Error::last_os_error());
}
if libc::fcntl(master_fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
return Err(std::io::Error::last_os_error());
}
}
let dup_afd = |interest| -> std::io::Result<AsyncFd<OwnedFd>> {
let fd = unsafe { libc::dup(master_fd) };
if fd < 0 {
return Err(std::io::Error::last_os_error());
}
AsyncFd::with_interest(unsafe { OwnedFd::from_raw_fd(fd) }, interest)
};
Ok((dup_afd(Interest::READABLE)?, dup_afd(Interest::WRITABLE)?))
})();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

In relay_pty_connection, the code calls master.as_raw_fd() and then attempts to use .ok_or_else(...) on it. However, AsRawFd::as_raw_fd returns a RawFd (which is a primitive i32 on Unix), not an Option<RawFd>. Calling .ok_or_else(...) on a primitive i32 will result in a compilation error.

To fix this, since AsRawFd is implemented for dyn MasterPty on Unix, you can directly use the returned RawFd without wrapping it in an Option check.

    let setup = (|| -> std::io::Result<(AsyncFd<OwnedFd>, AsyncFd<OwnedFd>)> {
        let master_fd = master.as_raw_fd();
        unsafe {
            let flags = libc::fcntl(master_fd, libc::F_GETFL);
            if flags < 0 {
                return Err(std::io::Error::last_os_error());
            }
            if libc::fcntl(master_fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
                return Err(std::io::Error::last_os_error());
            }
        }
        let dup_afd = |interest| -> std::io::Result<AsyncFd<OwnedFd>> {
            let fd = unsafe { libc::dup(master_fd) };
            if fd < 0 {
                return Err(std::io::Error::last_os_error());
            }
            AsyncFd::with_interest(unsafe { OwnedFd::from_raw_fd(fd) }, interest)
        };
        Ok((dup_afd(Interest::READABLE)?, dup_afd(Interest::WRITABLE)?))
    })();

Comment thread src/server.rs
Comment on lines +423 to +424
let shell = std::env::var("SHELL").unwrap_or_default();
CommandBuilder::new(if shell.is_empty() { "/bin/sh".to_string() } else { shell })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

When spawning the default shell, std::env::var("SHELL") is read from the server's environment. If the server is running as a daemon or a system service, SHELL might be unset or set to a shell that doesn't exist or shouldn't be executed. It is safer to default to a known safe shell like /bin/sh or /bin/bash if SHELL is not present, but also consider validating the environment variable or strictly using /bin/sh to avoid executing arbitrary binaries defined in the environment.

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.

1 participant