From 2d8b1d4438406a65a66f1de5bb7ccb6b584c334a Mon Sep 17 00:00:00 2001 From: orohi Date: Wed, 29 Jul 2026 04:24:48 +0300 Subject: [PATCH] Fix node SSH upload by replacing broken scp with stdin cat. Co-authored-by: Cursor --- internal/nodeinstall/ssh.go | 92 ++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/internal/nodeinstall/ssh.go b/internal/nodeinstall/ssh.go index 2537475..b2b93e6 100644 --- a/internal/nodeinstall/ssh.go +++ b/internal/nodeinstall/ssh.go @@ -7,6 +7,7 @@ import ( "net" "os" "path/filepath" + "strconv" "strings" "time" @@ -72,10 +73,16 @@ func run(client *ssh.Client, cmd string, log LogFunc) (string, error) { return out, nil } -func writeRemoteFile(client *ssh.Client, remotePath, content string, mode os.FileMode, log LogFunc) error { +func shellQuote(s string) string { + return strconv.Quote(s) +} + +// uploadViaStdin writes bytes to remotePath using `cat` over SSH stdin. +// Avoids legacy `scp -t`, which fails on many modern OpenSSH servers (status 1). +func uploadViaStdin(client *ssh.Client, remotePath string, data []byte, mode os.FileMode, log LogFunc) error { dir := filepath.ToSlash(filepath.Dir(remotePath)) - if _, err := run(client, fmt.Sprintf("mkdir -p %q", dir), log); err != nil { - return err + if _, err := run(client, "mkdir -p "+shellQuote(dir)+" 2>/dev/null || sudo mkdir -p "+shellQuote(dir), log); err != nil { + return fmt.Errorf("mkdir %s: %w", dir, err) } session, err := client.NewSession() @@ -84,61 +91,52 @@ func writeRemoteFile(client *ssh.Client, remotePath, content string, mode os.Fil } defer session.Close() + var stderr bytes.Buffer + session.Stderr = &stderr + stdin, err := session.StdinPipe() if err != nil { return err } - base := filepath.Base(remotePath) - go func() { - defer stdin.Close() - fmt.Fprintf(stdin, "C%04o %d %s\n", mode, len(content), base) - _, _ = io.WriteString(stdin, content) - fmt.Fprint(stdin, "\x00") - }() + // Prefer direct write; fall back to sudo tee if permission denied. + cmd := fmt.Sprintf( + `sh -c 'cat > %s && chmod %o %s' 2>/dev/null || sudo sh -c 'cat > %s && chmod %o %s'`, + shellQuote(remotePath), mode, shellQuote(remotePath), + shellQuote(remotePath), mode, shellQuote(remotePath), + ) if log != nil { - log(fmt.Sprintf("upload %s (%d bytes)", remotePath, len(content))) + log(fmt.Sprintf("upload %s (%d bytes) via ssh stdin", remotePath, len(data))) } - cmd := fmt.Sprintf("scp -t %q", dir) - if err := session.Run(cmd); err != nil { - return fmt.Errorf("scp %s: %w", remotePath, err) + + if err := session.Start(cmd); err != nil { + return fmt.Errorf("start upload: %w", err) + } + + if _, err := io.Copy(stdin, bytes.NewReader(data)); err != nil { + _ = stdin.Close() + _ = session.Wait() + return fmt.Errorf("write upload: %w (%s)", err, strings.TrimSpace(stderr.String())) + } + _ = stdin.Close() + + if err := session.Wait(); err != nil { + msg := strings.TrimSpace(stderr.String()) + if msg == "" { + msg = err.Error() + } + return fmt.Errorf("upload %s failed: %s", remotePath, msg) } return nil } +func writeRemoteFile(client *ssh.Client, remotePath, content string, mode os.FileMode, log LogFunc) error { + return uploadViaStdin(client, remotePath, []byte(content), mode, log) +} + func uploadBinary(client *ssh.Client, remotePath string, data []byte, log LogFunc) error { - dir := filepath.ToSlash(filepath.Dir(remotePath)) - if _, err := run(client, fmt.Sprintf("mkdir -p %q", dir), log); err != nil { - return err - } - - session, err := client.NewSession() - if err != nil { - return err - } - defer session.Close() - - stdin, err := session.StdinPipe() - if err != nil { - return err - } - - base := filepath.Base(remotePath) - go func() { - defer stdin.Close() - fmt.Fprintf(stdin, "C0755 %d %s\n", len(data), base) - _, _ = stdin.Write(data) - fmt.Fprint(stdin, "\x00") - }() - - if log != nil { - log(fmt.Sprintf("upload binary %s (%d bytes)", remotePath, len(data))) - } - if err := session.Run(fmt.Sprintf("scp -t %q", dir)); err != nil { - return fmt.Errorf("scp binary: %w", err) - } - return nil + return uploadViaStdin(client, remotePath, data, 0o755, log) } // Provision installs Docker (if needed), uploads agent binary + compose, starts the node. @@ -162,14 +160,14 @@ func Provision(n *models.Node, sshSecret string, agentBinary []byte, log LogFunc if _, err := run(client, `command -v docker >/dev/null 2>&1 || (curl -fsSL https://get.docker.com | sh)`, log); err != nil { return fmt.Errorf("install docker: %w", err) } - if _, err := run(client, `sudo mkdir -p /opt/vpn-panel-node/data && sudo chown -R $(whoami):$(whoami) /opt/vpn-panel-node || true`, log); err != nil { + if _, err := run(client, `sudo mkdir -p /opt/vpn-panel-node/data && (sudo chown -R "$(whoami):$(whoami)" /opt/vpn-panel-node || true)`, log); err != nil { return fmt.Errorf("prepare dir: %w", err) } if err := uploadBinary(client, "/opt/vpn-panel-node/vpn-node", agentBinary, log); err != nil { return err } - if err := writeRemoteFile(client, "/opt/vpn-panel-node/docker-compose.yml", ComposeYAML(n), 0644, log); err != nil { + if err := writeRemoteFile(client, "/opt/vpn-panel-node/docker-compose.yml", ComposeYAML(n), 0o644, log); err != nil { return err }