欢迎光临
我们一直在努力

【k3s】AutoK3s v0.9.3 —— Part 6 SSH/终端模块超深度逐行分析

Part 6: SSH/终端模块超深度逐行分析

AutoK3s v0.9.3 分析范围:pkg/hosts/executor.go、pkg/hosts/dialer/ssh.go、pkg/hosts/dialer/pty.go、pkg/hosts/dialer/docker.go、pkg/hosts/websocket.go 以及关联文件:pkg/utils/ssh.go、pkg/server/store/websocket/ssh/ssh.go、pkg/server/store/websocket/kubectl/kubectl.go、pkg/cluster/cluster.go


目录

  • 模块定位
  • 模块整体结构
  • 核心业务逻辑深度解析
  • 认证流程图与重试机制
  • Mermaid 图集

  • 1. 模块定位

    1.1 系统位置

    SSH/终端模块是 AutoK3s 中所有远程节点操作的核心通道。它处于系统中间层,向上服务于集群编排和 Web 终端,向下封装不同传输协议(SSH / Docker exec /本地 PTY)。

    ┌──────────────────────────────────────────────────────────────┐
    │ 调用方层 │
    │ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
    │ │ cluster.go │ │ server/ │ │ providers/ │ │
    │ │ execute() │ │ websocket/ │ │ tencent/alibaba/… │ │
    │ │ │ │ ssh/kubectl │ │ │ │
    │ └──────┬──────┘ └──────┬───────┘ └──────────┬───────────┘ │
    ├─────────┼────────────────┼─────────────────────┼─────────────┤
    │ │ 接口层 (hosts package) │ │
    │ ┌──────▼──────────────────────────────────────▼──────────┐ │
    │ │ Script 接口 Shell 接口 WebSocketDialer │ │
    │ │ (ExecuteCommands) (OpenShell/ (终端IO桥接) │ │
    │ │ Terminal) │ │
    │ └──────┬──────────────────────────────────────┬──────────┘ │
    ├─────────┼──────────────────────────────────────┼─────────────┤
    │ │ 实现层 (dialer package) │ │
    │ ┌──────▼──────┐ ┌──────────┐ ┌─────────────▼──────────┐ │
    │ │ SSHDialer │ │ PtyShell │ │ DockerShell │ │
    │ │ SSHShell │ │ │ │ │ │
    │ └─────────────┘ └──────────┘ └────────────────────────┘ │
    ├──────────────────────────────────────────────────────────────┤
    │ 传输层 │
    │ ┌─────────────┐ ┌──────────┐ ┌────────────────────────┐ │
    │ │ golang.org/ │ │ creack/ │ │ docker/docker/client │ │
    │ │ x/crypto/ssh│ │ pty │ │ │ │
    │ └─────────────┘ └──────────┘ └────────────────────────┘ │
    └──────────────────────────────────────────────────────────────┘

    1.2 被调场景全景

    调用方调用路径使用接口
    cluster.ProviderBase.execute() dialer.NewSSHDialer() → ExecuteCommands() → Close() Script
    cluster.ProviderBase.executeWithRetry() 包裹 execute(),额外2s×count次重试 Script
    cluster.terminal() NewSSHDialer() → OpenShell() → SetIO() → Terminal() Shell
    cluster.base.ReleaseManifests() NewSSHDialer() → ExecuteCommands() → Close() Script
    server/websocket/ssh NewSSHDialer()/NewDockerShell() → OpenShell() → NewWebSocketDialer() → Terminal() → ReadMessage() Shell + WebSocket
    server/websocket/kubectl NewPtyShell() → NewWebSocketDialer() → Terminal() → Write() → ReadMessage() Shell + WebSocket
    providers/tencent NewSSHDialer() → ExecuteCommands() Script
    providers/alibaba NewSSHDialer() → ExecuteCommands() Script
    providers/k3d NewDockerShell() → Terminal() Shell
    airgap/file_scp.go ExecuteCommands() (通过传入的 hosts.Script 接口) Script

    2. 模块整体结构

    2.1 Script 接口定义 (executor.go)

    package hosts

    type Script interface {
    ExecuteCommands(cmds string) (output string, err error)
    Close() error
    }

    仅 6 行,定义了命令执行器的最小契约:

    • ExecuteCommands:可变参数接收多条命令,返回合并输出和错误
    • Close:释放连接资源

    实现者: SSHDialer(通过 var _ hosts.Script = &SSHDialer{} 编译期断言)

    2.2 Shell 接口定义 (websocket.go)

    type Shell interface {
    SetIO(stdout, stderr io.Writer, stdin io.ReadCloser)
    ChangeWindowSize(win ShellWindowSize) error
    Terminal() error
    OpenTerminal(win ShellWindowSize) error
    Wait() error
    Write(b []byte) error
    Close() error
    }

    8 个方法,定义了交互终端的完整契约:

    方法职责
    SetIO 设置 stdout/stderr/stdin 三个 IO 流
    ChangeWindowSize 动态调整终端窗口大小
    Terminal CLI 模式启动终端(MakeRaw + 阻塞等待)
    OpenTerminal WebSocket 模式启动终端(非阻塞)
    Wait 等待远程命令退出
    Write 向终端写入数据
    Close 关闭终端会话

    实现者:

    • SSHShell — var _ hosts.Shell = &SSHShell{}
    • PtyShell — var _ hosts.Shell = &PtyShell{}
    • DockerShell — var _ hosts.Shell = &DockerShell{}

    2.3 ShellWindowSize 结构体

    type ShellWindowSize struct {
    Width int
    Height int
    }

    WebSocket 通信中的窗口尺寸载体,JSON 序列化后在 WebSocket TextMessage 中传输。

    2.4 SSHDialer 结构体

    type SSHDialer struct {
    sshKey string // SSH私钥内容(非路径,已读取)
    sshCert string // SSH证书内容(非路径,已读取)
    sshAddress string // 连接地址 IP:Port 或 InstanceID
    username string // SSH用户名
    password string // SSH密码
    passphrase string // SSH私钥密码短语
    useSSHAgentAuth bool // 是否使用SSH Agent认证

    conn *ssh.Client // 底层SSH客户端连接

    uid int // 远程用户UID(懒加载)
    logger *logrus.Logger // 日志器

    shells map[hosts.Shell]hosts.Shell // 活跃Shell会话集合
    }

    字段分析:

    • sshKey/sshCert:存储的是文件内容而非路径,在 NewSSHDialer 中通过 utils.SSHPrivateKeyPath() 读取
    • sshAddress:优先使用 PublicIPAddress[0]:SSHPort,无IP时退化为 InstanceID
    • conn:ssh.Dial() 返回的持久连接,ExecuteCommands 和 OpenShell 都基于它创建 Session
    • uid:初始值 -1,首次 ExecuteCommands 时通过 id -u 命令懒加载,用于判断是否需要 sudo
    • shells:用 map 跟踪所有打开的 Shell,Close() 时遍历关闭

    2.5 SSHShell 结构体

    type SSHShell struct {
    Stdin io.ReadCloser // 标准输入流
    Stdout io.Writer // 标准输出流
    Stderr io.Writer // 标准错误流
    Writer io.Writer // 日志写入器(未在核心流程中使用)

    Term string // 终端类型(xterm/环境变量TERM)
    Modes ssh.TerminalModes // 终端模式参数
    session *ssh.Session // 底层SSH会话
    dialer *SSHDialer // 所属Dialer引用
    }

    2.6 DockerShell 结构体

    type DockerShell struct {
    execID string // Docker exec ID

    Stdin io.ReadCloser // 标准输入流
    Stdout io.Writer // 标准输出流
    Stderr io.Writer // 标准错误流
    Writer io.Writer // 日志写入器

    ctx context.Context // 上下文
    client client.APIClient // Docker API客户端
    response *dockertypes.HijackedResponse // hijacked连接(stdin/stdout共用)
    }

    关键设计: DockerShell 同时实现 Shell 接口,但不实现 Script 接口——Docker 模式仅支持交互终端,不支持批量命令执行。

    2.7 PtyShell 结构体

    type PtyShell struct {
    Stdin io.ReadCloser
    Stdout io.Writer
    Stderr io.Writer
    Writer io.Writer

    ctx context.Context
    conn *os.File // PTY master端
    cmd *exec.Cmd // 本地命令
    }

    构建约束: //go:build darwin || linux — 仅支持 macOS 和 Linux。

    2.8 WebSocketDialer 结构体

    type WebSocketDialer struct {
    dialer Shell // 底层Shell实现(SSHShell/PtyShell/DockerShell)
    conn *websocket.Conn // WebSocket连接
    reader *TerminalReader // 终端读取器
    }

    核心职责: 将 WebSocket 连接桥接到任意 Shell 实现,是 Web 终端的适配层。

    2.9 辅助结构体

    TerminalReader

    type TerminalReader struct {
    conn *websocket.Conn // WebSocket连接
    reader io.Reader // 当前消息reader
    resize func(size ShellWindowSize) // 窗口大小调整回调
    ClosedCh chan bool // 关闭信号通道
    }

    BinaryWriter

    type BinaryWriter struct {
    conn *websocket.Conn
    }

    singleWriter

    type singleWriter struct {
    b io.Writer
    mu sync.Mutex
    }

    线程安全的 Writer 包装器,将 logrus.Out 和输出 buffer 同时写入——既记录日志又捕获输出。


    3. 核心业务逻辑深度解析

    3.1 NewSSHDialer() — SSH连接建立全流程

    func NewSSHDialer(n *types.Node, timeout bool, logger *logrus.Logger) (*SSHDialer, error)

    逐行解析:

    Step 1: 参数校验

    if len(n.PublicIPAddress) <= 0 && n.InstanceID == "" {
    return nil, errors.New("[ssh-dialer] no node IP or node ID is specified")
    }

    • 必须有公网IP或实例ID,否则无法建立连接

    if logger == nil {
    logger = logrus.StandardLogger()
    }

    • logger 为 nil 时降级到标准 logger
    Step 2: 初始化 Dialer 结构体

    d := &SSHDialer{
    username: n.SSHUser,
    password: n.SSHPassword,
    passphrase: n.SSHKeyPassphrase,
    useSSHAgentAuth: n.SSHAgentAuth,
    sshCert: n.SSHCert,
    logger: logger,
    shells: map[hosts.Shell]hosts.Shell{},
    uid: 1,
    }

    • uid: -1 标记为"未获取",后续 getUserID() 时判断
    • shells 初始化为空 map
    Step 3: 确定连接地址

    if len(n.PublicIPAddress) > 0 {
    d.sshAddress = fmt.Sprintf("%s:%s", n.PublicIPAddress[0], n.SSHPort)
    } else {
    d.sshAddress = n.InstanceID
    }

    • 优先使用公网IP + 端口(默认22)
    • 无IP时使用 InstanceID(这在云提供商场景中可能被代理处理)
    Step 4: 加载私钥和证书

    if d.password == "" && d.sshKey == "" && !d.useSSHAgentAuth && len(n.SSHKeyPath) > 0 {
    var err error
    d.sshKey, err = utils.SSHPrivateKeyPath(n.SSHKeyPath)
    if err != nil {
    return nil, err
    }

    if d.sshCert == "" && len(n.SSHCertPath) > 0 {
    d.sshCert, err = utils.SSHCertificatePath(n.SSHCertPath)
    if err != nil {
    return nil, err
    }
    }
    }

    认证优先级逻辑:

  • 如果已有 password → 跳过密钥加载
  • 如果已有 sshKey → 跳过密钥加载
  • 如果使用 SSH Agent → 跳过密钥加载
  • 仅当以上都不满足且 SSHKeyPath 非空时,从文件读取私钥
  • 证书同理:仅当 sshCert 为空且 SSHCertPath 非空时读取
  • Step 5: Backoff 重试连接

    try := 0
    if err := wait.ExponentialBackoff(defaultBackoff, func() (bool, error) {
    try++
    logger.Infof("the %d/%d time tring to ssh to %s with user %s", try, defaultBackoff.Steps, d.sshAddress, d.username)
    c, err := d.Dial(timeout)
    if err != nil {
    return false, nil // 返回 nil 而非 err —— 不中断重试
    }
    d.conn = c
    return true, nil
    }); err != nil {
    return nil, fmt.Errorf("[ssh-dialer] init dialer [%s] error: %w", d.sshAddress, err)
    }

    重试参数(defaultBackoff):

    var defaultBackoff = wait.Backoff{
    Duration: 15 * time.Second, // 每次间隔15秒
    Factor: 1, // 因子为1(固定间隔,非指数增长)
    Steps: 5, // 最多5次
    }

    • 总最大等待时间: 5 × 15s = 75s
    • Factor=1 意味着每次间隔不变,都是 15s(虽然名为 ExponentialBackoff,但 Factor=1 时退化为固定间隔)
    • 关键设计: Dial() 失败时返回 (false, nil) 而非 (false, err),这确保 ExponentialBackoff 会继续重试而非提前终止。只有超过5次都失败,ExponentialBackoff 返回 wait.ErrWaitTimeout,被包装为 [ssh-dialer] init dialer 错误

    3.2 Dial() — SSH握手

    func (d *SSHDialer) Dial(t bool) (*ssh.Client, error) {
    timeout := defaultBackoff.Duration // 15s
    if !t {
    timeout = 0 // 无超时
    }

    cfg, err := utils.GetSSHConfig(d.username, d.sshKey, d.passphrase, d.sshCert, d.password, timeout, d.useSSHAgentAuth)
    if err != nil {
    return nil, err
    }
    return ssh.Dial("tcp", d.sshAddress, cfg)
    }

    timeout 参数语义:

    • t=true(绝大多数调用场景):连接超时 = 15s
    • t=false:连接超时 = 0(无限等待)

    utils.GetSSHConfig() 认证配置详解:

    func GetSSHConfig(username, sshPrivateKeyString, passphrase, sshCert string, password string, timeout time.Duration, useAgentAuth bool) (*ssh.ClientConfig, error) {
    config := &ssh.ClientConfig{
    User: username,
    Timeout: timeout,
    HostKeyCallback: ssh.InsecureIgnoreHostKey(), // ⚠️ 忽略主机密钥验证
    }
    // … 认证方式分支 …
    }

    ⚠️ 安全注意: HostKeyCallback: ssh.InsecureIgnoreHostKey() 跳过主机密钥验证,存在中间人攻击风险,但在自动化运维场景中这是常见取舍。

    认证方式分支(按优先级):

    优先级条件认证方式代码
    1 useAgentAuth=true 且 SSH_AUTH_SOCK 环境变量存在 SSH Agent ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers)
    2 sshPrivateKeyString != "" 且 passphrase != "" 私钥+密码短语 ssh.ParsePrivateKeyWithPassphrase() → ssh.PublicKeys(signer)
    3 sshPrivateKeyString != "" 且 passphrase == "" 私钥 ssh.ParsePrivateKey() → ssh.PublicKeys(signer)
    4 password != "" 密码 ssh.Password(password)

    证书处理(在私钥认证分支内):

    if len(sshCert) > 0 {
    key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshCert))
    if _, ok := key.(*ssh.Certificate); !ok {
    return config, fmt.Errorf("unable to cast public key to SSH certificate")
    }
    signer, err = ssh.NewCertSigner(key.(*ssh.Certificate), signer)
    }

    • 证书认证叠加在私钥认证之上
    • 将已解析的 signer 升级为 certSigner

    3.3 getUserID() — 远程UID获取

    func (d *SSHDialer) getUserID() error {
    if d.uid >= 0 {
    return nil // 已缓存,直接返回
    }
    session, err := d.conn.NewSession()
    if err != nil {
    return err
    }
    defer func() { _ = session.Close() }()

    output, err := session.Output("id -u")
    if err != nil {
    return fmt.Errorf("failed to get current user id from remote host %s, %v", d.sshAddress, err)
    }
    d.uid, err = strconv.Atoi(strings.TrimSpace(string(output)))
    if err != nil {
    return fmt.Errorf("failed to parse uid output from remote host, output: %s, %v", string(output), err)
    }
    return nil
    }

    设计要点:

    • 懒加载:首次调用 ExecuteCommands 时触发
    • 缓存:后续调用直接返回,避免重复 id -u 命令
    • session.Output() 是便捷方法:创建session → 运行命令 → 捕获stdout → 关闭session
    • TrimSpace 处理输出中的换行符

    3.4 ExecuteCommands() — 远程命令执行

    func (d *SSHDialer) ExecuteCommands(cmds string) (string, error) {
    // Step 1: 获取用户ID(判断是否需要sudo)
    if err := d.getUserID(); err != nil {
    return "", err
    }

    // Step 2: 确定sudo前缀
    sudo := ""
    if d.uid > 0 {
    sudo = "sudo"
    }

    // Step 3: 创建新Session
    session, err := d.conn.NewSession()
    if err != nil {
    return "", err
    }
    defer session.Close()

    // Step 4: 命令包装(base64编码)
    encodedCMD := d.wrapCommands(strings.Join(cmds, "\\n"))
    cmd := fmt.Sprintf("echo \\"%s\\" | base64 -d | %s sh -", encodedCMD, sudo)
    d.logger.Debugf("executing cmd: %s", cmd)

    // Step 5: 输出捕获
    output := bytes.NewBuffer([]byte{})
    combinedOutput := singleWriter{
    b: io.MultiWriter(d.logger.Out, output),
    }
    session.Stderr = &combinedOutput
    session.Stdout = &combinedOutput

    // Step 6: 执行
    err = session.Run(cmd)
    return output.String(), err
    }

    逐层深度解析:

    Step 1-2: sudo 判断逻辑

    if d.uid > 0 {
    sudo = "sudo"
    }

    • uid == 0 → root 用户,不需要 sudo
    • uid > 0 → 普通用户,需要 sudo
    • uid == -1 → getUserID() 会先执行获取
    Step 3: Session 生命周期

    每次 ExecuteCommands 创建独立的 Session,通过 defer session.Close() 确保释放。这与 OpenShell 的持久会话不同。

    Step 4: 命令包装机制(关键设计!)

    const scriptWrapper = `#!/bin/sh
    set -e
    %s
    `

    func (d *SSHDialer) wrapCommands(cmd string) string {
    return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(scriptWrapper, cmd)))
    }

    最终执行的命令格式:

    echo "<base64编码>" | base64 -d | sudo sh

    为什么用 base64 包装?

  • 转义安全:避免命令中的引号、特殊字符在 SSH 传输中被 shell 解释
  • 多命令合并:strings.Join(cmds, "\\n") 将多条命令用换行连接
  • set -e:脚本中任何命令失败立即退出,确保错误传播
  • #!/bin/sh:使用 POSIX shell,避免 bash 依赖
  • 示例: 执行 mkdir -p /etc/k3s 和 cat /etc/k3s/config.yaml

    原始: mkdir -p /etc/k3s\\ncat /etc/k3s/config.yaml
    包装: #!/bin/sh\\nset -e\\nmkdir -p /etc/k3s\\ncat /etc/k3s/config.yaml
    编码: IyEvYmluL3NoCnNldCAtZQpta2RpciAtcCAvZXRjL2szcwpjYXQgL2V0Yy9rM3MvY29uZmlnLnlhbWwK
    最终: echo "IyEvYmluL3NoCnNldCAtZQpta2RpciAtcCAvZXRjL2szcwpjYXQgL2V0Yy9rM3MvY29uZmlnLnlhbWwK" | base64 -d | sudo sh –

    Step 5: 输出捕获架构

    output := bytes.NewBuffer([]byte{})
    combinedOutput := singleWriter{
    b: io.MultiWriter(d.logger.Out, output),
    }
    session.Stderr = &combinedOutput
    session.Stdout = &combinedOutput

    数据流:

    远程命令 stdout ─┐
    ├─→ singleWriter ─→ io.MultiWriter ─┬─→ logrus.Out (日志)
    远程命令 stderr ─┘ └─→ output buffer (返回值)

    • singleWriter 用 sync.Mutex 保证并发安全
    • io.MultiWriter 实现 tee:同时写日志和缓冲区
    • stdout 和 stderr 共用同一个 writer → 输出合并
    • session.Run() 阻塞直到命令完成
    Step 6: 返回值

    err = session.Run(cmd)
    return output.String(), err

    • 返回合并的 stdout+stderr 文本
    • session.Run() 在命令非零退出码时返回 *ssh.ExitError

    3.5 OpenShell() — 打开交互终端会话

    func (d *SSHDialer) OpenShell() (hosts.Shell, error) {
    shell := &SSHShell{
    dialer: d,
    }
    session, err := d.conn.NewSession()
    if err != nil {
    return nil, err
    }
    shell.session = session
    d.shells[shell] = shell
    return shell, nil
    }

    与 ExecuteCommands 的区别:

    • 不执行命令,仅创建持久 Session
    • 返回 Shell 接口,调用方后续通过 SetIO + Terminal 启动终端
    • 注册到 d.shells map,Dialer Close 时统一清理

    3.6 SSHShell.Terminal() — CLI 交互终端

    func (d *SSHShell) Terminal() error {
    var win hosts.ShellWindowSize
    fdInfo, _ := term.GetFdInfo(d.Stdout)
    fd := int(fdInfo)

    // Step 1: 保存当前终端状态并切换到 raw 模式
    oldState, err := terminal.MakeRaw(fd)
    defer func() {
    _ = terminal.Restore(fd, oldState)
    }()
    if err != nil {
    return err
    }

    // Step 2: 获取当前终端窗口大小
    win.Width, win.Height, err = terminal.GetSize(fd)
    if err != nil {
    return err
    }

    // Step 3: 打开远程终端
    if err := d.OpenTerminal(win); err != nil {
    return err
    }

    // Step 4: 阻塞等待会话结束
    return d.session.Wait()
    }

    Terminal vs OpenTerminal 的区别:

    特性Terminal()OpenTerminal()
    调用场景 CLI 直接操作 WebSocket 交互
    MakeRaw ✅ 修改本地终端为 raw 模式 ❌ 不修改
    获取窗口大小 从本地 fd 获取 由参数传入
    阻塞 session.Wait() 阻塞 不阻塞
    恢复终端 defer Restore 不需要恢复

    3.7 SSHShell.OpenTerminal() — 终端模式设置

    func (d *SSHShell) OpenTerminal(win hosts.ShellWindowSize) error {
    // Step 1: 设置终端类型
    d.Term = os.Getenv("TERM")
    if d.Term == "" {
    d.Term = "xterm"
    }

    // Step 2: 设置终端模式
    d.Modes = ssh.TerminalModes{
    ssh.ECHO: 1, // 回显启用
    ssh.TTY_OP_ISPEED: 14400, // 输入速度 14400 baud
    ssh.TTY_OP_OSPEED: 14400, // 输出速度 14400 baud
    }

    // Step 3: 绑定 IO 流
    d.session.Stdin = d.Stdin
    d.session.Stdout = d.Stdout
    d.session.Stderr = d.Stderr

    // Step 4: 请求 PTY
    if err := d.session.RequestPty(d.Term, win.Height, win.Width, d.Modes); err != nil {
    return err
    }

    // Step 5: 启动 Shell
    return d.session.Shell()
    }

    终端模式参数:

    • ECHO: 1 — 启用字符回显
    • TTY_OP_ISPEED/OSPEED: 14400 — 波特率设置(历史遗留参数,现代网络中无实际意义但SSH协议要求)

    请求顺序: RequestPty → Shell。这是 SSH 协议的标准流程:先协商终端参数,再启动用户 shell。

    3.8 SSHShell.Close()

    func (d *SSHShell) Close() error {
    defer delete(d.dialer.shells, d)
    if d.session != nil {
    if err := d.session.Close(); err != nil {
    return err
    }
    }
    return nil
    }

    • 先从 dialer 的 shells map 中移除自身
    • 再关闭 SSH session
    • 注意 defer delete 确保即使 Close 失败也会从 map 移除

    3.9 SSHDialer.Close()

    func (d *SSHDialer) Close() error {
    if d.conn != nil {
    return d.conn.Close()
    }
    for _, shell := range d.shells {
    shell.Close()
    delete(d.shells, shell)
    }
    return nil
    }

    设计缺陷分析:

    • 当 conn != nil 时,直接关闭连接但不关闭已打开的 shells
    • 当 conn == nil 时,遍历关闭 shells
    • 实际上 conn 在 NewSSHDialer 成功后永远非 nil,所以 shells 清理分支几乎不会执行
    • 但 conn.Close() 会隐式关闭所有基于该连接的 Session,所以功能上是正确的

    3.10 Backoff 重试机制详解

    系统中有两层重试:

    第一层:SSH连接重试(NewSSHDialer 内部)

    var defaultBackoff = wait.Backoff{
    Duration: 15 * time.Second,
    Factor: 1,
    Steps: 5,
    }

    • 间隔:15s(固定)
    • 次数:5次
    • 总时间:75s
    • 场景:SSH连接建立失败(网络不可达、认证失败等)
    第二层:命令执行重试(executeWithRetry)

    func (p *ProviderBase) executeWithRetry(count int, n *types.Node, cmds string) (string, error) {
    backoff := wait.Backoff{
    Duration: 2 * time.Second,
    Factor: 1,
    Steps: count,
    }
    // … 每次重试都重新创建 SSHDialer …
    }

    • 间隔:2s(固定)
    • 次数:由调用方指定
    • 场景:命令执行失败
    第三层:Docker连接重试(NewDockerShell 内部)

    // 使用 common.Backoff
    Backoff = wait.Backoff{
    Duration: 30 * time.Second,
    Factor: 1,
    Steps: 20,
    }

    • 间隔:30s(固定)
    • 次数:20次
    • 总时间:600s(10分钟!)
    • 场景:Docker exec 创建/附加失败

    3.11 DockerDialer — Docker exec 方式连接容器

    NewDockerShell() 全流程

    func NewDockerShell(n *types.Node) (*DockerShell, error) {

    Step 1: 参数校验

    if n.InstanceID == "" {
    return nil, errors.New("[docker-dialer] no container ID is specified")
    }

    Docker 模式必须有容器ID。

    Step 2: 创建 Docker 客户端

    d := &DockerShell{ctx: context.Background()}
    docker, err := dockerutils.GetDockerClient()

    使用 k3d 的 Docker 工具函数获取 Docker 客户端。

    Step 3: 查找容器

    f := filters.NewArgs()
    f.Add("name", fmt.Sprintf("^/?(%s-)?%s$", k3dtypes.DefaultObjectNamePrefix, n.InstanceID))

    containers, err := docker.ContainerList(d.ctx, dockertypes.ContainerListOptions{
    Filters: f,
    All: true,
    })

    容器名匹配正则: ^/?(k3d-)?<InstanceID>$

    • 容器名以 / 开头(Docker惯例)
    • 可能有 k3d- 前缀
    • 核心匹配 InstanceID

    Step 4: 容器校验

    if len(containers) > 1 {
    return nil, fmt.Errorf("…found: %d", len(containers))
    }
    if len(containers) == 0 {
    return nil, fmt.Errorf("…didn't find container for node '%s'", n.InstanceID)
    }

    必须恰好找到1个容器。

    Step 5: 创建 exec + 附加连接(带重试)

    if err := wait.ExponentialBackoff(common.Backoff, func() (bool, error) {
    exec, err := docker.ContainerExecCreate(d.ctx, container.ID, dockertypes.ExecConfig{
    Privileged: true,
    Tty: true,
    AttachStdin: true,
    AttachStderr: true,
    AttachStdout: true,
    Cmd: []string{"/bin/sh"},
    })
    if err != nil {
    return false, err // ⚠️ 返回 err,会中断重试!
    }

    execAttach, err := docker.ContainerExecAttach(d.ctx, exec.ID, dockertypes.ExecStartCheck{
    Tty: true,
    })
    if err != nil {
    return false, err // ⚠️ 返回 err,会中断重试!
    }

    d.execID = exec.ID
    d.response = &execAttach
    return true, nil
    }); err != nil { }

    ⚠️ 与 SSHDialer 的关键差异:

    • SSHDialer 重试中 return false, nil(不返回err)→ 总会重试满5次
    • DockerDialer 重试中 return false, err(返回err)→ 首次错误就中断重试
    • 这意味着 DockerDialer 的 common.Backoff(20次重试)实际上是无效的——任何错误都会立即终止

    Exec 配置:

    • Privileged: true — 特权模式
    • Tty: true — 分配 TTY
    • Cmd: ["/bin/sh"] — 启动 sh
    DockerShell.Terminal() — CLI 终端

    func (d *DockerShell) Terminal() error {
    defer func() { _ = d.Close() }()

    if err := d.ExecStart(true); err != nil {
    return err
    }

    fd, _ := term.GetFdInfo(d.Stderr)
    if term.IsTerminal(fd) {
    if err := d.MonitorTtySize(d.ctx); err != nil {
    logrus.Errorf("[docker-dialer] error monitoring tty size: %s", err.Error())
    }
    }

    if err := d.Wait(); err != nil {
    if !errors.Is(err, io.EOF) {
    return err
    }
    }
    return nil
    }

    流程:

  • defer Close() — 确保 finally 清理
  • ExecStart(true) — needRestore=true,设置 raw 模式
  • MonitorTtySize — 监控终端大小变化
  • Wait() — 阻塞等待 exec 完成,EOF 视为正常退出
  • DockerShell.ExecStart() — IO 流劫持

    这是 DockerShell 的核心,借鉴自 Docker CLI 的 hijack 实现:

    func (d *DockerShell) ExecStart(needRestore bool) error {
    // Step 1: 设置输入流(含 raw 模式和 escape 键检测)
    restoreInput, err := d.setInput(needRestore)
    if err != nil {
    return fmt.Errorf("[docker-dialer] unable to setup input stream: %s", err)
    }
    defer restoreInput()

    // Step 2: 启动输出和输入流的异步拷贝
    errCh := make(chan error, 1)
    go func() {
    defer close(errCh)
    errCh <- func() error {
    outputDone := d.beginOutputStream(restoreInput) // goroutine: response.Reader → Stderr
    inputDone, detached := d.beginInputStream(restoreInput) // goroutine: Stdin → response.Conn

    // Step 3: 等待任一完成
    select {
    case err := <-outputDone:
    return err
    case <-inputDone:
    // stdin关闭后,等待stdout完成
    if d.Stdout != nil || d.Stderr != nil {
    select {
    case err := <-outputDone:
    return err
    case <-d.ctx.Done():
    return d.ctx.Err()
    }
    }
    return nil
    case err := <-detached:
    return err // 用户按了 ctrl-p ctrl-q
    case <-d.ctx.Done():
    return d.ctx.Err()
    }
    }()
    }()

    if err := <-errCh; err != nil {
    return err
    }
    return nil
    }

    三个并发 goroutine 的协作:

    ┌─────────────────────────────────────────┐
    │ ExecStart │
    │ │
    │ ┌─────────────────────────────────┐ │
    │ │ beginOutputStream (goroutine) │ │
    │ │ response.Reader → io.Copy → Stderr │ │
    │ │ → outputDone chan │ │
    │ └─────────────────────────────────┘ │
    │ │
    │ ┌─────────────────────────────────┐ │
    │ │ beginInputStream (goroutine) │ │
    │ │ Stdin → io.Copy → response.Conn │ │
    │ │ → inputDone chan │ │
    │ │ → detached chan (if escape key) │ │
    │ └─────────────────────────────────┘ │
    │ │
    │ ┌─────────────────────────────────┐ │
    │ │ select { … } (main goroutine) │ │
    │ │ 等待最先完成的通道 │ │
    │ └─────────────────────────────────┘ │
    └─────────────────────────────────────────┘

    DockerShell.setInput() — 输入流设置

    func (d *DockerShell) setInput(needRestore bool) (restore func(), err error) {
    if d.Stdin == nil {
    return func() {}, nil
    }

    var restoreOnce sync.Once

    if needRestore {
    inFd, _ := term.GetFdInfo(d.Stdin)
    outFd, _ := term.GetFdInfo(d.Stderr)
    inState, _ := term.SetRawTerminal(inFd)
    outState, _ := term.SetRawTerminalOutput(outFd)

    restore = func() {
    restoreOnce.Do(func() {
    if outState != nil { _ = term.RestoreTerminal(inFd, outState) }
    if inState != nil { _ = term.RestoreTerminal(outFd, inState) }
    })
    }
    } else {
    restore = func() {}
    }

    // 转义键检测(ctrl-p ctrl-q)
    escapeKeys := defaultEscapeKeys // {16, 17}
    d.Stdin = ioutils.NewReadCloserWrapper(
    term.NewEscapeProxy(d.Stdin, escapeKeys), d.Stdin.Close)

    return restore, nil
    }

    关键设计:

    • sync.Once 确保终端恢复只执行一次
    • term.NewEscapeProxy 监听 stdin 中的 escape 序列(ctrl-p, ctrl-q),触发 EscapeError → detached 通道
    • needRestore=false 用于 WebSocket 模式(不需要修改本地终端)
    DockerShell.MonitorTtySize() — 终端大小监控

    func (d *DockerShell) MonitorTtySize(ctx context.Context) error {
    ttyFunc := d.ResizeTty

    // 立即执行一次resize
    if err := ttyFunc(ctx); err != nil {
    go func() {
    var err error
    for retry := 0; retry < 5; retry++ {
    time.Sleep(10 * time.Millisecond)
    if err = ttyFunc(ctx); err == nil { break }
    }
    }()
    }

    if runtime.GOOS == "windows" {
    // Windows: 轮询 250ms 检查窗口变化
    go func() { 250ms 轮询 }()
    } else {
    // Unix: 监听 SIGWINCH 信号
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, dockerSig.SIGWINCH)
    go func() {
    for range sigChan {
    _ = d.ResizeTty(ctx)
    }
    }()
    }
    return nil
    }

    平台差异处理:

    • Unix:SIGWINCH 信号驱动,事件式
    • Windows:250ms 轮询,对比式

    3.12 PtyShell — 本地 PTY 终端

    OpenTerminal()

    func (d *PtyShell) OpenTerminal(win hosts.ShellWindowSize) error {
    p, err := pty.StartWithSize(d.cmd, &pty.Winsize{
    Rows: uint16(win.Height),
    Cols: uint16(win.Width),
    })
    if err != nil {
    return err
    }

    d.conn = p // PTY master fd

    go func() {
    _, _ = io.Copy(d.conn, d.Stdin) // stdin → pty master
    }()

    go func() {
    _, _ = io.Copy(d.Stderr, d.conn) // pty master → stderr
    }()

    return nil
    }

    数据流:

    WebSocket BinaryMessage → TerminalReader.Read() → Stdin → io.Copy → pty master → bash进程

    WebSocket ← BinaryWriter.Write() ← Stderr ← io.Copy ← pty master ← bash进程输出

    Terminal() 直接返回错误:

    func (d *PtyShell) Terminal() error {
    return errors.New("pty terminal not supported")
    }

    PtyShell 不支持 CLI 模式,仅用于 WebSocket。

    ChangeWindowSize()

    func (d *PtyShell) ChangeWindowSize(win hosts.ShellWindowSize) error {
    return pty.Setsize(d.conn, &pty.Winsize{
    Rows: uint16(win.Height),
    Cols: uint16(win.Width),
    })
    }

    直接调用 creack/pty 库的 Setsize,通过 ioctl TIOCSWINSZ 修改 PTY 窗口大小。

    3.13 WebSocket 终端处理

    WebSocketDialer 初始化

    func NewWebSocketDialer(conn *websocket.Conn, dialer Shell) *WebSocketDialer {
    d := &WebSocketDialer{
    conn: conn,
    dialer: dialer,
    }

    r := NewTerminalReader(d.conn)
    r.SetResizeFunction(d.ChangeWindowSize)
    w := NewBinaryWriter(d.conn)

    d.reader = r
    d.dialer.SetIO(w, w, r) // stdout=w, stderr=w, stdin=r
    return d
    }

    IO 绑定关系:

    WebSocket → TerminalReader (stdin) → Shell
    WebSocket ← BinaryWriter (stdout/stderr) ← Shell

    • BinaryWriter 同时作为 stdout 和 stderr —— 远程输出统一写入 WebSocket
    • TerminalReader 作为 stdin —— 从 WebSocket 读取输入
    TerminalReader.Read() — WebSocket 消息分发

    func (t *TerminalReader) Read(p []byte) (int, error) {
    var msgType int
    var err error
    for {
    if t.reader == nil {
    // 从 WebSocket 读取下一条消息
    msgType, t.reader, err = t.conn.NextReader()
    if err != nil {
    t.reader = nil
    t.ClosedCh <- true
    if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
    return 0, io.EOF
    }
    return 0, err
    }
    }
    switch msgType {
    case websocket.TextMessage:
    // 文本消息 = 窗口大小调整指令
    body, e := ioutil.ReadAll(t.reader)
    if e != nil { break }
    r := ShellWindowSize{}
    if err = json.Unmarshal(body, &r); err != nil { break }
    if r.Width > 0 && r.Height > 0 {
    t.resize(r) // 触发 ChangeWindowSize
    }

    case websocket.BinaryMessage:
    // 二进制消息 = 终端输入数据
    n, readErr := t.reader.Read(p)
    return n, convert(readErr)
    }
    t.reader = nil // 消费完毕,重置以读取下一条
    }
    }

    消息协议设计:

    WebSocket 消息类型JSON 内容用途
    TextMessage {"Width": 80, "Height": 24} 调整终端窗口大小
    BinaryMessage 原始字节 终端输入数据

    循环逻辑: TextMessage 处理后不返回,继续循环读下一条消息;BinaryMessage 返回数据给 Shell。这确保了窗口调整指令不会混入终端输入流。

    BinaryWriter.Write()

    func (s *BinaryWriter) Write(p []byte) (int, error) {
    w, err := s.conn.NextWriter(websocket.BinaryMessage)
    if err != nil {
    return 0, convert(err)
    }
    defer func() { _ = w.Close() }()
    var n int
    if len(p) != 0 {
    n, err = w.Write(p)
    }
    return n, err
    }

    • 每次写入创建一个新的 WebSocket 消息帧
    • defer w.Close() 刷新帧
    • 空切片不写入但仍创建帧
    readMessage() — 会话生命周期管理

    func readMessage(ctx context.Context, con *websocket.Conn, closeSession func(), wait func() error, stop chan bool) error {
    sessionClosed := make(chan error, 1)
    go func() {
    sessionClosed <- wait() // 等待 Shell 会话结束
    }()

    for {
    select {
    case <-ctx.Done():
    closeSession()
    return nil

    case <-sessionClosed:
    closeSession()
    close(sessionClosed)
    _ = con.WriteMessage(websocket.CloseMessage,
    websocket.FormatCloseMessage(websocket.CloseNormalClosure, "EOF"))
    return nil

    case isStop := <-stop:
    if isStop {
    closeSession()
    close(stop)
    return nil
    }
    }
    }
    }

    三路 select 竞争:

    通道来源含义
    ctx.Done() HTTP 上下文取消 客户端断开连接/请求超时
    sessionClosed shell.Wait() 返回 远程 Shell 退出
    stop TerminalReader.ClosedCh WebSocket 读取错误/关闭

    无论哪个触发,都会调用 closeSession() 关闭底层 Shell 连接。

    convert() — 错误转换

    func convert(err error) error {
    if err == nil { return nil }
    if e, ok := err.(*websocket.CloseError); ok && e.Code == websocket.CloseNormalClosure {
    return io.EOF
    }
    return err
    }

    将 WebSocket 正常关闭错误转换为 io.EOF,使上层能优雅处理。

    3.14 WebSocket SSH 处理器 (server/store/websocket/ssh/ssh.go)

    func handler(apiOp *types.APIRequest) error {
    // Step 1: 解析查询参数
    provider := queryParams.Get("provider")
    id := queryParams.Get("cluster")
    node := queryParams.Get("node")
    height := queryParams.Get("height") // 默认 150
    width := queryParams.Get("width") // 默认 300

    // Step 2: 升级 HTTP 到 WebSocket
    upgrader.CheckOrigin = func(r *http.Request) bool { return true }
    c, err := upgrader.Upgrade(apiOp.Response, apiOp.Request, nil)

    // Step 3: 创建 Dialer
    dialer, err := newDialer(id, node, c)

    // Step 4: 启动终端
    dialer.Terminal(rows, columns)

    // Step 5: 消息循环
    dialer.ReadMessage(apiOp.Context())
    }

    newDialer() 分支逻辑:

    for _, n := range allNodes {
    if n.InstanceID == node {
    if state.Provider == "k3d" {
    // K3D 提供商 → Docker exec
    dialer, err := dialer.NewDockerShell(&n)
    wsDialer = hosts.NewWebSocketDialer(conn, dialer)
    } else {
    // 其他提供商 → SSH
    dialer, err := dialer.NewSSHDialer(&n, true, common.NewLogger(nil))
    shell, err := dialer.OpenShell()
    wsDialer = hosts.NewWebSocketDialer(conn, shell)
    }
    }
    }

    注意: SSH 模式下,NewWebSocketDialer 接收的是 SSHShell(通过 OpenShell() 获得),而非 SSHDialer。Docker 模式下接收的是 DockerShell 本身。

    3.15 WebSocket Kubectl 处理器 (server/store/websocket/kubectl/kubectl.go)

    func ptyHandler(apiOp *types.APIRequest) error {
    // … 参数解析和 WebSocket 升级 …

    dialer, err := dialer.NewPtyShell(exec.CommandContext(apiOp.Request.Context(), "bash"))
    wsDialer := hosts.NewWebSocketDialer(c, dialer)

    err = wsDialer.Terminal(rows, columns)

    // 注入 kubectl alias
    aliasCmd := fmt.Sprintf("alias kubectl='kubectl –context %s'\\n", apiOp.Name)
    aliasCmd = fmt.Sprintf("%salias k='kubectl –context %s'\\n", aliasCmd, apiOp.Name)
    err = wsDialer.Write([]byte(aliasCmd))

    return wsDialer.ReadMessage(apiOp.Context())
    }

    特殊设计: 启动 bash 后立即注入两个 alias,让用户直接使用 kubectl / k 命令操作指定集群。


    4. 认证流程图与重试机制

    4.1 SSH 认证决策树

    NewSSHDialer(n, timeout, logger)

    ├─ password != "" ?
    │ └─ YES → 密码认证: ssh.Password(password)

    ├─ useSSHAgentAuth && SSH_AUTH_SOCK env exists ?
    │ └─ YES → Agent认证: ssh.PublicKeysCallback(agent.Signers)

    ├─ sshKey != "" (已从文件读取) ?
    │ │
    │ ├─ passphrase != "" ?
    │ │ └─ YES → 带密码短语私钥: ParsePrivateKeyWithPassphrase → PublicKeys(signer)
    │ │
    │ ├─ passphrase == "" ?
    │ │ └─ YES → 普通私钥: ParsePrivateKey → PublicKeys(signer)
    │ │
    │ └─ sshCert != "" ?
    │ └─ YES → 证书+私钥: NewCertSigner(cert, signer) → PublicKeys(certSigner)

    └─ SSHKeyPath != "" ?
    └─ YES → 读取文件 → 回到上面的私钥处理流程

    实际优先级(在 GetSSHConfig 中):

  • SSH Agent(如果 useAgentAuth=true)
  • 私钥(如果 sshPrivateKeyString != "")
  • 密码(如果 password != "")
  • 在 NewSSHDialer 中的前置逻辑:

    • 如果 password != "" → 不加载密钥文件
    • 如果 useSSHAgentAuth → 不加载密钥文件
    • 如果 sshKey 已有值 → 不加载密钥文件
    • 只有以上都不满足且 SSHKeyPath != "" → 才从文件读取

    4.2 重试机制全景

    ┌─────────────────────────────────────────────────────────────────┐
    │ Layer 1: SSH 连接重试 (NewSSHDialer) │
    │ ┌───────────────────────────────────────────────────────┐ │
    │ │ Backoff: 15s × 5 = 75s max │ │
    │ │ 策略: 固定间隔 (Factor=1) │ │
    │ │ 行为: Dial失败 → (false, nil) → 继续重试 │ │
    │ │ 超时: 15s/次 (timeout=true时) │ │
    │ └───────────────────────────────────────────────────────┘ │
    │ │
    │ Layer 2: 命令执行重试 (executeWithRetry) │
    │ ┌───────────────────────────────────────────────────────┐ │
    │ │ Backoff: 2s × count (调用方指定) │ │
    │ │ 策略: 固定间隔 (Factor=1) │ │
    │ │ 行为: 每次重试都重新创建 SSHDialer (含Layer1重试) │ │
    │ │ 注意: Layer2 × Layer1 = 潜在 75s × count │ │
    │ └───────────────────────────────────────────────────────┘ │
    │ │
    │ Layer 3: Docker 连接重试 (NewDockerShell) │
    │ ┌───────────────────────────────────────────────────────┐ │
    │ │ Backoff: 30s × 20 = 600s max (10分钟!) │ │
    │ │ 策略: 固定间隔 (Factor=1) │ │
    │ │ 行为: err != nil → (false, err) → 立即中断重试 ⚠️ │ │
    │ │ 实际效果: 首次失败即退出 │ │
    │ └───────────────────────────────────────────────────────┘ │
    └─────────────────────────────────────────────────────────────────┘


    5. Mermaid 图集

    5.1 SSH 连接流程图

    #mermaid-svg-lw96VU5dglswd5Si{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lw96VU5dglswd5Si .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lw96VU5dglswd5Si .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lw96VU5dglswd5Si .error-icon{fill:#552222;}#mermaid-svg-lw96VU5dglswd5Si .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lw96VU5dglswd5Si .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lw96VU5dglswd5Si .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lw96VU5dglswd5Si .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lw96VU5dglswd5Si .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lw96VU5dglswd5Si .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lw96VU5dglswd5Si .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lw96VU5dglswd5Si .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lw96VU5dglswd5Si .marker.cross{stroke:#333333;}#mermaid-svg-lw96VU5dglswd5Si svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lw96VU5dglswd5Si p{margin:0;}#mermaid-svg-lw96VU5dglswd5Si .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-lw96VU5dglswd5Si .cluster-label text{fill:#333;}#mermaid-svg-lw96VU5dglswd5Si .cluster-label span{color:#333;}#mermaid-svg-lw96VU5dglswd5Si .cluster-label span p{background-color:transparent;}#mermaid-svg-lw96VU5dglswd5Si .label text,#mermaid-svg-lw96VU5dglswd5Si span{fill:#333;color:#333;}#mermaid-svg-lw96VU5dglswd5Si .node rect,#mermaid-svg-lw96VU5dglswd5Si .node circle,#mermaid-svg-lw96VU5dglswd5Si .node ellipse,#mermaid-svg-lw96VU5dglswd5Si .node polygon,#mermaid-svg-lw96VU5dglswd5Si .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lw96VU5dglswd5Si .rough-node .label text,#mermaid-svg-lw96VU5dglswd5Si .node .label text,#mermaid-svg-lw96VU5dglswd5Si .image-shape .label,#mermaid-svg-lw96VU5dglswd5Si .icon-shape .label{text-anchor:middle;}#mermaid-svg-lw96VU5dglswd5Si .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lw96VU5dglswd5Si .rough-node .label,#mermaid-svg-lw96VU5dglswd5Si .node .label,#mermaid-svg-lw96VU5dglswd5Si .image-shape .label,#mermaid-svg-lw96VU5dglswd5Si .icon-shape .label{text-align:center;}#mermaid-svg-lw96VU5dglswd5Si .node.clickable{cursor:pointer;}#mermaid-svg-lw96VU5dglswd5Si .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lw96VU5dglswd5Si .arrowheadPath{fill:#333333;}#mermaid-svg-lw96VU5dglswd5Si .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lw96VU5dglswd5Si .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lw96VU5dglswd5Si .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lw96VU5dglswd5Si .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lw96VU5dglswd5Si .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lw96VU5dglswd5Si .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lw96VU5dglswd5Si .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lw96VU5dglswd5Si .cluster text{fill:#333;}#mermaid-svg-lw96VU5dglswd5Si .cluster span{color:#333;}#mermaid-svg-lw96VU5dglswd5Si div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lw96VU5dglswd5Si .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lw96VU5dglswd5Si rect.text{fill:none;stroke-width:0;}#mermaid-svg-lw96VU5dglswd5Si .icon-shape,#mermaid-svg-lw96VU5dglswd5Si .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lw96VU5dglswd5Si .icon-shape p,#mermaid-svg-lw96VU5dglswd5Si .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lw96VU5dglswd5Si .icon-shape .label rect,#mermaid-svg-lw96VU5dglswd5Si .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lw96VU5dglswd5Si .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lw96VU5dglswd5Si .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lw96VU5dglswd5Si :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    NO

    YES

    YES

    NO

    YES

    YES

    NO

    NO

    YES

    NO

    YES

    NO

    Dial

    YES

    NO

    YES

    YES

    NO

    YES

    NO

    NO

    YES

    NO

    Dial(t)

    timeout = 15s if t else 0

    utils.GetSSHConfig()构建 ClientConfig

    useAgentAuth ?

    SSH Agent 认证

    sshKey != '' ?

    passphrase != '' ?

    ParsePrivateKeyWithPassphrase

    ParsePrivateKey

    sshCert != '' ?

    NewCertSigner

    PublicKeys(signer)

    password != '' ?

    Password(password)

    无认证方式

    ssh.Dial tcp, sshAddress, cfg

    NewSSHDialer(n, timeout, logger)

    len(PublicIPAddress) > 0|| InstanceID != '' ?

    return errorno node IP or node ID

    初始化 SSHDialeruid=-1, shells={}

    PublicIPAddress > 0 ?

    sshAddress = IP:Port

    sshAddress = InstanceID

    password=='' && sshKey==''&& !useSSHAgentAuth&& SSHKeyPath != '' ?

    utils.SSHPrivateKeyPath()读取私钥文件内容

    sshCert=='' &&SSHCertPath != '' ?

    utils.SSHCertificatePath()读取证书文件内容

    进入 Backoff 重试循环

    try++Dial(timeout)

    ssh.Dial 成功 ?

    d.conn = creturn dialer, nil

    try < 5 ?

    等待 15s

    return errorinit dialer timeout

    5.2 Script 与 Shell 接口类图

    #mermaid-svg-CljTQwQgPywNhb6u{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CljTQwQgPywNhb6u .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CljTQwQgPywNhb6u .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CljTQwQgPywNhb6u .error-icon{fill:#552222;}#mermaid-svg-CljTQwQgPywNhb6u .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CljTQwQgPywNhb6u .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CljTQwQgPywNhb6u .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CljTQwQgPywNhb6u .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CljTQwQgPywNhb6u .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CljTQwQgPywNhb6u .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CljTQwQgPywNhb6u .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CljTQwQgPywNhb6u .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CljTQwQgPywNhb6u .marker.cross{stroke:#333333;}#mermaid-svg-CljTQwQgPywNhb6u svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CljTQwQgPywNhb6u p{margin:0;}#mermaid-svg-CljTQwQgPywNhb6u g.classGroup text{fill:#9370DB;stroke:none;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-CljTQwQgPywNhb6u g.classGroup text .title{font-weight:bolder;}#mermaid-svg-CljTQwQgPywNhb6u .cluster-label text{fill:#333;}#mermaid-svg-CljTQwQgPywNhb6u .cluster-label span{color:#333;}#mermaid-svg-CljTQwQgPywNhb6u .cluster-label span p{background-color:transparent;}#mermaid-svg-CljTQwQgPywNhb6u .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CljTQwQgPywNhb6u .cluster text{fill:#333;}#mermaid-svg-CljTQwQgPywNhb6u .cluster span{color:#333;}#mermaid-svg-CljTQwQgPywNhb6u .nodeLabel,#mermaid-svg-CljTQwQgPywNhb6u .edgeLabel{color:#131300;}#mermaid-svg-CljTQwQgPywNhb6u .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-CljTQwQgPywNhb6u .label text{fill:#131300;}#mermaid-svg-CljTQwQgPywNhb6u .labelBkg{background:#ECECFF;}#mermaid-svg-CljTQwQgPywNhb6u .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-CljTQwQgPywNhb6u .classTitle{font-weight:bolder;}#mermaid-svg-CljTQwQgPywNhb6u .node rect,#mermaid-svg-CljTQwQgPywNhb6u .node circle,#mermaid-svg-CljTQwQgPywNhb6u .node ellipse,#mermaid-svg-CljTQwQgPywNhb6u .node polygon,#mermaid-svg-CljTQwQgPywNhb6u .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CljTQwQgPywNhb6u .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-CljTQwQgPywNhb6u g.clickable{cursor:pointer;}#mermaid-svg-CljTQwQgPywNhb6u g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-CljTQwQgPywNhb6u g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-CljTQwQgPywNhb6u .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-CljTQwQgPywNhb6u .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-CljTQwQgPywNhb6u .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-CljTQwQgPywNhb6u .dashed-line{stroke-dasharray:3;}#mermaid-svg-CljTQwQgPywNhb6u .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-CljTQwQgPywNhb6u #compositionStart,#mermaid-svg-CljTQwQgPywNhb6u .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-CljTQwQgPywNhb6u #compositionEnd,#mermaid-svg-CljTQwQgPywNhb6u .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-CljTQwQgPywNhb6u #dependencyStart,#mermaid-svg-CljTQwQgPywNhb6u .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-CljTQwQgPywNhb6u #dependencyStart,#mermaid-svg-CljTQwQgPywNhb6u .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-CljTQwQgPywNhb6u #extensionStart,#mermaid-svg-CljTQwQgPywNhb6u .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-CljTQwQgPywNhb6u #extensionEnd,#mermaid-svg-CljTQwQgPywNhb6u .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-CljTQwQgPywNhb6u #aggregationStart,#mermaid-svg-CljTQwQgPywNhb6u .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-CljTQwQgPywNhb6u #aggregationEnd,#mermaid-svg-CljTQwQgPywNhb6u .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-CljTQwQgPywNhb6u #lollipopStart,#mermaid-svg-CljTQwQgPywNhb6u .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-CljTQwQgPywNhb6u #lollipopEnd,#mermaid-svg-CljTQwQgPywNhb6u .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-CljTQwQgPywNhb6u .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-CljTQwQgPywNhb6u .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CljTQwQgPywNhb6u .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CljTQwQgPywNhb6u .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CljTQwQgPywNhb6u :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    implements

    implements

    implements

    implements

    creates via OpenShell()

    delegates

    owns

    creates

    parsed from JSON

    passed to ChangeWindowSize

    «interface»

    Script

    +ExecuteCommands(cmds …string)(string, error)

    +Close() : error

    «interface»

    Shell

    +SetIO(stdout, stderr io.Writer, stdin io.ReadCloser)

    +ChangeWindowSize(win ShellWindowSize) : error

    +Terminal() : error

    +OpenTerminal(win ShellWindowSize) : error

    +Wait() : error

    +Write(b []byte) : error

    +Close() : error

    ShellWindowSize

    +Width int

    +Height int

    SSHDialer

    -sshKey string

    -sshCert string

    -sshAddress string

    -username string

    -password string

    -passphrase string

    -useSSHAgentAuth bool

    -conn *ssh.Client

    -uid int

    -logger *logrus.Logger

    -shells map[Shell]Shell

    +NewSSHDialer(n *Node, timeout bool, logger *Logger)(*SSHDialer, error)

    +Dial(t bool)(*ssh.Client, error)

    +GetClient() : *ssh.Client

    -getUserID() : error

    +Close() : error

    -wrapCommands(cmd string) : string

    +ExecuteCommands(cmds …string)(string, error)

    +OpenShell()(Shell, error)

    SSHShell

    +Stdin io.ReadCloser

    +Stdout io.Writer

    +Stderr io.Writer

    +Writer io.Writer

    +Term string

    +Modes ssh.TerminalModes

    -session *ssh.Session

    -dialer *SSHDialer

    +Write([]byte) : error

    +Wait() : error

    +Close() : error

    +SetIO(stdout, stderr, stdin)

    +ChangeWindowSize(win) : error

    +SetWriter(w io.Writer) : *SSHShell

    +Terminal() : error

    +OpenTerminal(win) : error

    DockerShell

    -execID string

    +Stdin io.ReadCloser

    +Stdout io.Writer

    +Stderr io.Writer

    +Writer io.Writer

    -ctx context.Context

    -client client.APIClient

    -response *HijackedResponse

    +NewDockerShell(n *Node)(*DockerShell, error)

    +Close() : error

    +SetStdio(stdout, stderr, stdin) : *DockerShell

    +SetIO(stdout, stderr, stdin)

    +SetWriter(w) : *DockerShell

    +Terminal() : error

    +OpenTerminal(win) : error

    +ExecStart(needRestore bool) : error

    +Wait() : error

    +ResizeTtyTo(ctx, h, w) : error

    +ResizeTty(ctx) : error

    +MonitorTtySize(ctx) : error

    -setInput(needRestore bool) (func(), error)

    -beginOutputStream(restore) : chan error

    -beginInputStream(restore)(chan struct, chan error)

    +ChangeWindowSize(win) : error

    +Write([]byte) : error

    PtyShell

    +Stdin io.ReadCloser

    +Stdout io.Writer

    +Stderr io.Writer

    +Writer io.Writer

    -ctx context.Context

    -conn *os.File

    -cmd *exec.Cmd

    +NewPtyShell(cmd *exec.Cmd)(*PtyShell, error)

    +Close() : error

    +SetIO(stdout, stderr, stdin)

    +Terminal() : error

    +OpenTerminal(win) : error

    +ChangeWindowSize(win) : error

    +Wait() : error

    +Write(b []byte) : error

    WebSocketDialer

    -dialer Shell

    -conn *websocket.Conn

    -reader *TerminalReader

    +NewWebSocketDialer(conn, dialer) : *WebSocketDialer

    +Close()

    +Write(bytes []byte) : error

    +Terminal(height, width int) : error

    +ReadMessage(ctx) : error

    +ChangeWindowSize(win)

    TerminalReader

    -conn *websocket.Conn

    -reader io.Reader

    +ClosedCh chan bool

    -resize func(ShellWindowSize)

    +NewTerminalReader(conn) : *TerminalReader

    +Close() : error

    +SetResizeFunction(fn)

    +Read(p []byte)(int, error)

    BinaryWriter

    -conn *websocket.Conn

    +NewBinaryWriter(conn) : *BinaryWriter

    +Write(p []byte)(int, error)

    5.3 ExecuteCommands 流程图

    #mermaid-svg-fwb0HEx5cs42zyW9{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fwb0HEx5cs42zyW9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fwb0HEx5cs42zyW9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fwb0HEx5cs42zyW9 .error-icon{fill:#552222;}#mermaid-svg-fwb0HEx5cs42zyW9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fwb0HEx5cs42zyW9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fwb0HEx5cs42zyW9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fwb0HEx5cs42zyW9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fwb0HEx5cs42zyW9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fwb0HEx5cs42zyW9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fwb0HEx5cs42zyW9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fwb0HEx5cs42zyW9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fwb0HEx5cs42zyW9 .marker.cross{stroke:#333333;}#mermaid-svg-fwb0HEx5cs42zyW9 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fwb0HEx5cs42zyW9 p{margin:0;}#mermaid-svg-fwb0HEx5cs42zyW9 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-fwb0HEx5cs42zyW9 .cluster-label text{fill:#333;}#mermaid-svg-fwb0HEx5cs42zyW9 .cluster-label span{color:#333;}#mermaid-svg-fwb0HEx5cs42zyW9 .cluster-label span p{background-color:transparent;}#mermaid-svg-fwb0HEx5cs42zyW9 .label text,#mermaid-svg-fwb0HEx5cs42zyW9 span{fill:#333;color:#333;}#mermaid-svg-fwb0HEx5cs42zyW9 .node rect,#mermaid-svg-fwb0HEx5cs42zyW9 .node circle,#mermaid-svg-fwb0HEx5cs42zyW9 .node ellipse,#mermaid-svg-fwb0HEx5cs42zyW9 .node polygon,#mermaid-svg-fwb0HEx5cs42zyW9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fwb0HEx5cs42zyW9 .rough-node .label text,#mermaid-svg-fwb0HEx5cs42zyW9 .node .label text,#mermaid-svg-fwb0HEx5cs42zyW9 .image-shape .label,#mermaid-svg-fwb0HEx5cs42zyW9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-fwb0HEx5cs42zyW9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fwb0HEx5cs42zyW9 .rough-node .label,#mermaid-svg-fwb0HEx5cs42zyW9 .node .label,#mermaid-svg-fwb0HEx5cs42zyW9 .image-shape .label,#mermaid-svg-fwb0HEx5cs42zyW9 .icon-shape .label{text-align:center;}#mermaid-svg-fwb0HEx5cs42zyW9 .node.clickable{cursor:pointer;}#mermaid-svg-fwb0HEx5cs42zyW9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fwb0HEx5cs42zyW9 .arrowheadPath{fill:#333333;}#mermaid-svg-fwb0HEx5cs42zyW9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fwb0HEx5cs42zyW9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fwb0HEx5cs42zyW9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fwb0HEx5cs42zyW9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fwb0HEx5cs42zyW9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fwb0HEx5cs42zyW9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fwb0HEx5cs42zyW9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fwb0HEx5cs42zyW9 .cluster text{fill:#333;}#mermaid-svg-fwb0HEx5cs42zyW9 .cluster span{color:#333;}#mermaid-svg-fwb0HEx5cs42zyW9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fwb0HEx5cs42zyW9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fwb0HEx5cs42zyW9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-fwb0HEx5cs42zyW9 .icon-shape,#mermaid-svg-fwb0HEx5cs42zyW9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fwb0HEx5cs42zyW9 .icon-shape p,#mermaid-svg-fwb0HEx5cs42zyW9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fwb0HEx5cs42zyW9 .icon-shape .label rect,#mermaid-svg-fwb0HEx5cs42zyW9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fwb0HEx5cs42zyW9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fwb0HEx5cs42zyW9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fwb0HEx5cs42zyW9 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    YES

    NO

    YES

    NO

    YES

    NO

    ExecuteCommands(cmds …string)

    getUserID()

    uid >= 0 ?(已缓存)

    跳过

    conn.NewSession()

    session.Output(id -u)

    strconv.Atoi(output)

    d.uid = uid

    uid > 0 ?(非root)

    sudo = 'sudo'

    sudo = ''

    conn.NewSession()

    cmds 用 \\ 连接

    wrapCommands:包装为 #!/bin/shset -e

    base64 编码

    构造最终命令:echo base64 | base64 -d | sudo sh –

    创建 output buffer

    创建 singleWriter:MultiWriter(logger.Out, output)

    session.Stderr = combinedOutput

    session.Stdout = combinedOutput

    session.Run(cmd)

    Run 成功 ?

    return output.String(), nil

    return output.String(), err

    defer: session.Close()

    5.4 DockerDialer 流程图

    #mermaid-svg-SVkSFcqDg1XwFkrP{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SVkSFcqDg1XwFkrP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SVkSFcqDg1XwFkrP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SVkSFcqDg1XwFkrP .error-icon{fill:#552222;}#mermaid-svg-SVkSFcqDg1XwFkrP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SVkSFcqDg1XwFkrP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SVkSFcqDg1XwFkrP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SVkSFcqDg1XwFkrP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SVkSFcqDg1XwFkrP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SVkSFcqDg1XwFkrP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SVkSFcqDg1XwFkrP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SVkSFcqDg1XwFkrP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SVkSFcqDg1XwFkrP .marker.cross{stroke:#333333;}#mermaid-svg-SVkSFcqDg1XwFkrP svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SVkSFcqDg1XwFkrP p{margin:0;}#mermaid-svg-SVkSFcqDg1XwFkrP .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-SVkSFcqDg1XwFkrP .cluster-label text{fill:#333;}#mermaid-svg-SVkSFcqDg1XwFkrP .cluster-label span{color:#333;}#mermaid-svg-SVkSFcqDg1XwFkrP .cluster-label span p{background-color:transparent;}#mermaid-svg-SVkSFcqDg1XwFkrP .label text,#mermaid-svg-SVkSFcqDg1XwFkrP span{fill:#333;color:#333;}#mermaid-svg-SVkSFcqDg1XwFkrP .node rect,#mermaid-svg-SVkSFcqDg1XwFkrP .node circle,#mermaid-svg-SVkSFcqDg1XwFkrP .node ellipse,#mermaid-svg-SVkSFcqDg1XwFkrP .node polygon,#mermaid-svg-SVkSFcqDg1XwFkrP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SVkSFcqDg1XwFkrP .rough-node .label text,#mermaid-svg-SVkSFcqDg1XwFkrP .node .label text,#mermaid-svg-SVkSFcqDg1XwFkrP .image-shape .label,#mermaid-svg-SVkSFcqDg1XwFkrP .icon-shape .label{text-anchor:middle;}#mermaid-svg-SVkSFcqDg1XwFkrP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SVkSFcqDg1XwFkrP .rough-node .label,#mermaid-svg-SVkSFcqDg1XwFkrP .node .label,#mermaid-svg-SVkSFcqDg1XwFkrP .image-shape .label,#mermaid-svg-SVkSFcqDg1XwFkrP .icon-shape .label{text-align:center;}#mermaid-svg-SVkSFcqDg1XwFkrP .node.clickable{cursor:pointer;}#mermaid-svg-SVkSFcqDg1XwFkrP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SVkSFcqDg1XwFkrP .arrowheadPath{fill:#333333;}#mermaid-svg-SVkSFcqDg1XwFkrP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SVkSFcqDg1XwFkrP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SVkSFcqDg1XwFkrP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SVkSFcqDg1XwFkrP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SVkSFcqDg1XwFkrP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SVkSFcqDg1XwFkrP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SVkSFcqDg1XwFkrP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SVkSFcqDg1XwFkrP .cluster text{fill:#333;}#mermaid-svg-SVkSFcqDg1XwFkrP .cluster span{color:#333;}#mermaid-svg-SVkSFcqDg1XwFkrP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SVkSFcqDg1XwFkrP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SVkSFcqDg1XwFkrP rect.text{fill:none;stroke-width:0;}#mermaid-svg-SVkSFcqDg1XwFkrP .icon-shape,#mermaid-svg-SVkSFcqDg1XwFkrP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SVkSFcqDg1XwFkrP .icon-shape p,#mermaid-svg-SVkSFcqDg1XwFkrP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SVkSFcqDg1XwFkrP .icon-shape .label rect,#mermaid-svg-SVkSFcqDg1XwFkrP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SVkSFcqDg1XwFkrP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SVkSFcqDg1XwFkrP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SVkSFcqDg1XwFkrP :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    NO

    YES

    >1

    0

    1

    NO

    YES

    NO

    YES

    YES

    NO

    YES

    NO

    YES

    NO

    NewDockerShell(n *Node)

    n.InstanceID != '' ?

    error: no container ID

    创建 DockerShellctx = Background

    dockerutils.GetDockerClient()

    构造 name filter:^/?(k3d-)?InstanceID$

    docker.ContainerList(All: true)

    len == 1 ?

    error: multiple containers

    error: no container found

    Backoff 重试循环30s × 20

    ContainerExecCreate:Privileged, Tty, /bin/sh

    Create 成功 ?

    return false, err⚠️ 立即中断重试

    ContainerExecAttach:Tty=true

    Attach 成功 ?

    return false, err⚠️ 立即中断重试

    d.execID = exec.IDd.response = execAttach

    return dialer, nil

    Terminal() (CLI模式)

    defer Close()

    ExecStart(needRestore=true)

    setInput(true):SetRawTerminal + EscapeProxy

    beginOutputStream:response.Reader → io.Copy → Stderr

    beginInputStream:Stdin → io.Copy → response.Conn

    select: outputDone/inputDone/detached/ctx.Done

    term.IsTerminal ?

    MonitorTtySize:SIGWINCH → ResizeTty

    跳过监控

    Wait():ContainerExecInspect

    ExitCode == 0 ?

    return nil

    err == io.EOF ?

    return err

    OpenTerminal (WebSocket模式)

    ExecStart(needRestore=false)

    setInput(false):仅 EscapeProxy, 不设Raw

    beginOutputStream + beginInputStream

    非阻塞返回 nil

    5.5 WebSocket 通信图

    Remote Host

    Shell (SSH/Docker/PTY)

    BinaryWriter

    TerminalReader

    WebSocketDialer

    API Server

    Web Browser

    Remote Host

    Shell (SSH/Docker/PTY)

    BinaryWriter

    TerminalReader

    WebSocketDialer

    API Server

    Web Browser

    #mermaid-svg-7nqIIzuGPjgPKGjg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7nqIIzuGPjgPKGjg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7nqIIzuGPjgPKGjg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7nqIIzuGPjgPKGjg .error-icon{fill:#552222;}#mermaid-svg-7nqIIzuGPjgPKGjg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7nqIIzuGPjgPKGjg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7nqIIzuGPjgPKGjg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7nqIIzuGPjgPKGjg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7nqIIzuGPjgPKGjg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7nqIIzuGPjgPKGjg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7nqIIzuGPjgPKGjg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7nqIIzuGPjgPKGjg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7nqIIzuGPjgPKGjg .marker.cross{stroke:#333333;}#mermaid-svg-7nqIIzuGPjgPKGjg svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7nqIIzuGPjgPKGjg p{margin:0;}#mermaid-svg-7nqIIzuGPjgPKGjg .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7nqIIzuGPjgPKGjg text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-7nqIIzuGPjgPKGjg .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7nqIIzuGPjgPKGjg .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-7nqIIzuGPjgPKGjg .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-7nqIIzuGPjgPKGjg .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-7nqIIzuGPjgPKGjg #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-7nqIIzuGPjgPKGjg .sequenceNumber{fill:white;}#mermaid-svg-7nqIIzuGPjgPKGjg #sequencenumber{fill:#333;}#mermaid-svg-7nqIIzuGPjgPKGjg #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-7nqIIzuGPjgPKGjg .messageText{fill:#333;stroke:none;}#mermaid-svg-7nqIIzuGPjgPKGjg .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7nqIIzuGPjgPKGjg .labelText,#mermaid-svg-7nqIIzuGPjgPKGjg .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-7nqIIzuGPjgPKGjg .loopText,#mermaid-svg-7nqIIzuGPjgPKGjg .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-7nqIIzuGPjgPKGjg .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7nqIIzuGPjgPKGjg .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-7nqIIzuGPjgPKGjg .noteText,#mermaid-svg-7nqIIzuGPjgPKGjg .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-7nqIIzuGPjgPKGjg .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7nqIIzuGPjgPKGjg .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7nqIIzuGPjgPKGjg .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7nqIIzuGPjgPKGjg .actorPopupMenu{position:absolute;}#mermaid-svg-7nqIIzuGPjgPKGjg .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-7nqIIzuGPjgPKGjg .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7nqIIzuGPjgPKGjg .actor-man circle,#mermaid-svg-7nqIIzuGPjgPKGjg line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-7nqIIzuGPjgPKGjg :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    SSH: RequestPty + Shell

    Docker: ExecStart(false)

    PTY: pty.StartWithSize

    alt

    [用户输入]

    alt

    [窗口调整]

    alt

    [远程输出]

    loop

    [终端会话]

    alt

    [正常关闭]

    [浏览器关闭]

    [请求超时]

    HTTP Upgrade (provider, cluster, node, height, width)

    upgrader.Upgrade() → WebSocket

    NewWebSocketDialer(conn, shell)

    NewTerminalReader(conn)

    SetResizeFunction(ChangeWindowSize)

    NewBinaryWriter(conn)

    SetIO(BW, BW, TR)

    OpenTerminal(ShellWindowSize{150, 300})

    ReadMessage(ctx)

    BinaryMessage (键盘输入)

    Read() → 返回二进制数据

    stdin → io.Copy → Remote

    传输输入

    TextMessage {"Width":80,"Height":24}

    json.Unmarshal → ShellWindowSize

    resize(r) 回调

    ChangeWindowSize(win)

    WindowChange / ResizeTtyTo / pty.Setsize

    stdout/stderr 输出

    Write(输出数据)

    BinaryMessage (终端输出)

    会话结束

    Wait() 返回

    sessionClosed 信号

    Close()

    Close()

    CloseMessage (EOF)

    WebSocket Close

    ClosedCh ← true

    stop 信号

    Close()

    Close()

    ctx.Done()

    Close()

    Close()

    5.6 三种 Dialer 的统一架构对比

    #mermaid-svg-dBGbimjqjzmwRLJY{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dBGbimjqjzmwRLJY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dBGbimjqjzmwRLJY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dBGbimjqjzmwRLJY .error-icon{fill:#552222;}#mermaid-svg-dBGbimjqjzmwRLJY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dBGbimjqjzmwRLJY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dBGbimjqjzmwRLJY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dBGbimjqjzmwRLJY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dBGbimjqjzmwRLJY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dBGbimjqjzmwRLJY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dBGbimjqjzmwRLJY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dBGbimjqjzmwRLJY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dBGbimjqjzmwRLJY .marker.cross{stroke:#333333;}#mermaid-svg-dBGbimjqjzmwRLJY svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dBGbimjqjzmwRLJY p{margin:0;}#mermaid-svg-dBGbimjqjzmwRLJY .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-dBGbimjqjzmwRLJY .cluster-label text{fill:#333;}#mermaid-svg-dBGbimjqjzmwRLJY .cluster-label span{color:#333;}#mermaid-svg-dBGbimjqjzmwRLJY .cluster-label span p{background-color:transparent;}#mermaid-svg-dBGbimjqjzmwRLJY .label text,#mermaid-svg-dBGbimjqjzmwRLJY span{fill:#333;color:#333;}#mermaid-svg-dBGbimjqjzmwRLJY .node rect,#mermaid-svg-dBGbimjqjzmwRLJY .node circle,#mermaid-svg-dBGbimjqjzmwRLJY .node ellipse,#mermaid-svg-dBGbimjqjzmwRLJY .node polygon,#mermaid-svg-dBGbimjqjzmwRLJY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dBGbimjqjzmwRLJY .rough-node .label text,#mermaid-svg-dBGbimjqjzmwRLJY .node .label text,#mermaid-svg-dBGbimjqjzmwRLJY .image-shape .label,#mermaid-svg-dBGbimjqjzmwRLJY .icon-shape .label{text-anchor:middle;}#mermaid-svg-dBGbimjqjzmwRLJY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dBGbimjqjzmwRLJY .rough-node .label,#mermaid-svg-dBGbimjqjzmwRLJY .node .label,#mermaid-svg-dBGbimjqjzmwRLJY .image-shape .label,#mermaid-svg-dBGbimjqjzmwRLJY .icon-shape .label{text-align:center;}#mermaid-svg-dBGbimjqjzmwRLJY .node.clickable{cursor:pointer;}#mermaid-svg-dBGbimjqjzmwRLJY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dBGbimjqjzmwRLJY .arrowheadPath{fill:#333333;}#mermaid-svg-dBGbimjqjzmwRLJY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dBGbimjqjzmwRLJY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dBGbimjqjzmwRLJY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dBGbimjqjzmwRLJY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dBGbimjqjzmwRLJY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dBGbimjqjzmwRLJY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dBGbimjqjzmwRLJY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dBGbimjqjzmwRLJY .cluster text{fill:#333;}#mermaid-svg-dBGbimjqjzmwRLJY .cluster span{color:#333;}#mermaid-svg-dBGbimjqjzmwRLJY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dBGbimjqjzmwRLJY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dBGbimjqjzmwRLJY rect.text{fill:none;stroke-width:0;}#mermaid-svg-dBGbimjqjzmwRLJY .icon-shape,#mermaid-svg-dBGbimjqjzmwRLJY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dBGbimjqjzmwRLJY .icon-shape p,#mermaid-svg-dBGbimjqjzmwRLJY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dBGbimjqjzmwRLJY .icon-shape .label rect,#mermaid-svg-dBGbimjqjzmwRLJY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dBGbimjqjzmwRLJY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dBGbimjqjzmwRLJY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dBGbimjqjzmwRLJY :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    Script 批量命令模式

    cluster.execute()

    NewSSHDialer()

    ExecuteCommands(cmds…)

    base64编码 → session.Run()

    Close()

    WebSocket Kubectl 模式

    kubectl.KubeHandler()

    HTTP → WebSocket Upgrade

    NewPtyShell(bash)

    NewWebSocketDialer(conn, pty)

    Terminal(h, w) → OpenTerminal()

    Write(alias commands)

    ReadMessage(ctx)

    WebSocket SSH 模式

    YES

    NO

    ssh.Handler()

    HTTP → WebSocket Upgrade

    newDialer()

    Provider == k3d ?

    NewDockerShell()

    NewSSHDialer() → OpenShell()

    NewWebSocketDialer(conn, shell)

    Terminal(h, w) → OpenTerminal()

    ReadMessage(ctx)

    CLI 模式 (autok3s ssh terminal)

    cluster.terminal()

    NewSSHDialer()

    OpenShell()

    SetIO(os.Stdout, os.Stderr, os.Stdin)

    SSHShell.Terminal()

    MakeRaw → OpenTerminal → Wait


    附录:关键设计总结

    A. 命令执行的 base64 包装设计

    原始命令 → strings.Join(\\n) → #!/bin/sh\\nset -e\\n<cmds> → base64编码 → echo "base64" | base64 -d | sudo sh –

    优点: 避免转义问题,支持多命令,强制错误传播 缺点: 无法看到实际执行的命令(日志中只有 base64),调试困难

    B. 三种实现的功能矩阵

    功能SSHDialer/SSHShellDockerShellPtyShell
    Script (ExecuteCommands)
    Shell (OpenTerminal)
    CLI Terminal() ✅ (SSHShell)
    WebSocket Terminal() ✅ (via WebSocketDialer)
    ChangeWindowSize ✅ (WindowChange) ✅ (ContainerExecResize) ✅ (pty.Setsize)
    平台限制 全平台 全平台 darwin/linux only
    认证方式 密码/密钥/Agent/证书 Docker socket 无(本地)
    重试机制 15s×5 30s×20 (实际1次)

    C. 安全注意事项

  • ssh.InsecureIgnoreHostKey() — 跳过主机密钥验证
  • Docker exec 使用 Privileged: true — 容器内特权
  • WebSocket CheckOrigin 始终返回 true — 允许任意来源的 WebSocket 连接
  • sudo 自动提权 — 当非 root 用户时自动添加 sudo 前缀
  • D. 已发现的代码问题

  • DockerDialer 重试失效:return false, err 导致 wait.ExponentialBackoff 在首次错误即终止,20次重试配置形同虚设
  • SSHDialer.Close() 逻辑:conn != nil 时不清理 shells map(虽然 conn.Close 会隐式关闭 sessions)
  • SSHShell.Write() 空实现:return nil 不写入任何数据,PtyShell 和 DockerShell 也有类似的空/近空 Write
  • DockerShell.setInput 中 fd 交叉:RestoreTerminal(inFd, outState) 和 RestoreTerminal(outFd, inState) 参数顺序可能反了
  • 赞(0)
    未经允许不得转载:171主机测评 » 【k3s】AutoK3s v0.9.3 —— Part 6 SSH/终端模块超深度逐行分析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址