Skip to content

[client] Support non-PTY no-command interactive SSH sessions#5093

Merged
lixmal merged 11 commits intomainfrom
ssh-no-tty-no-cmd
Jan 27, 2026
Merged

[client] Support non-PTY no-command interactive SSH sessions#5093
lixmal merged 11 commits intomainfrom
ssh-no-tty-no-cmd

Conversation

@lixmal
Copy link
Copy Markdown
Collaborator

@lixmal lixmal commented Jan 12, 2026

Describe your changes

  • Support ssh -T <host> (interactive login without PTY):
    • Windows runs this the same way as -t, but without ConPTY
    • Unix runs this via su -l (with fallback to the executor / privilege dropper) instead of login
  • Add missing session field to some log messages
  • Remove obsolete Interactive field

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • New Features

    • Simplified session flow with a dedicated PTY login path and unified execution path for commands and non-PTY shells.
    • More consistent logging context across platforms for better traceability.
  • Bug Fixes

    • Sessions now attach to backend shells, wait for shell exit, and correctly propagate exit codes.
    • Improved PTY lifecycle, startup/error reporting, and resource cleanup on both Unix and Windows.
    • Better cross-platform user-switching and environment handling.
  • Tests

    • Added comprehensive PTY-mode and executor-emulation tests.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 12, 2026

📝 Walkthrough

Walkthrough

Refactors SSH session dispatch and command execution: introduces logger propagation across executor/SU/S4U flows, renames handlers (handlePty → handlePtyLogin, handleCommand → handleExecution), changes non‑interactive proxy lifecycle to shell‑driven, revises PTY lifecycle/cleanup, and adds extensive PTY tests.

Changes

Cohort / File(s) Summary
Proxy Session Handling
client/ssh/proxy/proxy.go
Non‑interactive sessions now attach backend Stdin/Stdout/Stderr, start a backend shell, wait for shell exit or client context, and propagate shell exit codes via handleProxyExitCode instead of immediately waiting on session context.
Session Dispatch
client/ssh/server/session_handlers.go, client/ssh/server/session_handlers_js.go
Consolidates dispatch to two paths: PTY login → handlePtyLogin, otherwise → handleExecution; removes prior multi‑branch handlers and related logic.
Execution Entry & Command Creation
client/ssh/server/command_execution.go, client/ssh/server/command_execution_js.go, client/ssh/server/command_execution_unix.go, client/ssh/server/command_execution_windows.go
Renames handleCommandhandleExecution, adds PTY/window params, and threads logger *log.Entry through createCommand, createSuCommand, createExecutorCommand, and prepareCommandEnv; updates PTY vs non‑PTY execution flows.
PTY Management & Cleanup (Unix)
client/ssh/server/command_execution_unix.go, client/ssh/server/userswitching_unix.go
Adds createShellCommand, renames handlePtyhandlePtyLogin, propagates ptyMgr into completion handlers, changes PTY IO cleanup ordering, and uses logger in SU/executor paths.
PTY / ConPty (Windows)
client/ssh/server/winpty/conpty.go, client/ssh/server/command_execution_windows.go
ExecutePtyWithUserToken no longer takes explicit context (uses session.Context()); threads logger into Windows PTY/token/env flows and replaces global log calls with injected logger.
Privilege Dropping / Executors
client/ssh/server/executor_unix.go, client/ssh/server/executor_windows.go
Introduces logger‑aware PrivilegeDropper with functional options (NewPrivilegeDropper(WithLogger(...))), adds log() helper, and propagates logger through S4U/executor code paths; adjusts shell invocation when no command provided.
User Switching (Windows)
client/ssh/server/userswitching_windows.go
createExecutorCommand/createUserSwitchCommand now accept a logger and forward it to privilege dropper and env prep; removes Interactive field usage in WindowsExecutorConfig construction.
Port Forwarding & Server Minor
client/ssh/server/port_forwarding.go, client/ssh/server/server.go
Removes unused isPortForwardingEnabled() and updates a comment in GetStatus.
Tests & Compatibility
client/ssh/server/server_config_test.go, client/ssh/server/compatibility_test.go, client/ssh/server/server_test.go
Renames/simplifies non‑PTY shell test, adds TestSSHPtyModes and runTestExecutor for PTY mode coverage and exit‑code behavior, and adjusts shell argument expectations for Unix tests.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Proxy
  participant Backend as BackendSession
  participant Shell

  Client->>Proxy: start non-interactive SSH session
  Proxy->>Backend: create backend session
  Proxy->>Backend: attach Stdin/Stdout/Stderr
  Backend->>Shell: start shell
  alt shell started
    Note over Proxy,Shell: Proxy waits for client ctx done or shell exit
    Shell-->>Backend: shell exits (code)
    Backend-->>Proxy: report exit code
    Proxy-->>Client: close session, return exit code
  else start failed
    Backend-->>Proxy: start error
    Proxy-->>Client: log and propagate exit via handleProxyExitCode
  end
Loading
sequenceDiagram
  participant Client
  participant Server
  participant PTYMgr as PTY Manager / WinPTY
  participant Executor

  Client->>Server: SSH session (pty? / command?)
  alt PTY requested and no command
    Server->>PTYMgr: handlePtyLogin (create PTY, winCh)
    PTYMgr->>Executor: execute login shell
    Executor-->>PTYMgr: command exits
    PTYMgr-->>Server: cleanup & report exit
    Server-->>Client: close session
  else command or non-PTY
    Server->>Server: handleExecution (createCommand with logger)
    Server->>Executor: createExecutorCommand / createSuCommand (with logger)
    Executor-->>Server: cmd started / exit code
    Server-->>Client: return output & exit code
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • pascal-fischer
  • pappz
  • mlsmaycon

Poem

🐇 I hopped through logs both near and far,

threaded whiskers into every char.
PTY doors opened, shells took their bow,
exit codes carried home — I knew just how!
A tiny rabbit cheers the new, logged flow.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title '[client] Support non-PTY no-command interactive SSH sessions' clearly and specifically summarizes the main feature being added, matching the core changes throughout the codebase.
Description check ✅ Passed The PR description covers the key changes (ssh -T support, log improvements, Interactive field removal) and includes the required checklist items, though it lacks a specific issue ticket number and docs PR URL.

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

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@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: 0

🧹 Nitpick comments (9)
client/ssh/server/server_config_test.go (1)

548-551: Test validates basic functionality but could be strengthened.

The test verifies that non-PTY shell sessions don't error out, which validates the core fix. However, it doesn't confirm the shell is actually functional or produces expected output.

Consider verifying the session output to ensure the shell was properly initialized and can accept/return data, especially since the PR mentions platform-specific behavior (Windows runs without ConPTY, Unix uses su -l with fallback).

client/ssh/server/command_execution_windows.go (2)

245-258: Inconsistent logger usage in fallback path.

Line 249 uses the global log.Debugf while the logger parameter is available in scope. For consistency with the logger-threading pattern introduced in this PR, use the injected logger.

Suggested fix
 func (s *Server) prepareCommandEnv(logger *log.Entry, localUser *user.User, session ssh.Session) []string {
 	username, domain := s.parseUsername(localUser.Username)
 	userEnv, err := s.getUserEnvironment(logger, username, domain)
 	if err != nil {
-		log.Debugf("failed to get user environment for %s\\%s, using fallback: %v", domain, username, err)
+		logger.Debugf("failed to get user environment for %s\\%s, using fallback: %v", domain, username, err)
 		env := prepareUserEnv(localUser, getUserShell(localUser.Uid))

315-320: Consider refactoring to avoid empty Server instantiation.

Creating an empty &Server{} solely to call getUserEnvironmentWithToken is a code smell. The method doesn't appear to use any Server state. Consider making getUserEnvironmentWithToken a standalone function or passing the required dependencies directly.

client/ssh/server/sftp_windows.go (1)

34-38: Consider passing a logger for consistency.

Unlike the Unix SFTP subprocess case, this runs in the server process where session context is available. For consistency with other Windows privilege-dropping paths that thread loggers (e.g., executePtyCommandWithUserToken), consider accepting and passing a logger here.

client/ssh/server/command_execution_unix.go (1)

140-149: Consider removing unused logger parameter or documenting intent.

The logger parameter is accepted but unused (_). If this is for API consistency or future use, consider adding a brief comment. Otherwise, if no logging is planned here, the parameter could potentially be removed from this platform-specific implementation.

client/ssh/server/command_execution.go (1)

79-80: Log message may be misleading when UsedFallback is true but err is nil.

When privilegeResult.UsedFallback is true and err is nil, the log message "su command failed" is inaccurate—the su command wasn't attempted or didn't fail; rather, a fallback was already used during privilege checking.

Suggested improvement
 	if err != nil || privilegeResult.UsedFallback {
-		logger.Debugf("su command failed, falling back to executor: %v", err)
+		if err != nil {
+			logger.Debugf("su command failed, falling back to executor: %v", err)
+		} else {
+			logger.Debugf("using executor due to privilege fallback")
+		}
 		cmd, cleanup, err := s.createExecutorCommand(logger, session, localUser, hasPty)
client/ssh/server/executor_windows.go (3)

191-214: Consider extracting isLocalUser as a standalone function.

Creating a PrivilegeDropper instance at line 194 solely to call isLocalUser is somewhat indirect since isLocalUser doesn't use the logger field. This could be simplified.

Optional refactor
+// isLocalUser determines if this is a local user vs domain user
+func isLocalUser(domain string) bool {
+	hostname, err := os.Hostname()
+	if err != nil {
+		hostname = "localhost"
+	}
+	return domain == "" || domain == "." ||
+		strings.EqualFold(domain, hostname)
+}

 func generateS4UUserToken(logger *log.Entry, username, domain string) (windows.Handle, error) {
 	userCpn := buildUserCpn(username, domain)
-
-	pd := NewPrivilegeDropper(logger)
-	isDomainUser := !pd.isLocalUser(domain)
+	isDomainUser := !isLocalUser(domain)

Then update the PrivilegeDropper method to call the standalone function if needed.


481-492: Minor inconsistencies: typo and global logger usage.

  1. Line 481: Comment has typo "useVerifier" → should be "user"
  2. Line 484: Uses global log.Debugf instead of a logger parameter, inconsistent with the rest of the file's pattern
Suggested fix
-// userExists checks if the target useVerifier exists on the system
-func userExists(fullUsername, username, domain string) error {
+// userExists checks if the target user exists on the system
+func (pd *PrivilegeDropper) userExists(fullUsername, username, domain string) error {
 	if _, err := lookupUser(fullUsername); err != nil {
-		log.Debugf("User %s not found: %v", fullUsername, err)
+		pd.log().Debugf("User %s not found: %v", fullUsername, err)
 		if domain != "" && domain != "." {
 			_, err = lookupUser(username)
 		}

And update the call site in createToken:

-	if err := userExists(fullUsername, username, domain); err != nil {
+	if err := pd.userExists(fullUsername, username, domain); err != nil {

242-247: Pre-existing global logger usage remains.

For future consistency, cleanupLsaConnection and lookupPrincipalName (lines 292, 311, 316) still use the global log.Debugf. Consider updating these in a follow-up if full logger consistency is desired.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 614e7d5 and 9049447.

📒 Files selected for processing (18)
  • client/cmd/ssh_exec_unix.go
  • client/cmd/ssh_sftp_unix.go
  • client/ssh/proxy/proxy.go
  • client/ssh/server/command_execution.go
  • client/ssh/server/command_execution_js.go
  • client/ssh/server/command_execution_unix.go
  • client/ssh/server/command_execution_windows.go
  • client/ssh/server/executor_unix.go
  • client/ssh/server/executor_unix_test.go
  • client/ssh/server/executor_windows.go
  • client/ssh/server/server.go
  • client/ssh/server/server_config_test.go
  • client/ssh/server/session_handlers.go
  • client/ssh/server/session_handlers_js.go
  • client/ssh/server/sftp_windows.go
  • client/ssh/server/userswitching_unix.go
  • client/ssh/server/userswitching_windows.go
  • client/ssh/server/winpty/conpty.go
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: lixmal
Repo: netbirdio/netbird PR: 4015
File: client/ssh/server/server_test.go:396-406
Timestamp: 2025-11-14T11:11:50.812Z
Learning: On Windows, the NetBird SSH server only supports PowerShell as the shell (powershell.exe or pwsh.exe). cmd.exe and other shells are not supported due to parsing quirks and complexity.
📚 Learning: 2025-11-14T13:05:31.729Z
Learnt from: lixmal
Repo: netbirdio/netbird PR: 4015
File: client/ssh/server/userswitching_windows.go:89-139
Timestamp: 2025-11-14T13:05:31.729Z
Learning: In client/ssh/server/executor_windows.go, the WindowsExecutorConfig struct's Pty, PtyWidth, and PtyHeight fields are intentionally left unused for now and will be implemented in a future update.

Applied to files:

  • client/ssh/server/session_handlers_js.go
  • client/ssh/server/winpty/conpty.go
  • client/ssh/server/executor_unix_test.go
  • client/ssh/server/session_handlers.go
  • client/ssh/server/userswitching_windows.go
  • client/ssh/server/command_execution_unix.go
  • client/ssh/server/command_execution.go
  • client/ssh/server/executor_windows.go
  • client/ssh/server/command_execution_windows.go
📚 Learning: 2025-11-13T00:29:53.247Z
Learnt from: lixmal
Repo: netbirdio/netbird PR: 4015
File: client/cmd/ssh_exec_unix.go:53-74
Timestamp: 2025-11-13T00:29:53.247Z
Learning: In client/ssh/server/executor_unix.go, the method ExecuteWithPrivilegeDrop(ctx context.Context, config ExecutorConfig) has a void return type (no error return). It handles failures by exiting the process directly with appropriate exit codes rather than returning errors to the caller.

Applied to files:

  • client/ssh/server/winpty/conpty.go
  • client/ssh/proxy/proxy.go
  • client/ssh/server/sftp_windows.go
  • client/ssh/server/userswitching_unix.go
  • client/ssh/server/executor_unix_test.go
  • client/ssh/server/server_config_test.go
  • client/ssh/server/executor_unix.go
  • client/cmd/ssh_exec_unix.go
  • client/ssh/server/session_handlers.go
  • client/ssh/server/userswitching_windows.go
  • client/ssh/server/command_execution_js.go
  • client/ssh/server/command_execution_unix.go
  • client/ssh/server/command_execution.go
  • client/cmd/ssh_sftp_unix.go
  • client/ssh/server/executor_windows.go
  • client/ssh/server/command_execution_windows.go
📚 Learning: 2026-01-05T06:33:50.474Z
Learnt from: lixmal
Repo: netbirdio/netbird PR: 5031
File: client/ssh/proxy/proxy.go:537-559
Timestamp: 2026-01-05T06:33:50.474Z
Learning: The NetBird SSH proxy (client/ssh/proxy/proxy.go) is ephemeral and tied to a single SSH client session. It operates through stdio and exists only for the lifetime of one client connection, so session-scoped contexts are appropriate for the proxy's lifecycle.

Applied to files:

  • client/ssh/proxy/proxy.go
  • client/cmd/ssh_exec_unix.go
🧬 Code graph analysis (10)
client/ssh/server/session_handlers_js.go (2)
client/ssh/server/server.go (1)
  • Server (137-175)
client/ssh/server/user_utils.go (1)
  • PrivilegeCheckResult (50-66)
client/ssh/server/userswitching_unix.go (2)
client/ssh/server/executor_unix.go (1)
  • NewPrivilegeDropper (43-45)
client/ssh/server/executor_windows.go (1)
  • NewPrivilegeDropper (46-48)
client/ssh/server/executor_unix_test.go (2)
client/ssh/server/executor_unix.go (1)
  • NewPrivilegeDropper (43-45)
client/ssh/server/executor_windows.go (1)
  • NewPrivilegeDropper (46-48)
client/cmd/ssh_exec_unix.go (2)
client/ssh/server/executor_unix.go (1)
  • NewPrivilegeDropper (43-45)
client/ssh/server/executor_windows.go (1)
  • NewPrivilegeDropper (46-48)
client/ssh/server/userswitching_windows.go (4)
client/ssh/server/server.go (1)
  • Server (137-175)
shared/management/status/error.go (1)
  • Errorf (70-75)
client/ssh/server/executor_unix.go (1)
  • NewPrivilegeDropper (43-45)
client/ssh/server/executor_windows.go (1)
  • NewPrivilegeDropper (46-48)
client/ssh/server/command_execution_js.go (1)
client/ssh/server/server.go (1)
  • Server (137-175)
client/ssh/server/command_execution_unix.go (3)
client/ssh/server/server.go (1)
  • Server (137-175)
shared/management/status/error.go (1)
  • Errorf (70-75)
client/ssh/server/user_utils.go (1)
  • PrivilegeCheckResult (50-66)
client/cmd/ssh_sftp_unix.go (2)
client/ssh/server/executor_unix.go (1)
  • NewPrivilegeDropper (43-45)
client/ssh/server/executor_windows.go (1)
  • NewPrivilegeDropper (46-48)
client/ssh/server/executor_windows.go (1)
client/ssh/server/executor_unix.go (2)
  • NewPrivilegeDropper (43-45)
  • PrivilegeDropper (38-40)
client/ssh/server/command_execution_windows.go (3)
client/ssh/server/executor_unix.go (1)
  • NewPrivilegeDropper (43-45)
client/ssh/server/executor_windows.go (1)
  • NewPrivilegeDropper (46-48)
client/ssh/server/winpty/conpty.go (1)
  • ExecutePtyWithUserToken (59-71)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: Client / Unit
  • GitHub Check: iOS / Build
  • GitHub Check: Android / Build
  • GitHub Check: release_ui
  • GitHub Check: release
  • GitHub Check: Build Cache
  • GitHub Check: release_ui_darwin
  • GitHub Check: Client / Unit
  • GitHub Check: Linux
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
  • GitHub Check: Windows
  • GitHub Check: Darwin
🔇 Additional comments (29)
client/ssh/server/server.go (1)

338-338: LGTM!

The comment update correctly reflects the new behavior: with the added support for ssh -T <host> (non-PTY interactive sessions), those sessions now get proper session handling and are tracked in the sessions map. Only -N and port-forwarding-only connections remain as "authenticated connections without sessions."

client/ssh/server/server_config_test.go (2)

486-487: LGTM!

Test name and description accurately reflect the new purpose: verifying that non-PTY shell sessions (ssh -T) work regardless of port forwarding configuration.


494-518: LGTM!

Good test matrix covering all port forwarding configuration combinations. The simplified struct is appropriate since the expected outcome (success) is now uniform across all test cases.

client/ssh/proxy/proxy.go (1)

217-239: LGTM! Shell-driven lifecycle for non-interactive sessions.

The implementation correctly:

  1. Attaches session I/O streams before starting the shell
  2. Uses a buffered channel to receive the wait result asynchronously
  3. Handles both client context cancellation and shell completion via select
  4. Propagates exit codes properly via handleProxyExitCode

This aligns with the PR objective of supporting ssh -T <host> (interactive login without PTY).

client/ssh/server/winpty/conpty.go (1)

59-71: LGTM! Context sourcing simplified appropriately.

Removing the explicit ctx parameter and deriving it from session.Context() internally is a cleaner API. This aligns with the learning that session-scoped contexts are appropriate for the SSH proxy lifecycle. The change ensures consistent context sourcing across the codebase.

client/ssh/server/command_execution_windows.go (2)

270-281: LGTM! Handler correctly threads logger.

The handlePtyLogin function properly accepts and uses the logger parameter, maintaining consistent logging context throughout the PTY login flow.


300-342: LGTM! PTY execution with proper logger propagation.

The function correctly:

  1. Accepts and uses the logger for tracing and debug output
  2. Creates a new PrivilegeDropper with the logger
  3. Handles token lifecycle with proper cleanup in defer
  4. Delegates to winpty.ExecutePtyWithUserToken with the session
client/cmd/ssh_sftp_unix.go (1)

39-39: LGTM! Nil logger is acceptable for standalone SFTP subprocess.

Passing nil to NewPrivilegeDropper is appropriate here since this is a standalone subprocess invoked via command-line. The subprocess has its own logging via log.Tracef and doesn't need a logger context threaded from a parent session.

client/ssh/server/session_handlers_js.go (1)

12-22: LGTM! Consistent handler naming and signature.

The rename from handlePty to handlePtyLogin aligns with the broader refactor across the PR. The logger parameter is properly utilized for error logging, maintaining API consistency with Unix and Windows implementations.

client/ssh/server/executor_unix_test.go (5)

18-19: LGTM! Test adapted to new constructor signature.

Passing nil for the logger in tests is appropriate since test assertions don't require structured logging context.


76-77: LGTM!


110-111: LGTM!


159-160: LGTM!


229-230: LGTM!

client/ssh/server/session_handlers.go (1)

65-71: Clear and correct session dispatch logic.

The two-path routing correctly handles:

  • PTY interactive login (ssh <host>) → handlePtyLogin
  • All other cases (commands, -t, -T) → handleExecution

This aligns well with the PR objective to support ssh -T <host> for non-PTY interactive sessions.

client/cmd/ssh_exec_unix.go (1)

55-55: LGTM - nil logger is appropriate here.

This is a spawned child process for privilege dropping that communicates via stdout/stderr. Structured logging isn't needed in this context, and the executor handles failures by exiting with specific exit codes. Based on learnings, this aligns with the established pattern.

client/ssh/server/userswitching_unix.go (1)

184-195: Logger propagation looks correct.

The logger parameter is properly threaded through to NewPrivilegeDropper(logger) and used for debug logging. The cleanup function being a no-op on Unix is appropriate since there's no token handle to close (unlike Windows).

client/ssh/server/userswitching_windows.go (3)

91-100: Logger propagation correctly implemented for Windows.

The logger is properly threaded to createUserSwitchCommand and debug logging uses the provided logger instance.


115-121: WindowsExecutorConfig construction simplified.

The Interactive field has been removed as per PR objectives. Based on learnings, the PTY-related fields (Pty, PtyWidth, PtyHeight) are intentionally unused and will be implemented in a future update.


123-138: Cleanup function properly handles token lifecycle.

The cleanup function correctly closes the Windows token handle and logs any errors via the provided logger. This ensures proper resource cleanup after process creation.

client/ssh/server/executor_unix.go (2)

38-53: Well-designed logger integration with safe fallback.

The log() helper method provides a clean pattern for optional logging - returns the injected logger if available, otherwise falls back to the standard logger. This ensures consistent logging behavior across all call sites.


217-233: Login shell execution enables non-PTY interactive sessions.

The change to always use -l (login shell) flag is correct for proper shell initialization. This is the key change that enables ssh -T <host> to work correctly - it spawns a login shell even without PTY allocation. The safe command logging via safeLogCommand() properly protects sensitive command content from being exposed in logs.

client/ssh/server/command_execution_js.go (1)

18-30: LGTM - Stub signatures correctly aligned with other platforms.

The function signatures now accept *log.Entry as the first parameter, maintaining consistency with Unix and Windows implementations. The logger is appropriately ignored in these stubs since the platform doesn't support SSH server command execution.

client/ssh/server/command_execution_unix.go (2)

102-129: Good addition of username validation and debug logging.

The username validation at the start of createSuCommand is a good security practice, though I note that createExecutorCommand in userswitching_unix.go also validates the username. This provides defense-in-depth but consider whether both validation points are necessary.

The debug logging of the constructed su command (line 124) is helpful for troubleshooting while keeping the command arguments visible (unlike the executor path which masks the command content).


162-178: Improved PTY login error handling.

The handlePtyLogin function now provides better user feedback by writing an error message to session.Stderr() before exiting. This helps users understand why their session failed rather than just seeing a disconnection.

client/ssh/server/command_execution.go (1)

15-57: LGTM!

The function properly threads the logger through the command creation and execution paths. The PTY vs non-PTY branching is clean, and error handling appropriately logs and exits with meaningful messages.

client/ssh/server/executor_windows.go (3)

30-40: LGTM!

The struct is clean. Based on learnings, the Pty, PtyWidth, and PtyHeight fields are intentionally reserved for a future update.


42-56: LGTM!

The PrivilegeDropper implementation with logger support is consistent with the Unix implementation pattern shown in executor_unix.go. The log() helper provides a safe fallback when no logger is configured.


575-578: LGTM!

The stub correctly returns an error indicating su is unavailable on Windows, with a signature matching the cross-platform interface.

Comment thread client/ssh/server/executor_unix.go Outdated
Copy link
Copy Markdown
Contributor

@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: 0

Caution

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

⚠️ Outside diff range comments (1)
client/ssh/server/executor_windows.go (1)

205-228: Incorrect PrivilegeDropper instantiation - missing WithLogger wrapper.

On line 208, NewPrivilegeDropper(logger) is called with a *log.Entry directly, but NewPrivilegeDropper expects ...PrivilegeDropperOption variadic arguments. This should use the WithLogger functional option pattern as defined in lines 49-62.

🐛 Proposed fix
-	pd := NewPrivilegeDropper(logger)
+	pd := NewPrivilegeDropper(WithLogger(logger))
🧹 Nitpick comments (2)
client/ssh/server/compatibility_test.go (2)

557-576: Consider portability of bash -c assumption.

The exit_code_preserved_with_pty test uses bash -c 'exit 43' which assumes bash is available. While this is generally true on most Unix systems, it may fail on minimal systems or containers with only sh.

Consider using a more portable approach:

♻️ Suggested alternative
-		args := append(baseArgs, "-tt", fmt.Sprintf("%s@%s", username, host), "bash -c 'exit 43'")
+		args := append(baseArgs, "-tt", fmt.Sprintf("%s@%s", username, host), "sh -c 'exit 43'")

526-532: Stdin write errors not checked in goroutine.

The goroutine writes to stdin but doesn't handle potential write errors beyond logging them implicitly via stdin.Write. If the connection closes unexpectedly, this could cause silent failures.

This is acceptable for test code where the primary assertion is on the output, but consider using a done channel or error capture for more robust test diagnostics.

Copy link
Copy Markdown
Contributor

@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

🤖 Fix all issues with AI agents
In `@client/ssh/server/compatibility_test.go`:
- Around line 567-579: The goroutine that writes to stdin uses t.Errorf which
may run after the test returns (race/panic); replace those t.Errorf calls with
non-test-failing logging (t.Logf) or, preferably, synchronize the goroutine with
the main test using a done channel: create a done := make(chan error) (or chan
struct{}), send any write error into it from the goroutine (or nil on success),
and defer/close stdin inside the goroutine as now; in the main test wait for the
done channel (or select with the test context/timeout) before returning so
cmd.Wait() and the goroutine are ordered and any error is reported from the main
goroutine (use t.Fatalf/t.Errorf there if needed).
🧹 Nitpick comments (2)
client/ssh/server/command_execution_windows.go (2)

23-34: Ensure logger is never nil before use.

These new logger calls will panic if any call site passes a nil *log.Entry. Please confirm callers always supply a logger, or add a safe default (e.g., standard logger) at the entry points.

Also applies to: 38-48, 62-64


245-249: Use the injected logger on the fallback path.

The fallback currently logs via the global logger, which drops the session context. Consider switching to the injected logger for consistency.

♻️ Suggested change
-		log.Debugf("failed to get user environment for %s\\%s, using fallback: %v", domain, username, err)
+		logger.Debugf("failed to get user environment for %s\\%s, using fallback: %v", domain, username, err)

Comment thread client/ssh/server/compatibility_test.go Outdated
@lixmal lixmal force-pushed the ssh-no-tty-no-cmd branch from e72ff73 to 7ada564 Compare January 22, 2026 12:44
Copy link
Copy Markdown
Contributor

@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

🤖 Fix all issues with AI agents
In `@client/ssh/server/compatibility_test.go`:
- Around line 591-602: The test "exit_code_preserved_no_pty" performs an
unchecked type assertion on err to *exec.ExitError which can be nil and cause a
panic; update the test to mirror the safe pattern used in
exit_code_preserved_with_pty by using a two-value type assertion on err (e.g.,
if exitErr, ok := err.(*exec.ExitError); ok { assert.Equal(...,
exitErr.ExitCode()) } else { t.Fatalf(...) }) so ExitCode() is only called when
the assertion succeeds and the test fails with a clear message otherwise.

Comment thread client/ssh/server/compatibility_test.go
Copy link
Copy Markdown
Contributor

@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

🤖 Fix all issues with AI agents
In `@client/ssh/server/compatibility_test.go`:
- Around line 611-657: The three test cases ("exit_code_preserved_with_pty",
"stderr_works_no_pty", "stderr_merged_with_pty") assume a POSIX shell via
hardcoded "sh -c ..."; guard them by checking for shell availability (e.g. use
exec.LookPath("sh")) at the start of each t.Run and call t.Skipf when not found
(or on windows) so they don't run on systems without /bin/sh; update the test
blocks containing these names to perform that check before building args and
invoking exec.Command.

Comment thread client/ssh/server/compatibility_test.go
@lixmal lixmal force-pushed the ssh-no-tty-no-cmd branch from f204d92 to 80d6e42 Compare January 22, 2026 16:10
@lixmal lixmal force-pushed the ssh-no-tty-no-cmd branch from 80d6e42 to 82dc0c6 Compare January 22, 2026 16:25
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
26.2% Duplication on New Code (required ≤ 5%)

See analysis details on SonarQube Cloud

@lixmal lixmal merged commit 06966da into main Jan 27, 2026
37 of 38 checks passed
@lixmal lixmal deleted the ssh-no-tty-no-cmd branch January 27, 2026 10:05
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