feat(pam tunnel): add --foreground, --background, --run flags and cross-process tunnel registry#1848
Open
msawczynk wants to merge 8 commits intoKeeper-Security:masterfrom
Open
Conversation
…rsistence Co-authored-by: Cursor <cursoragent@cursor.com>
…teractive shell detection, --pid-file, improved status output Made-with: Cursor
…tch mode fix - Add --run / -R flag: start tunnel, wait for connection, execute command, stop tunnel, exit with commands exit code - Add --timeout flag (default 30s) for WebRTC connection wait - Wait for tunnel connection in --foreground before printing banner and writing PID file - Raise CommandError instead of input() when --target-host/--target-port are missing in batch mode (fixes script hang) - Import wait_for_tunnel_connection and subprocess Made-with: Cursor
…l list note - Add --background / -bg: fork before tunnel start, child daemonizes (setsid), starts tunnel, signals parent via pipe when connected, then blocks. Parent prints readiness info and returns. Linux/macOS only. - Add tunnel list limitation note to --foreground and --background banners - Addresses user feedback: multiple commands need --background, not --run Made-with: Cursor
… list/stop - Add file-based tunnel registry (~/.keeper/tunnel-sessions/<pid>.json) - Each foreground/background/run tunnel registers on connect, unregisters on cleanup - pam tunnel list now shows tunnels from ALL Commander processes - pam tunnel stop falls back to file registry, sends SIGTERM to owning process - pam tunnel stop --all also stops cross-process tunnels - Stale entries (dead PIDs) are auto-cleaned on list Made-with: Cursor
…oach The os.fork() approach did not work reliably on Linux because the Rust WebRTC library's internal state (threads, FFI objects, network connections) does not survive a fork. Replaced with subprocess.Popen(start_new_session=True) that launches a separate keeper process with --foreground, then polls the file-based tunnel registry for readiness. This also makes --background work on Windows (no os.fork required). Made-with: Cursor
- Atomic registry writes (temp + rename) prevent corruption during concurrent reads - Mutual exclusivity check for --foreground/--background/--run flags - SIGHUP handler for --foreground so terminal closure triggers clean shutdown - Capture stderr from --background subprocess for actionable error messages - Pass params.server to --background subprocess for non-default Keeper servers - Reduce --background polling interval from 1s to 0.5s for better responsiveness - Initialize cmd_exit before try block in --run to prevent NameError edge case - Extract _stop_tunnel_process() for cross-platform process termination - Debug logging for corrupt registry files Made-with: Cursor
…l removal) Move the file-based tunnel registry from ~/.keeper/tunnel-sessions/ to <tempdir>/keeper-tunnel-sessions/ so that deleting or replacing Keeper credentials no longer orphans running tunnel metadata. The system temp directory is cleared on reboot, matching the tunnel lifecycle. On Unix the directory is created with mode 0700 for isolation. Made-with: Cursor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add
--foreground/-fg,--pid-file,--run/-R, and--timeoutflags topam tunnel startthat enable tunnels to be used in non-interactive contexts: shell scripts, systemd services, CI/CD pipelines, and headless automation -- without requiring an interactivekeeper shellsession.--foreground: Blocks the Commander process after the tunnel connects, keeping it alive untilSIGTERM/SIGINT/Ctrl+C. Now waits for the WebRTC connection to reach"connected"state before printing the status banner and writing the PID file.--pid-file: Writes the process PID to a file for external signal-based management.--run <COMMAND>: Starts the tunnel, waits for connection, executes the given shell command, stops the tunnel, and exits with the command's exit code. Ideal for single-script workflows.--timeout <SECONDS>: Controls how long to wait for the tunnel to connect (default: 30s). Used with--foreground,--background, and--run.--background/-bg: Launches a separate Commander process with--foreground, polls the file-based tunnel registry for readiness, then returns control to the caller. The tunnel continues running as an independent background process. Works on all platforms.--target-host/--target-portare missing in non-interactive mode, raisesCommandErrorinstead of callinginput()(which hangs in batch/script contexts).<tempdir>/keeper-tunnel-sessions/) enablespam tunnel listto discover tunnels from any Commander session.pam tunnel stop <RECORD_UID>sendsSIGTERMto the owning process. Stale entries (dead PIDs) are auto-cleaned.Motivation
Currently,
pam tunnel startworks only inside an interactivekeeper shellsession because tunnels run as in-process background threads (spawned bystart_rust_tunnel()viakeeper_pam_webrtc_rs). When Commander is invoked in single-command mode (keeper "pam tunnel start <UID>"),PAMTunnelStartCommand.execute()returns after starting the tunnel, Commander exits, and all tunnel threads die with the process.This is a common pain point for users automating infrastructure with Keeper PAM:
pg_dumpthrough a tunnel)Changes
File:
keepercommander/commands/tunnel_and_connections.py(1 file changed)New imports:
signal,subprocess,threading(all stdlib);unregister_tunnel_session,wait_for_tunnel_connection(fromtunnel_helpers)New arguments on
PAMTunnelStartCommand.pam_cmd_parser:--backgroundlogic (subprocess-based, beforestart_rust_tunnel()):keepercommand with--foregroundand all user-supplied flagssubprocess.Popen(start_new_session=True)as an independent process<tempdir>/keeper-tunnel-sessions/) for readiness--runlogic (new branch, checked before--foreground):wait_for_tunnel_connection(result, timeout=connect_timeout)subprocess.run(command, shell=True)close_tube()+unregister_tunnel_session()sys.exit(proc.returncode)KeyboardInterrupt: exits with code 130--foregroundconnection readiness: Before printing the status banner and writing the PID file, callswait_for_tunnel_connection(result, timeout=connect_timeout)to ensure the tunnel is actually usable.Interactive shell detection: When
--foregroundor--backgroundis used insidekeeper shell(interactive mode,batch_mode=False), the blocking logic is skipped and a message informs the user that tunnels already persist in the shell.File-based tunnel registry: New module-level functions (
_register_tunnel,_unregister_tunnel,_list_registered_tunnels,_tunnel_registry_dir,_is_pid_alive) manage JSON metadata files in<tempdir>/keeper-tunnel-sessions/. Each foreground/background/run tunnel registers on connect and unregisters on cleanup.Enhanced
PAMTunnelListCommand: Now reads both the in-processPyTubeRegistryand the file-based registry. Cross-process tunnels appear in the listing with their mode and PID. Stale entries (dead PIDs) are auto-cleaned.Enhanced
PAMTunnelStopCommand: When a tunnel is not found in the in-process registry, falls back to the file registry and sendsSIGTERMto the owning process.--allalso stops cross-process tunnels.Batch mode fix for
--target-host/--target-port: Whenparams.batch_modeisTrueand these values are missing, raisesCommandErrorinstead of callinginput(), which would hang in scripts.No other files are modified. No new external dependencies are introduced. Only Python stdlib modules (
json,signal,subprocess,threading,time) are used.Usage
Testing
--foregroundaccepted -- argument parser recognizes the flag-fgshorthand accepted -- short form worksFalse-- without the flag,foregroundisFalse--pid-fileaccepted -- argument parser recognizes the flag--pid-filedefaults toNone-- without the flag,pid_fileisNone--runaccepted -- argument parser recognizes the flag, stores string value--rundefaults toNone-- without the flag,run_commandisNone--runwith--port----runand--portwork together--timeoutaccepted -- argument parser recognizes the flag, stores int value--timeoutdefaults to 30 -- without the flag,connect_timeoutis30--timeoutcustom value ----timeout 120setsconnect_timeoutto120--backgroundaccepted -- argument parser recognizes the flag-bgshorthand accepted -- short form works--backgrounddefaults toFalse-- without the flag,backgroundisFalse--backgroundwith--pid-file-- both flags work together--host,--port,--no-trickle-ice,--target-host,--target-portall work alongside new flagsunregister_tunnel_session-- importable fromtunnel_and_connections_stop_tunnel_process-- importable fromtunnel_and_connections--foreground,--background,--runall parse individually but runtime rejects combinations--foregroundis set--runexecutes and exits -- tunnel starts, command runs, tunnel stops, exit code propagated<tempdir>/keeper-tunnel-sessions/tunnel listtunnel listshows cross-process tunnels -- tunnels visible from any Commander sessiontunnel stopstops cross-process tunnels -- sends SIGTERM to owning process--backgrounddaemonizes and returns -- tunnel starts, waits, daemonizes, returns prompt--foregroundwaits for connection -- status banner only prints after WebRTC connection is establishedkill -SIGTERMtriggersclose_tube()and process exits 0KeyboardInterrupttriggers the same clean shutdown--foregroundinsidekeeper shellprints info message, does not block--target-host/--target-portin batch mode raisesCommandErrorType=simplesystemd unit starts, runs, and stops cleanlyBackward Compatibility
--foreground,--pid-file,--run,--timeout,--background) are optional and default toFalse/None/None/30/Falseexecute()method follows the exact same code path as before (the replacedpasswas a no-op)--target-host/--target-portonly triggers whenparams.batch_modeisTrueand the resource requires host/port supply -- interactive shell users still get theinput()promptPAMTunnelListCommandandPAMTunnelStopCommandare enhanced to read the file-based registry but continue to work identically for in-process tunnelsPAMTunnelEditCommand,PAMTunnelDiagnoseCommand, or any other commandjson,signal,subprocess,threading,timefrom stdlibPAMTunnelCommandgroup command registration--foreground,--background, and--runare mutually exclusive (enforced at runtime)