goph

melbahjaMIT活跃维护

用 Go 写的原生 SSH 客户端库,能在代码里连远程主机执行命令、传文件,省去自己封装 SSH 协议的麻烦。

🤘 The native golang ssh client to execute your commands over ssh connection. 🚀🚀

v2.0.1发布于 2026-07-15 18:40 UTC2,036star141fork同步于 2026-11-07 07:34 UTC

怎么上手

由 README 提炼,执行前请确认
go get github.com/melbahja/goph/v2
硬件要求按 README 说明

README 未说明

下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

轻量 Go SSH 客户端库,用简单 API 在代码里远程执行命令和传文件

用 Go 写的原生 SSH 客户端库,能在代码里连远程主机执行命令、传文件,省去自己封装 SSH 协议的麻烦。

核心能力

  • 用 goph.New 加选项函数一行建立 SSH 连接
  • 默认校验 known_hosts,支持密码、私钥、passphrase 和 ssh agent
  • 支持 SOCKS5 代理与跳板机(jump host)多级连接
  • 通过 SFTP 上传下载文件并做 Open/Create/Chmod 等操作
  • 支持 context.Context 取消远程命令,可自定义取消信号

容易踩的坑

默认开启 known_hosts 校验,连新主机会因主机密钥未知而失败,需先加入 known_hosts 或显式关闭校验;导入路径带 /v2,写成不带版本号的旧路径会拉不到对应版本;用私钥认证时若私钥有 passphrase,必须作为第二个参数传给 WithKeyFile,否则连接失败

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

2 个版本v2.0.1
  1. v2.0.1v2最新正式版2026-07-15 · 2026-07-15 18:40 UTC

    Goph v2

    Goph v2 brings Functional options API, context-aware commands, SOCKS5 proxy, jump hosts, lazy agent sockets, and many more features.

    Breaking Changes

    • Removed goph.Auth type and goph.Config struct.

      • Replaced by functional options passed directly to goph.New.
    • Removed constructors: NewUnknown, NewConn.

      • Use goph.New(user, addr, opts...) with WithInsecureIgnoreHostKey() where needed.
    • Removed auth constructor functions: goph.Password(), goph.Key(), goph.UseAgent().

      • Replaced by WithPassword, WithKeyFile, WithDefaultAgent, WithAgentSocket etc.
    • Client.Config field removed.

      • *ssh.ClientConfig is now ephemeral: built in New, passed to options and dial helpers, then discarded. It is no longer stored on the connected Client.
    • Renamed key auth options:

      • WithKey(file, passphrase) => WithKeyFile(file, passphrase)
      • WithRawKey(pem, passphrase) => WithKey(pem, passphrase)
    • Renamed key parsing helpers:

      • GetSigner(file, passphrase) => ParseKeyFile(file, passphrase)
      • GetSignerForRawKey(key, passphrase) => ParseKey(key, passphrase)

    New Features

    • Functional options API.

      • WithPassword, WithKeyFile, WithKey, WithAgent, WithAgentSocket, WithDefaultAgent, WithSigner, WithAuth
      • WithPort, WithTimeout, WithKnownHosts, WithInsecureIgnoreHostKey, WithHostKeyCallback, WithBannerCallback, WithConfig
      • WithProxy, WithJump
    • Dialer for reusable connections.

      • d := goph.NewDialer(opts...) then d.New(user, addr) for multiple hosts with shared defaults.
    • SOCKS5 proxy authentication.

      • WithProxy can accepts URLs with embedded credentials: socks5://user:pass@host:port.
    • Context-aware command cancellation.

      • RunContext and CommandContext accept context.Context.
      • Cancellation sends ssh.SIGINT to the remote session.
    • goph.Dial.

      • goph.Dial(c *Client, config *ssh.ClientConfig) error for manual Client construction and custom ssh.ClientConfig.
      • Client now stores User; Dial defaults config.User to Client.User or the current OS user when empty.
      • Direct TCP dials now respect config.Timeout.
    • Security fixes.

      • WithPassword no longer includes a keyboard-interactive fallback. Added standalone WithKeyboardInteractive(handler) that gives callers full control over each server prompt (password, OTP, 2FA, etc.).
      • Cmd.String warning now clearly states that Path and Args are not shell-escaped and that callers must sanitize untrusted input.
    • Known-hosts helpers.

      • KnownHosts now documents that the file must exist.
      • New EnsureKnownHosts creates the file (and parent directory) if missing.
      • DefaultKnownHosts uses EnsureKnownHosts so first-time users no longer hit a file-read error when ~/.ssh/known_hosts is absent.

    Pull Request by @melbahja in https://github.com/melbahja/goph/pull/78 Full Changelog https://github.com/melbahja/goph/compare/v1.5.0...v2.0.1

  2. v1.5.22026-07-15 · 2026-07-15 18:03 UTC
    查看更新说明
    • Security update for crypto pkg for goph v1

    Full Changelog: https://github.com/melbahja/goph/compare/v1.5.1...v1.5.2

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    🤘  Features

    • Easy to use and simple API
    • Supports known hosts by default.
    • Supports connections with passwords.
    • Supports connections with private keys.
    • Supports connections with protected private keys with passphrase.
    • Supports multiple auth methods in a single connection.
    • Supports upload files from local to remote.
    • Supports download files from remote to local.
    • Supports connections with ssh agent.
    • Supports connections with custom signers.
    • Supports adding new hosts to known_hosts file.
    • Supports host key callback check from default known_hosts file.
    • Supports file operations like: Open, Create, Chmod... via SFTP.
    • Supports context.Context for command cancellation.
    • Supports SOCKS5 proxy for connecting through intermediaries.
    • Supports proxy jump for connecting through jump hosts.

    🚀  Installation

    go get github.com/melbahja/goph/v2
    

    📄  Get Started

    Run a command via ssh can be simple as this example:

    package main
    
    import (
    	"log"
    	"fmt"
    	"github.com/melbahja/goph/v2"
    )
    
    func main() {
    
    	// Start new ssh connection with private key.
    	client, err := goph.New("root", "192.1.1.3", goph.WithPassword("try_password_first"))
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Defer closing the network connection.
    	defer client.Close()
    
    	// Execute your command.
    	out, err := client.Run("ls /tmp/")
    
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Get your output as []byte.
    	fmt.Println(string(out))
    }
    

    Docs are available in Go Docs.

    📚  Usage Examples

    Expand each group then expand individual examples.

    🔐  Authentication Examples

    client, err := goph.New("root", "192.1.1.3",
    	goph.WithPassword("your_password"),
    )
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
    )
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
    )
    
    privateKey, _ := os.ReadFile("/home/user/.ssh/id_rsa")
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithKey(privateKey, "passphrase"),
    )
    
    if goph.HasAgent() {
    	client, err := goph.New("root", "192.1.1.3",
    		goph.WithDefaultAgent(),
    	)
    }
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithAgentSocket("/run/user/1000/keyring/ssh"),
    )
    
    conn, err := net.Dial("unix", "/custom/agent.sock")
    if err != nil {
    	log.Fatal(err)
    }
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithAgent(conn),
    )
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithKeyboardInteractive(func(user, instruction, question string, echo bool) (string, error) {
    		if strings.Contains(strings.ToLower(question), "password") {
    			return "your_password", nil
    		}
    		return "", fmt.Errorf("unexpected prompt: %s", question)
    	}),
    )
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithKeyboardInteractive(func(user, instruction, question string, echo bool) (string, error) {
    		if strings.Contains(strings.ToLower(question), "otp") ||
    			strings.Contains(strings.ToLower(question), "verification code") {
    			fmt.Print(question, " ")
    			var code string
    			fmt.Scanln(&code)
    			return code, nil
    		}
    		return "", fmt.Errorf("unexpected prompt: %s", question)
    	}),
    )
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithPassword("try_password_first"),
    	goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
    	goph.WithDefaultAgent(),
    )
    
    signer, err := ssh.NewSignerFromKey(myPrivateKey) // or from any source
    if err != nil {
    	log.Fatal(err)
    }
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithSigner(signer),
    )
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithAuth(ssh.Password("pass")),
    	goph.WithAuth(ssh.PublicKeysCallback(myCustomCallback)),
    )
    

    📡  Features Examples

    client, err := goph.New("root", "192.1.1.3",
    	goph.WithPassword("pass"),
    	goph.WithPort(2222),
    	goph.WithTimeout(10*time.Second),
    )
    
    // Known hosts verification is ON by default via ~/.ssh/known_hosts
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithPassword("pass"),
    )
    
    // Or specify a custom known_hosts file:
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithPassword("pass"),
    	goph.WithKnownHosts("/path/to/known_hosts"),
    )
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithPassword("pass"),
    	goph.WithInsecureIgnoreHostKey(),
    )
    
    hostKeyCallback := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
    	// Check the key against a database or prompt the user
    	return nil
    }
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithPassword("pass"),
    	goph.WithHostKeyCallback(hostKeyCallback),
    )
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithPassword("pass"),
    	goph.WithBannerCallback(func(message string) error {
    		log.Printf("SSH Banner: %s", message)
    		return nil
    	}),
    )
    
    client, err := goph.New("root", "target-host",
    	goph.WithPassword("pass"),
    	goph.WithProxy("socks5://127.0.0.1:1080"),
    )
    
    // With proxy authentication:
    client, err = goph.New("root", "target-host",
    	goph.WithPassword("pass"),
    	goph.WithProxy("socks5://proxyuser:[email protected]:1080"),
    )
    
    // First connect to the jump host
    jump, err := goph.New("jumpuser", "bastion.example.com",
    	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
    	goph.WithPort(2222),
    )
    if err != nil {
    	log.Fatal(err)
    }
    defer jump.Close()
    
    // Then tunnel through it to the target
    client, err := goph.New("root", "internal-host",
    	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
    	goph.WithJump(jump),
    )
    
    // Put the proxy on the JUMP client, then pass the jump to the target
    jump, err := goph.New("jumpuser", "bastion.example.com",
    	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
    	goph.WithProxy("socks5://127.0.0.1:1080"),
    )
    if err != nil {
    	log.Fatal(err)
    }
    defer jump.Close()
    
    client, err := goph.New("root", "internal-host",
    	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
    	goph.WithJump(jump),
    )
    
    out, err := client.Run("ls /tmp/")
    if err != nil {
    	log.Fatal(err)
    }
    fmt.Println(string(out))
    
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    
    // Sends SIGINT and returns error after 1 second
    out, err := client.RunContext(ctx, "sleep 5")
    
    ctx, cancel := context.WithCancel(context.Background())
    cmd, err := client.CommandContext(ctx, "sleep", "60")
    if err != nil {
    	log.Fatal(err)
    }
    
    // Cancel from another goroutine
    go func() {
    	time.Sleep(5 * time.Second)
    	cancel()
    }()
    
    if err := cmd.Run(); err != nil {
    	// context canceled
    	fmt.Println("Command stopped:", err)
    }
    
    cmd, err := client.CommandContext(ctx, "sleep", "30")
    if err != nil {
    	log.Fatal(err)
    }
    
    // Send SIGKILL instead of the default SIGINT on cancellation
    cmd.Cancel = func() error {
    	return cmd.Signal(ssh.SIGKILL)
    }
    
    if err := cmd.Run(); err != nil {
    	log.Fatal(err)
    }
    
    cmd, err := client.Command("ls", "-alh", "/tmp")
    if err != nil {
    	log.Fatal(err)
    }
    
    // Set env vars (server must accept them)
    cmd.Env = []string{"MY_VAR=MYVALUE"}
    
    // Run (CombinedOutput, Output, Start, Wait also available)
    err = cmd.Run()
    
    // With context:
    cmd, err = client.CommandContext(ctx, "ls", "-alh", "/tmp")
    
    err := client.Upload("/path/to/local/file", "/path/to/remote/file")
    
    err := client.Download("/path/to/remote/file", "/path/to/local/file")
    
    sftp, err := client.NewSftp()
    if err != nil {
    	log.Fatal(err)
    }
    defer sftp.Close()
    
    // Create a file
    file, err := sftp.Create("/tmp/remote_file")
    if err != nil {
    	log.Fatal(err)
    }
    defer file.Close()
    
    file.Write([]byte("Hello world"))
    
    // Open existing file
    f, _ := sftp.Open("/etc/hostname")
    // ... read from f
    
    // Other methods: MkdirAll, Remove, Rename, ReadDir, Stat, etc.
    
    // Execute a script from any io.Reader (e.g. a string, a file handle, an HTTP body)
    cmd, err := client.Script(ctx, strings.NewReader("echo hello"))
    if err != nil {
    	log.Fatal(err)
    }
    out, err := cmd.CombinedOutput()
    
    // Read a local script file into memory and execute it on the remote host
    cmd, err := client.ScriptFile(ctx, "/path/to/local/script.sh")
    if err != nil {
    	log.Fatal(err)
    }
    
    // Override the interpreter for a PHP script:
    cmd, err = client.ScriptFile(ctx, "/path/to/script.php", goph.WithPath("/usr/bin/php"))
    
    // Override the shell path (default: /bin/sh)
    cmd, err = client.ScriptFile(ctx, "/path/to/local/script.sh", goph.WithPath("/bin/zsh"))
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithPassword("pass"),
    	goph.WithConfig(func(config *ssh.ClientConfig) error {
    		config.Timeout = 30 * time.Second
    		config.ClientVersion = "SSH-2.0-MyApp"
    		return nil
    	}),
    )
    
    c := &goph.Client{
    	User: "root",
    	Addr: "192.1.1.3",
    	Port: 22,
    }
    
    config := &ssh.ClientConfig{
    	Auth:            []ssh.AuthMethod{ssh.Password("pass")},
    	HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }
    
    if err := goph.Dial(c, config); err != nil {
    	log.Fatal(err)
    }
    defer c.Close()
    
    d := goph.NewDialer(
    	goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
    	goph.WithPort(2222),
    )
    
    client1, _ := d.New("root", "host1")
    client2, _ := d.New("root", "host2", goph.WithPassword("fallback"))
    

    🛠️  Utils and Helpers

    found, err := goph.CheckKnownHost("myhost", remoteAddr, publicKey, "")
    if err != nil {
    	// key mismatch! possible MITM attack!
    	log.Fatal(err)
    }
    if !found {
    	// host is new, ask user to trust it
    }
    
    err := goph.AddKnownHost("myhost", remoteAddr, publicKey, "")
    
    signer, err := goph.ParseKeyFile("/home/user/.ssh/id_rsa", "passphrase")
    if err != nil {
    	log.Fatal(err)
    }
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithSigner(signer),
    )
    
    keyBytes, _ := os.ReadFile("/home/user/.ssh/id_rsa")
    signer, err := goph.ParseKey(keyBytes, "passphrase")
    if err != nil {
    	log.Fatal(err)
    }
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithSigner(signer),
    )
    
    if goph.HasAgent() {
    	fmt.Println("SSH agent is available at", os.Getenv("SSH_AUTH_SOCK"))
    } else {
    	fmt.Println("SSH agent not running or SSH_AUTH_SOCK not set")
    }
    
    path, err := goph.DefaultKnownHostsPath()
    if err != nil {
    	log.Fatal(err)
    }
    fmt.Println("Known hosts file:", path)
    
    // EnsureKnownHosts returns a callback and creates the file (and ~/.ssh/)
    // if it does not exist yet — useful when writing first-time tools.
    cb, err := goph.EnsureKnownHosts(filepath.Join(os.Getenv("HOME"), ".ssh", "my_hosts"))
    if err != nil {
    	log.Fatal(err)
    }
    
    client, err := goph.New("root", "192.1.1.3",
    	goph.WithPassword("pass"),
    	goph.WithHostKeyCallback(cb),
    )
    

    🛡️  Security Notes

    • Known-hosts verification is enabled by default. Do not use goph.WithInsecureIgnoreHostKey() in production; it makes you vulnerable to MITM attacks.
    • A missing ~/.ssh/known_hosts file is created automatically as an empty file, but unknown host keys are still rejected until you explicitly trust them.
    • Command arguments are not shell-escaped. Cmd.String() returns raw Path and Args. Never pass untrusted input directly into commands; sanitize or use fixed argument lists.
    • Prompt the user before calling goph.AddKnownHost. A mismatched key from goph.CheckKnownHost should be treated as a potential MITM attack.

    ❓  FAQ

    Expand each question to see the answer.

    SSH exec requests do not support setting a working directory in the protocol. The server runs the command in the user's default shell context (typically their home directory). To run a command in a specific directory, prefix it:

    out, err := client.Run("cd /var/log && ls -la")
    

    Either connect as root, or use sudo -S and feed the password through stdin. Be aware that sudo may require a TTY, which goph does not provide by default:

    cmd, err := client.Command("sudo", "-S", "systemctl", "restart", "nginx")
    if err != nil {
    	log.Fatal(err)
    }
    
    cmd.Stdin = strings.NewReader("your_sudo_password\n")
    out, err := cmd.CombinedOutput()
    

    Yes. goph is pure Go and works on Windows, Linux, and macOS. For Windows-specific key providers (CNG/CAPI, Pageant, named-pipe OpenSSH agent), build or obtain an ssh.Signer and use WithSigner:

    signer, err := ssh.NewSignerFromKey(myPrivateKey)
    client, err := goph.New("user", "host", goph.WithSigner(signer))
    

    🤝  Missing a Feature?

    Feel free to open a new issue, or contact me.

    📘  License

    Goph is provided under the MIT License.