Files
VoiletSshTools/README.zh-CN.md
T
liyp c3af8b4ab9 docs: README 改为英文默认,中文版移到 README.zh-CN.md
- README.md 英文,顶部有简体中文链接
- README.zh-CN.md 中文,顶部有 English 链接
- 补上安装/发布下载、macOS quarantine、CI 只跑 GitHub 的说明
2026-10-23 13:19:06 +08:00

8.9 KiB
Raw Permalink Blame History

sshtools

面向脚本的非交互 SSH 命令行工具:golink 对应 PuTTY 的 plink,goscp 对应 pscp 的上传方向。

纯 Go、CGO_ENABLED=0,每个平台一个静态二进制。SSH 协议栈和全部密码学(RSA、ECDSA、Ed25519、 AES-GCM、ChaCha20-Poly1305、SHA-2)都是 Go 自己的实现,编译进二进制里 —— 不链接系统 OpenSSL, 也不需要额外安装任何东西。

English | 简体中文

安装

从 最新发布 下载对应平台的二进制,chmod +x 即可。

平台 golink goscp
Windows x86-64 golink_windows_amd64.exe goscp_windows_amd64.exe
Windows ARM64 golink_windows_arm64.exe goscp_windows_arm64.exe
Linux x86-64 golink_linux_amd64 goscp_linux_amd64
Linux ARM64 golink_linux_arm64 goscp_linux_arm64
macOS Intel golink_darwin_amd64 goscp_darwin_amd64
macOS Apple Silicon golink_darwin_arm64 goscp_darwin_arm64

每次发布都附带 SHA256SUMS。

macOS 上从网上下载的二进制带 quarantine 属性,执行一次 xattr -d com.apple.quarantine ./golink 即可。

两个工具共同遵守的约定

  • 从不提示:不问密码、不问主机密钥、不问任何东西;
  • 从不分配终端,也不打开交互式 shell;
  • stdout 只放远端程序的标准输出,诊断信息一律走 stderr;
  • 退出码有明确含义(见下文);
  • 所有凭据都要提前给,所以可以放心放进 cron 和 CI。

连上去、跑一条命令、把远端退出码原样带回来。

golink user@host -p "password" -port 22 -command "command"

Go 标准库的 flag 遇到第一个位置参数就停止解析,所以 golink 在解析前把 argv 重排了一遍: 目标在最前、最后、或者中间都行,flag 的顺序也无所谓。

示例

# 最普通的一条
golink root@10.0.0.5 -p 'secret' -port 22 -command "uptime && df -h"

# 密码走环境变量,不出现在 ps 和 shell history 里
GOLINK_PASSWORD='secret' golink deploy@web1 -command "systemctl restart nginx"

# 远端退出码原样返回,脚本里可以直接判断
if golink ci@build1 -p "$PASS" -command "make test"; then echo 通过; else echo "失败: $?"; fi

# 本地 stdin 直接喂给远端命令
tar czf - ./src | golink root@backup -p "$PASS" -command "cat > /srv/src.tgz"

# 把远端输出接进本地管道
golink db@10.0.0.9 -p "$PASS" -command "cat /var/log/app.log" | grep ERROR

# 密钥登录(OpenSSH 格式私钥)
golink ops@bastion -i ~/.ssh/id_ed25519 -command hostname

# 写死主机指纹,避免中间人
golink ops@10.0.0.7 -p "$PASS" -hostkey "$(ssh-keyscan -p 22 10.0.0.7 2>/dev/null | ssh-keygen -lf - | awk '{print $2}')" -command true

# 命令也可以当位置参数跟在后面(plink 风格)
golink ops@web1 -p "$PASS" systemctl status nginx

参数

参数 默认 说明
-p / -password / -pw 空 SSH 密码;也可以用环境变量 GOLINK_PASSWORD
-port / -P 22 SSH 端口;也可以写在目标里 host:2222
-l 空 登录用户,等价于 user@host 里的 user
-command 空 远端执行的命令;后面的位置参数会自动拼上去
-i 空 OpenSSH 格式私钥文件(不支持带口令的私钥,见下)
-knownhosts 空 known_hosts 文件;未知或变更的主机密钥直接失败,不会提示
-hostkey 空 要求的主机指纹,SHA256:... 或 MD5 均可
-timeout 15s 只覆盖「建连 + 握手 + 认证」,命令跑多久都不受它限制
-v 关 把连接、主机指纹、认证过程打到 stderr,不污染 stdout
-version / -h 版本 / 帮助

目标写法支持 user@host、user@host:2222、[::1]:2222。

退出码

退出码 含义
0-255 远端命令的退出码原样返回(被信号杀死时按 shell 惯例返回 128+信号,如 SIGTERM → 143)
2 用法错误:参数写错、没写用户、没写命令、没给任何凭据
255 连接、主机密钥或认证失败(和 OpenSSH 一致)

stdout 只放远端命令的标准输出,远端 stderr 走本地 stderr,-v 的诊断信息也走 stderr, 所以 golink ... | ... 和 golink ... > out.txt 都是安全的。

goscp

把本地文件或目录传到远端,走 SCP 协议(远端跑 scp -t),不依赖 SFTP 子系统。

goscp [flags] [user@]host[:port] -files "dir or file" [-files ...] [-dest /remote/dir]

一个 -files 同时管文件和目录:它看路径本身,是文件就发一个条目,是目录就递归整棵树。 没有 -r 开关,也没有第二种模式。

示例

# 传一个文件到远端家目录
goscp user@host -p "$PASS" -port 22 -files "build/output.bin"

# 传整个目录到指定位置(远端目录不存在会自动建)
goscp user@host -p "$PASS" -files "build" -dest /home/user/app

# 多个文件:重复写,或者跟在后头
goscp user@host -p "$PASS" -files a.txt -files b.txt -dest /srv/in
goscp user@host -p "$PASS" -files a.txt b.txt -dest /srv/in

# 密码走环境变量
GOSCP_PASSWORD='secret' goscp user@host -files ./dist/

参数

参数 默认 说明
-files 空 要上传的本地文件或目录,自动识别;可重复,尾随位置参数也算
-dest 远端家目录 远端目标目录,不存在会自动 mkdir -p
-p / -password / -pw 空 SSH 密码;也可用 GOSCP_PASSWORD / GOLINK_PASSWORD
-port / -P 22 SSH 端口
-l 空 登录用户
-i 空 OpenSSH 格式私钥
-knownhosts / -hostkey 空 主机密钥策略
-timeout 15s 建连 + 认证超时,不含传输
-v 关 逐条列出上传的文件和大小
-q 关 不打印最后那行汇总

退出码

退出码 含义
0 全部成功
1 传输失败(本地路径不存在、远端 scp 报错、权限不足)
2 用法错误(参数、目标、没给文件、没给凭据)
255 连接、主机密钥或认证失败

行为说明

  • 目录保留名字和相对结构:-files build -dest /srv → /srv/build/...
  • 权限:普通文件 0644(本地有执行位则 0755),目录 0755
  • 符号链接:命令行上直接点名的会跟随;目录内部遇到的跳过并在 stderr 提示
  • 文件名含空格没问题;含换行的不支持
  • 传输是流式的,测试里逐字节校验过

主机密钥策略(重要)

默认不校验主机密钥。脚本无法回答「是否信任这台主机」,所以行为等同 plink -batch: 接受并继续,代价是理论上可被中间人攻击。

在意的话就钉死:

  • -hostkey SHA256:... —— 指纹不符直接失败;
  • -knownhosts ~/.ssh/known_hosts —— 未知或变更直接失败,绝不提示。

安全提示

密码写在命令行会出现在 ps 和 shell history 里,和 plink -pw 一样。优先用 GOLINK_PASSWORD / GOSCP_PASSWORD 或 -i 密钥。

带口令的私钥会被明确拒绝并给出提示:这两个工具从不交互提问,请先用 ssh-keygen -p 去掉口令。

编译

./build.sh          # 所有程序 × 所有平台,产物在 dist/
./build.sh local    # 只编当前平台
VERSION=1.2.3 ./build.sh

唯一的依赖是 golang.org/x/crypto,只要 module cache 有货,离线也能编。 build.sh 把 GOCACHE 指到仓库内的 .gocache/,因为有些沙箱里 ~/.cache/go-build 是只读的。

推一个 v* tag 会触发 GitHub Actions:先跑两套测试,再交叉编译并把二进制挂到 GitHub Release。 Gitea 也会识别 .github/workflows,所以 job 里带了 if: github.server_url == 'https://github.com' 的判断,在 Gitea 上保持空转。

自测

./smoketest/run.sh        # golink,38 项
./smoketest/run-goscp.sh  # goscp,19 项

两套都在本地跑:编一个迷你 sshd(smoketest/sshd,支持 password / keyboard-interactive / publickey 三种认证),通过回环驱动。每个用例都关掉 stdin 并加了超时,任何等待交互输入的行为 都会表现为失败而不是挂起。

目录结构

cmd/golink/          命令行入口:参数、退出码映射
cmd/goscp/           命令行入口
internal/cli/        参数重排,让 flag 可以出现在任意位置
internal/sshx/       建连、认证、主机密钥策略
internal/scp/        SCP 协议客户端
smoketest/sshd/      测试用迷你 SSH 服务端
smoketest/run.sh     golink 端到端测试
smoketest/run-goscp.sh
build.sh             六个目标的交叉编译

还没做

  • goscp 的下载方向(远端 → 本地)
  • 断点续传、并行传输、进度显示
  • SFTP 后端(会引入依赖,目前 SCP 让二进制保持自包含)
  • 跳板机(-J)、端口转发、交互式 shell / pty