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

228 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# sshtools
面向脚本的非交互 SSH 命令行工具:**golink** 对应 PuTTY 的 `plink`,**goscp** 对应 `pscp` 的上传方向。
纯 Go、`CGO_ENABLED=0`,每个平台一个静态二进制。SSH 协议栈和全部密码学(RSA、ECDSA、Ed25519、
AES-GCM、ChaCha20-Poly1305、SHA-2)都是 Go 自己的实现,编译进二进制里 —— 不链接系统 OpenSSL,
也不需要额外安装任何东西。
[English](README.md) | **简体中文**
## 安装
从 [最新发布](../../releases/latest) 下载对应平台的二进制,`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
连上去、跑一条命令、把远端退出码原样带回来。
```bash
golink user@host -p "password" -port 22 -command "command"
```
Go 标准库的 `flag` 遇到第一个位置参数就停止解析,所以 golink 在解析前把 `argv` 重排了一遍:
目标在最前、最后、或者中间都行,flag 的顺序也无所谓。
### 示例
```bash
# 最普通的一条
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 子系统。
```bash
goscp [flags] [user@]host[:port] -files "dir or file" [-files ...] [-dest /remote/dir]
```
一个 `-files` 同时管文件和目录:它看路径本身,是文件就发一个条目,是目录就递归整棵树。
没有 `-r` 开关,也没有第二种模式。
### 示例
```bash
# 传一个文件到远端家目录
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`
去掉口令。
## 编译
```bash
./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 上保持空转。
## 自测
```bash
./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