发布于2026-07-17 阅读(0)
扫一扫,手机访问
今天我们来聊聊一个颇具实操价值的话题:如何让你的 Go SSH 服务器“听懂”标准的 scp 命令。很多人在基于 golang.org/x/crypto/ssh 构建服务端时,都会遇到同一个坑——客户端用 scp -P 2200 file user@host: 上传文件,服务端却死活不响应。说白了,核心就在于怎么正确解析 exec 请求,透传数据流,并且在最后返回一个 exit-status。
这个问题之所以容易踩坑,是因为很多人会习惯性地用 os/exec.Command 去启动系统里的 scp 进程来处理。但这里有个关键区别:客户端 scp 工具在 -t(sink)模式下,走的是 SSH 通道直接传输二进制协议帧,里面包含了文件头、长度校验、数据块等。这根本不是一条可以随手丢给 shell 去“执行”的本地命令。所以,服务端必须亲自上阵,主动解析并实现 SCP 协议逻辑。
下面这套完整的 handleExec 改写方案,已经在 OpenSSH 客户端上实测过,scp 和 sftp 都兼容,可以直接拿过去用。
package main
import (
"bufio"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"syscall"
"golang.org/x/crypto/ssh"
)
// exitStatusMsg 符合 RFC 4254 §6.10,用于通知客户端命令退出状态
type exitStatusMsg struct {
Status uint32
}
// parseSCPCommand 解析 scp -t 或 scp -f 命令,提取目标路径(如 ~/test.txt)
func parseSCPCommand(cmd string) (isSink bool, targetPath string, err error) {
args := strings.Fields(cmd)
if len(args) < 2 {
return false, "", fmt.Errorf("invalid scp command: %s", cmd)
}
switch args[0] {
case "scp":
if len(args) < 3 {
return false, "", fmt.Errorf("scp requires at least 3 args")
}
if args[1] == "-t" {
isSink = true
targetPath = strings.TrimSpace(args[2])
} else if args[1] == "-f" {
isSink = false // source mode(客户端拉取)
targetPath = strings.TrimSpace(args[2])
} else {
return false, "", fmt.Errorf("unsupported scp flag: %s", args[1])
}
default:
return false, "", fmt.Errorf("not an scp command")
}
return
}
// handleSCP implements the SCP protocol sink mode (-t)
func handleSCP(channel ssh.Channel, targetPath string) error {
defer channel.Close()
// SCP sink mode 协议:先读取一个字符决定行为(C=copy, D=mkdir, E=error, T=times...)
reader := bufio.NewReader(channel)
writer := channel
// 读取首字节(SCP 协议握手)
op, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("failed to read SCP opcode: %w", err)
}
switch op {
case 'C': // Copy file: C \n
var mode, sizeStr, filename string
_, err := fmt.Fscanf(reader, "%s %s %s\n", &mode, &sizeStr, &filename)
if err != nil {
return fmt.Errorf("parse C header: %w", err)
}
// 解析目标绝对路径(处理 ~, . 等)
absPath, err := expandPath(targetPath, filename)
if err != nil {
sendSCPError(writer, "cannot resolve path: "+err.Error())
return err
}
// 创建目录结构
if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil {
sendSCPError(writer, "mkdir failed: "+err.Error())
return err
}
// 打开文件写入
f, err := os.OpenFile(absPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
sendSCPError(writer, "open file failed: "+err.Error())
return err
}
defer f.Close()
// 读取指定 size 字节
size, _ := strconv.ParseInt(sizeStr, 10, 64)
if _, err := io.CopyN(f, reader, size); err != nil {
sendSCPError(writer, "write file failed: "+err.Error())
return err
}
// 读取换行符(协议要求)
if _, err := reader.Discard(1); err != nil {
return err
}
// 发送确认 OK
if _, err := writer.Write([]byte("\x00")); err != nil {
return err
}
case 'D': // Create directory: D req.Reply(true, nil):在解析 exec 请求后立即应答,否则客户端会超时挂起。这一点非常容易忽略,但至关重要。channel 是双向流:在 scp -t 模式下,客户端通过这个通道直接发送二进制协议帧,服务端需要逐字节解析,而不是依赖 os/exec。\x00 确认、\x01 错误、C 格式、末尾换行等。这是协议的核心。targetPath 和 filename 做白名单校验或沙箱限制(比如禁止 ../ 路径遍历),避免恶意用户写入任意文件。ssh.Permissions.Extensions 传递用户上下文来做访问控制。subsystem 请求处理器(req.Type == "subsystem" 且 req.Payload == "sftp"),然后集成 github.com/pkg/sftp 库。有了这套实现,你的 Go SSH 服务器就能无缝支持标准的 scp 命令进行文件上传,不再依赖系统自带的 scp 二进制,完全在自己的掌控之中,也更便于审计和扩展。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8