# SSH_TOOLS Go 实现的 SSH 命令行工具集,对标 PuTTY 的 plink / pscp:单个静态二进制,纯 Go,不依赖本机 OpenSSH,也不需要安装任何运行时。 当前包含 **golink**(对应 plink)和 **goscp**(对应 pscp 的上传方向)。 --- ## golink 非交互 SSH 命令执行器:连上去、跑一条命令、把远端退出码原样带回来。 **从不提示密码、从不询问主机密钥、从不分配终端**,所以可以直接放进脚本和定时任务。 ### 你要的那行,原样可用 ``` golink user@host -p "password" -port 22 -command "命令" ``` Go 标准库的 flag 遇到第一个位置参数就停止解析,所以 golink 在解析前把参数重排了一遍: 位置参数在前、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` 都是安全的。 ### 多平台二进制 | 文件 | 平台 | | --- | --- | | `dist/golink_linux_amd64` | Linux x86-64 | | `dist/golink_linux_arm64` | Linux ARM64 | | `dist/golink_darwin_amd64` | macOS Intel | | `dist/golink_darwin_arm64` | macOS Apple Silicon | | `dist/golink_windows_amd64.exe` | Windows x86-64 | | `dist/golink_windows_arm64.exe` | Windows ARM64 | 都是 `CGO_ENABLED=0` 的静态二进制,`chmod +x` 就能用;`dist/SHA256SUMS` 里有校验和。 ### 重新编译 ```bash ./build.sh # 六个平台全部编译到 dist/ ./build.sh local # 只编当前平台,产出 dist/golink VERSION=1.2.3 ./build.sh ``` 唯一的依赖是 `golang.org/x/crypto`(本地 module cache 里已经有 v0.41.0),所以**离线也能编译**。 注意 build.sh 默认把 `GOCACHE` 指到仓库内的 `.gocache/`:某些受限环境里 `~/.cache/go-build` 是只读的,不换目录 go 会以非零码退出。 ### 自测 ```bash ./smoketest/run.sh ``` 38 项端到端检查,全部在本地跑,不需要外网。它会编译一个自带的迷你 sshd (`smoketest/sshd`,支持 password / keyboard-interactive / publickey 三种认证),然后逐个 验证:参数顺序、stdin 管道、20 万字节回传、退出码透传(含 143/137)、stdout/stderr 分离、 主机指纹校验、known_hosts、错误密码快速失败、加密私钥报错、键盘交互登录等。 每个用例都关掉 stdin 并加了超时——只要哪里会等待交互输入,测试就会挂掉。 ### Windows 提示 - cmd 里中文乱码:先 `chcp 65001`;PowerShell 7 一般没问题。 - 退出码在 cmd 里看 `%ERRORLEVEL%`,PowerShell 里看 `$LASTEXITCODE`。 - 首次运行可能被 SmartScreen / 杀软拦,需要放行。 ### 主机密钥策略(重要) 默认**不校验**主机密钥(`InsecureIgnoreHostKey`),因为脚本无法回答「是否信任这台主机」的 交互提示——这跟 plink `-batch` 的取舍是一样的,代价是理论上可被中间人攻击。要防这一手: - `-hostkey SHA256:xxxx`:写死指纹,不匹配就拒绝; - `-knownhosts ~/.ssh/known_hosts`:未知或变更直接失败,绝不提示。 ### 安全提示 密码写在命令行会出现在 `ps` 和 shell history 里(plink 的 `-pw` 同样如此)。写脚本时更推荐 `GOLINK_PASSWORD` 环境变量或 `-i` 密钥。带口令的私钥不支持:golink 不会交互式提问,遇到会 直接报错并提示先用 `ssh-keygen -p` 去掉口令。 ### 目录结构 ``` cmd/golink/ 命令行入口:参数、退出码映射 internal/cli/ 参数重排(让 flag 可以出现在任意位置) internal/sshx/ 建连、认证、主机密钥策略(后续工具复用) smoketest/sshd/ 测试用迷你 SSH 服务端 smoketest/run.sh 端到端测试 build.sh 交叉编译脚本 dist/ 产物 ``` --- ## goscp 把本地文件或目录传到远端,走 SCP 协议(远端跑 `scp -t`),不依赖 SFTP 子系统。 ```bash goscp [flags] [user@]host[:port] -files "dir or file" [-files ...] [-dest /remote/dir] ``` ### 示例 ```bash # 传一个文件到远端家目录 goscp jarue@172.19.4.240 -p jarue -port 22 -files "build/output.bin" # 传整个目录到指定位置(远端目录不存在会自动建) goscp jarue@172.19.4.240 -p jarue -files "build" -dest /home/jarue/app # 多个文件 goscp user@host -p "$PASS" -files a.txt -files 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` | 空 | known_hosts 文件;未知或变更直接失败 | | `-hostkey` | 空 | 要求的主机指纹 | | `-timeout` | 15s | 建连 + 认证超时,不含传输 | | `-v` | 关 | 逐条列出上传的文件和大小 | | `-q` | 关 | 不打印最后那行汇总 | ### 退出码 | 退出码 | 含义 | | --- | --- | | 0 | 全部成功 | | 1 | 传输失败(本地路径不存在、远端 scp 报错、权限不足) | | 2 | 用法错误(参数、目标、没给文件、没给凭据) | | 255 | 连接、主机密钥或认证失败 | ### 行为说明 - **文件和目录同一条命令**:`-files` 给什么就传什么,目录自动递归,不需要 `-r` 之类的开关 - 目录保留相对结构:`-files build -dest /srv` → `/srv/build/...` - 权限:普通文件 0644,本地本身有执行位的保持 0755,目录 0755 - 符号链接:命令行上直接点名的会跟随;目录内部遇到的跳过并在 stderr 提示 - 文件名含空格没问题(SCP 头按行解析);含换行的不支持 - 传输是流式的,1 MiB 文件实测逐字节一致 ### 还没做 - 下载方向(远端 → 本地) - 断点续传、并行传输、进速显示 - SFTP 后端(需要额外依赖,目前零依赖) - 跳板机(`-J`)、端口转发、交互式 shell / pty