ssh-handoff

command module
v0.0.0-...-467db85 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 24 Imported by: 0

README

ssh-handoff

ssh-handoff 将用户在终端中建立的 SSH 连接暴露为可复用的本地 session。Agent 通过 session ID 在同一条已认证 transport 上新建 channel 执行命令或传输文件;用户同时持有原始 Shell,连接生命周期由 open 进程管理。

支持 Linux、macOS、WSL 和原生 Windows。Unix 平台使用系统 OpenSSH 的 ControlMaster;Windows 使用 Plink connection sharing,通过 ConPTY 保留完整的交互终端。

安装

从源码构建需要 Go 1.26 或更新版本。Linux、macOS 与 WSL 还需要支持 ControlMaster 和 ControlPath 的系统 OpenSSH 客户端。原生 Windows 需要 Windows 10 1809 或更新版本以及 Plink 0.84 或更新版本;plink.exe 可放在 ssh-handoff.exe 同目录、同目录的 bin 子目录或 PATH 中,也可以通过 SSH_HANDOFF_PLINK 指定完整路径。

可以直接安装最新版:

go install github.com/cyhforlight/ssh-handoff@latest

也可以克隆仓库后,在源码目录中构建:

go build -o ssh-handoff .

在 PowerShell 中构建 Windows 可执行文件:

go build -o ssh-handoff.exe .

快速开始

在一个真实终端中提供目标并建立 SSH 连接:

ssh-handoff open --host example.com --user user

open 会先显示生成的 session ID(如 A3B4)。随后像平时一样完成密码、MFA、扫码、主机指纹确认或堡垒机选机等操作,以进入目标服务器。

随后,在另一个终端中使用 session ID 执行命令:

ssh-handoff run A3B4 'uname -a'

run 在未托管和托管状态下都可使用。未托管时,原始 Shell 可以照常用于人工操作,包括运行 htop、编辑器等交互程序;run 使用独立 channel,不会占用该终端。

ID 输入不区分大小写,a3b4 和 A3B4 指向同一个 session。

托管状态用于原始 Shell 的定时保活。需要启用时,在空闲 Shell 中按 Ctrl-]:

[ssh-handoff] A3B4 已托管;按 Ctrl-] 恢复交互。

托管期间,ssh-handoff 会阻止普通键盘输入,并每 10 分钟向原始 Shell 写入一次空操作保持活跃。再次按 Ctrl-] 可以恢复人工交互。两次切换都应在空 Shell prompt 上进行,不要在 htop、编辑器或其他交互程序中切换。

命令

open
ssh-handoff open --host HOST --user USER [--port PORT] [--identity FILE] [--mode exec|shell-pty] [--note NOTE]
ssh-handoff open --profile NAME [--mode exec|shell-pty] [--note NOTE]

直接连接在所有平台上可用。--host 和 --user 必填,--port 默认为 22;host 可以是域名、IPv4 或 IPv6,ssh-handoff 会原样交给当前平台的 SSH 客户端:

ssh-handoff open --host example.com --user operator
ssh-handoff open --host 2001:db8::10 --user operator --port 2222
ssh-handoff open --host internal.example.com --user operator --identity ~/.ssh/id_ed25519

Linux、macOS 与 WSL 还支持 OpenSSH profile:

ssh-handoff open --profile myserver
ssh-handoff open --profile jump-alias --mode shell-pty --note '生产环境'

profile 等价于执行 ssh myserver,HostName、User、Port、IdentityFile、ProxyJump 等配置继续由 OpenSSH 从 ~/.ssh/config 原生读取。profile 与 direct 参数完全互斥。profile 中的 RemoteCommand 只作用于 open 的原始连接;run 和 copy 的新 channel 会清除它,使用各自的命令或 Shell 请求。

原生 Windows 使用 Plink,不支持 OpenSSH profile、PuTTY saved session 或 OpenSSH-to-PuTTY 配置转换。Windows 上的 --identity 应指向 Plink 能够读取的私钥文件;Unix 上则应使用 OpenSSH 能够读取的格式。主机指纹、密码、MFA、ssh-agent、Pageant 和密钥认证均由底层客户端在原始终端中处理。

open 不提供 IPv4/IPv6、compression、任意 -o 或 extra args。复杂的 Unix OpenSSH 配置应放入 profile。文件传输由连接建立后的 copy 操作提供,不扩张 open 的连接参数。

一个 open 进程对应一个 session。关闭该进程或终端会结束 session。

run
ssh-handoff run [--stream] [--timeout DURATION] <session-id> '<command>'
ssh-handoff run [--stream] [--timeout DURATION] <session-id> - < script.sh

run 通过已认证 transport 新建 channel,同步执行一条非交互命令。默认超时为一分钟:

ssh-handoff run --timeout 2m A3B4 'kubectl get nodes -o wide'

shell-pty session 会在新建命令 channel 后依次发送工作命令和 exit,并保持输入开启,直到远端主动结束 channel 或执行超时。

命令参数为 - 时,ssh-handoff 从本地标准输入读取完整命令文本,适合执行包含多层引号或多行内容的脚本:

ssh-handoff run A3B4 - < deploy.sh

在 PowerShell 中可以使用:

Get-Content -Raw -Encoding utf8 .\deploy.sh | .\ssh-handoff.exe run A3B4 -

标准输入只用于提供命令文本;读取完成后,命令仍作为一次非交互执行发送,不会继续向远端程序转发输入。

需要提权的命令应使用非交互模式:

ssh-handoff run A3B4 'sudo -n systemctl restart myapp'

sudo -n 的权限错误会直接返回。run 超时只终止本地 SSH 调用,远端进程可能继续运行;超时后先检查远端进程和已有状态,再决定是否重试。

长时间运行的命令可以加 --stream,让输出在命令结束前持续返回:

ssh-handoff run --stream --timeout 10m A3B4 'docker compose pull'

流式模式使用 NDJSON,每行都是一条完整 JSON 事件。exec session 产生 stdout 和 stderr 事件,shell-pty session 产生 output 事件;最后一行只会是一个 result 或 error 事件。已经发送的输出不会在 result 中重复:

{"type":"stdout","data":"pulling api\n"}
{"type":"stderr","data":"warning: retrying\n"}
{"type":"result","session":"A3B4","mode":"exec","exit_code":0,"timed_out":false}

默认模式在命令结束后输出单个 JSON 结果,适合只关心最终状态的短命令。

copy
ssh-handoff copy upload <session-id> <local-path> <remote-path>
ssh-handoff copy download <session-id> <remote-path> <local-path>

copy 通过 exec session 的独立 command channel 传输单个普通文件。上传把本地文件作为远端命令的原始 stdin,下载把远端命令的原始 stdout 写入本地文件,因此文本和二进制内容都不会经过 JSON、UTF-8 或换行转换:

ssh-handoff copy upload A3B4 ./app.tar /tmp/app.tar
ssh-handoff copy download A3B4 /var/log/app.log ./app.log

本地和远端目标都先写入同目录的临时文件。上传确认本地读取和临时文件传输均成功后,再通过一个独立 command channel 提交远端文件;下载确认本地写入和传输成功后替换本地目标。这样本地读写失败不会发布不完整文件。上传异常中断或共享连接丢失时,可能留下 .ssh-handoff- 临时文件。

远端需要 POSIX Shell 以及 cat、mv、rm;下载还要求 exec channel 的 stdout 只包含命令输出。远端路径按字面值处理,可以是绝对路径或相对于远端初始目录的相对路径;不展开 ~、环境变量或通配符。不支持目录、文件属性、进度和断点续传;源路径指向符号链接时传输其目标文件的内容。

list
ssh-handoff list

列出仍然存活的 session,包括 ID、规范化连接信息、备注和执行模式。

close
ssh-handoff close A3B4

关闭指定 session 及其 SSH transport。ID 输入不区分大小写;该 session 中正在执行的 run 可能随之中断。

帮助

使用 ssh-handoff --help 查看命令总览,或使用 ssh-handoff <command> --help 查看子命令帮助。

工作模型

一个 session 持有一条已经完成认证的底层 SSH transport,它可以同时承载多个相互独立的 channel:

已认证的 SSH transport(OpenSSH ControlMaster / Plink upstream)
├── 原始 Shell channel:人工操作、托管切换和定时保活
├── 命令 channel:每次 run 单独新建
└── 文件 channel:每次 copy 单独新建 exec channel

run 和 copy 的数据不会进入原始终端,托管状态只作用于原始 Shell channel。每次操作都新建独立 channel,可以在同一 session 上并发;有先后依赖的操作应顺序发起。channel 在调用结束后关闭,不继承原始 Shell 或上一次操作的工作目录、环境变量和历史状态。新 channel 的初始目录、环境和 Shell 初始化由远端 SSH 服务决定;需要共享状态的多个步骤应组合在同一个命令字符串中,并显式包含所需的 cd 和环境变量。

session 属于创建它的本地系统用户。Agent 需要以同一用户运行,并能访问相同的 runtime 目录及 OpenSSH control socket 或 PuTTY connection-sharing 命名管道;未共享这些路径的容器、其他 Windows 安全令牌、WSL 实例或隔离环境无法发现该 session。

PTY(伪终端)让远端程序以连接到终端的方式运行,输出中可能包含输入回显、Shell prompt 和控制字符。原始 Shell channel 使用 PTY,以保留用户操作终端所需的交互能力。部分堡垒机或设备入口拒绝标准 SSH exec 请求,只接受带 PTY 的 Shell channel;shell-pty session 用于兼容这类入口。

open --mode 创建下面两类 session 之一,并固定该 session 后续命令 channel 的类型;run 没有 mode 参数。

exec session

open 默认创建 exec session。此类 session 的命令 channel 不分配 PTY,工作命令通过 SSH exec 请求发送。stdout、stderr 和命令退出状态可以分别取得。

shell-pty session

使用 open --mode shell-pty 创建此类 session。它的命令 channel 请求 PTY 并启动远端 Shell,随后输入预先给定的工作命令和 exit,并保持输入开启直到 Shell 会话结束或超时。这个 Shell 用于适配远端入口;ssh-handoff 不会在执行期间继续转发用户输入,因此工作命令仍须能够一次性、非交互地完成。需要持续输入、菜单操作或全屏终端的程序应在原始 Shell 中运行。

输出

run

run 的结果结构取决于 session 的执行模式。

以下单个 JSON 结构用于未指定 --stream 的默认模式。流式模式的 NDJSON 事件见 run 命令说明。

exec

exec session 成功执行后的结果分别提供 stdout 和 stderr 字段:

{
  "session": "A3B4",
  "mode": "exec",
  "stdout": "server\n",
  "stderr": "",
  "exit_code": 0,
  "timed_out": false
}

exit_code 正常情况下等于远端工作命令的退出状态;SSH 客户端自身发生错误时通常为 255。

本地 timeout 无法证明远端命令已经结束,因此超时时结果会增加 warning 字段,提示先检查远端进程状态再决定是否重试。

shell-pty

shell-pty session 成功执行后的结果使用单一的 output 字段:

{
  "session": "A3B4",
  "mode": "shell-pty",
  "output": "server\r\n",
  "exit_code": 0,
  "timed_out": false
}

shell-pty 的 output 记录该命令 channel 从 Shell 启动到关闭期间产生的完整终端流,可能包含启动信息、命令回显、提示符、工作命令输出、exit 回显和控制字符,具体内容由远端入口和 Shell 决定。exit_code 表示整个 PTY Shell 会话的退出状态,只用于辅助判断会话是否正常结束;该模式不提供工作命令的退出状态。

run 的参数错误、session 相关错误和本地执行错误使用统一的 JSON 结构:

{
  "error": {
    "code": "session_not_found",
    "message": "session not found: A3B4"
  }
}

错误代码包括 invalid_arguments、session_not_found、session_unavailable、execution_error 和 local_error。run 超时时返回 timed_out: true 和一条远端状态提示,ssh-handoff 进程以状态码 124 退出。

copy

成功上传或下载后输出一行确认文本:

copied upload via A3B4

失败信息写入 stderr,并以非零状态退出。传输失败时不会替换已有目标文件。

list

list 成功时输出表格,例如:

ID    MODE  CONNECTION                   NOTE
A3B4  exec  user@example.com:22          生产环境
B5C6  exec  profile internal-production

list 失败时将错误写入 stderr,并以非零状态退出。

close

成功关闭 session 后输出:

closed A3B4

close 失败时将错误写入 stderr,并以非零状态退出。

Agent Prompt

可以直接把 session ID 和下面这段提示词交给 Agent:

请使用本机的 ssh-handoff 操作我已经认证的 SSH session <SESSION_ID>。

- 使用 `ssh-handoff run [--timeout DURATION] <SESSION_ID> '<command>'` 执行普通命令;长时间运行的命令加 `--stream` 实时接收 NDJSON;复杂或多行命令可通过 `ssh-handoff run <SESSION_ID> - < SCRIPT` 从本地标准输入读取。
- 使用 `ssh-handoff copy upload <SESSION_ID> <LOCAL> <REMOTE>` 上传单个文件,使用 `ssh-handoff copy download <SESSION_ID> <REMOTE> <LOCAL>` 下载单个文件;copy 只支持 exec session。
- 如果没有给出 session ID,先运行 `ssh-handoff list`,根据 ID、连接信息和备注识别;无法确定时询问我。
- 命令应包含所需的 `cd`、环境变量和完整参数,并能够非交互执行。
- 需要提权时使用 `sudo -n`,权限失败时告诉我。
- `timed_out: true` 时检查远端实际状态,再决定是否重试。
- 默认模式根据单个 JSON 结果判断;`--stream` 模式持续处理输出事件,并以最后的 `result` 或 `error` 事件判断完成状态。
- 任务完成后保持 session 存活。

设计文档

项目目标、核心语义和范围说明见 docs/PROJECT.md。

Documentation

The Go Gopher

There is no documentation for this package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL