商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 如何在 Go SSH 服务器中正确支持 SCP 文件传输

如何在 Go SSH 服务器中正确支持 SCP 文件传输

  发布于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
  • 严格遵循 SCP 协议:包括 \x00 确认、\x01 错误、C 格式、末尾换行等。这是协议的核心。
  • ⚠️ 路径安全:务必对 targetPathfilename 做白名单校验或沙箱限制(比如禁止 ../ 路径遍历),避免恶意用户写入任意文件。
  • ⚠️ 权限与用户隔离:生产环境请用非 root 用户运行,并通过 ssh.Permissions.Extensions 传递用户上下文来做访问控制。
  • ? 扩展性提示:如果未来需要支持 sftp,可以注册 subsystem 请求处理器(req.Type == "subsystem"req.Payload == "sftp"),然后集成 github.com/pkg/sftp 库。

有了这套实现,你的 Go SSH 服务器就能无缝支持标准的 scp 命令进行文件上传,不再依赖系统自带的 scp 二进制,完全在自己的掌控之中,也更便于审计和扩展。

本文转载于:https://www.php.cn/faq/2332897.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注