下面的代码是保存为ssh2http.go , 执行如下代码
go mod init example.com/m
go mod tidy
GOOS=linux GOARCH=amd64 go build ssh2http.go
# 可以通过 go tool dist list 获取
PLATFORMS=(
"linux/amd64"
"linux/arm64"
"darwin/amd64"
"darwin/arm64"
"windows/amd64"
)
# 运行
ssh2http --ssh-host=1.2.3.4:22 \
--ssh-key-passphrase=123456 \
--ssh-key=/Users/changhui.wy/.ssh/id_ed25519 \
--local=:38080
ssh2http --ssh-host=1.2.2.3:22 \
--ssh-user=root \
--ssh-password=123456 \
--local=:38080
源码如下:
package main
import (
"bufio"
"encoding/base64"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"strings"
"sync"
"time"
"golang.org/x/crypto/ssh"
)
var (
httpProxyUser = flag.String("http-proxy-user", "", "HTTP proxy username")
httpProxyPass = flag.String("http-proxy-pass", "", "HTTP proxy password")
sshHost = flag.String("ssh-host", "", "SSH server address (e.g. 1.2.3.4:22)")
sshUser = flag.String("ssh-user", "root", "SSH username")
sshPassword = flag.String("ssh-password", "", "SSH password (optional if using private key)")
sshKeyFile = flag.String("ssh-key", "", "Path to private key file (e.g. id_rsa)")
localAddr = flag.String("local", ":8080", "Local HTTP proxy listen address (e.g. :8080)")
reconnectSec = flag.Int("reconnect-interval", 5, "Reconnect interval in seconds after failure")
sshKeyPassphrase = flag.String("ssh-key-passphrase", "", "Passphrase for encrypted private key (optional)")
)
func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0])
fmt.Fprintln(os.Stderr, "\nOptions:")
flag.PrintDefaults()
fmt.Fprintln(os.Stderr, `
Examples:
# Encrypted private key
ssh2http --ssh-host=1.2.3.4:22 --ssh-key=id_rsa --ssh-key-passphrase='mysecret' --http-proxy-user=admin --http-proxy-pass=admin --local=:8080
# Unencrypted private key
ssh2http --ssh-host=1.2.3.4:22 --ssh-user=ubuntu --ssh-key=id_rsa --local=:8080
# use for shell
export https_proxy=http://admin:admin@localhost:8080
export http_proxy=http://admin:admin@localhost:8080
Notes:
- Either --ssh-key or (--ssh-password and --ssh-user) must be provided.
- If private key is encrypted, use --ssh-key-passphrase.
`)
}
if len(os.Args) == 1 {
flag.Usage()
os.Exit(0)
}
flag.Parse()
if *sshHost == "" {
log.Fatal("Error: --ssh-host is required")
}
if !strings.Contains(*sshHost, ":") {
*sshHost = *sshHost + ":22"
}
if *sshPassword == "" && *sshKeyFile == "" {
log.Fatal("Error: either --ssh-password or --ssh-key must be provided")
}
// 启动 HTTP 代理服务器
//http代理服务器支持账目
proxy := &HTTPProxy{
sshHost: *sshHost,
sshUser: *sshUser,
sshPassword: *sshPassword,
sshKeyFile: *sshKeyFile,
reconnectSec: time.Duration(*reconnectSec) * time.Second,
}
log.Printf("Starting HTTP proxy on %s, forwarding via SSH to %s", *localAddr, *sshHost)
err := http.ListenAndServe(*localAddr, proxy)
if err != nil {
log.Fatalf("Failed to start HTTP proxy: %v", err)
}
}
// HTTPProxy 实现 http.Handler,作为 HTTP 代理
type HTTPProxy struct {
sshHost string
sshUser string
sshPassword string
sshKeyFile string
reconnectSec time.Duration
mu sync.RWMutex
client *ssh.Client
lastError error
}
// 获取当前有效的 SSH 客户端(带自动重连)
func (p *HTTPProxy) getSSHClient() (*ssh.Client, error) {
p.mu.RLock()
if p.client != nil {
// 快速检查连接是否还活着(可选)
_, _, err := p.client.SendRequest("keepalive@openssh.com", true, nil)
if err == nil {
client := p.client
p.mu.RUnlock()
return client, nil
}
// 连接已失效,关闭并重连
p.client.Close()
p.client = nil
}
p.mu.RUnlock()
// 重新连接
return p.reconnect()
}
func (p *HTTPProxy) reconnect() (*ssh.Client, error) {
p.mu.Lock()
defer p.mu.Unlock()
// 如果已有有效连接,直接返回
if p.client != nil {
return p.client, nil
}
for {
log.Printf("Connecting to SSH server: %s", p.sshHost)
client, err := p.dialSSH()
if err == nil {
p.client = client
p.lastError = nil
log.Println("SSH connection established")
go p.keepAlive(client)
return client, nil
}
p.lastError = err
log.Printf("Failed to connect SSH: %v, retrying in %v...", err, p.reconnectSec)
time.Sleep(p.reconnectSec)
}
}
func (p *HTTPProxy) dialSSH() (*ssh.Client, error) {
var auth []ssh.AuthMethod
if p.sshPassword != "" {
auth = append(auth, ssh.Password(p.sshPassword))
}
if p.sshKeyFile != "" {
keyBytes, err := os.ReadFile(p.sshKeyFile)
if err != nil {
return nil, fmt.Errorf("read private key file: %w", err)
}
var signer ssh.Signer
var parseErr error
// 如果提供了 passphrase,尝试用它解密
if *sshKeyPassphrase != "" {
signer, parseErr = ssh.ParsePrivateKeyWithPassphrase(keyBytes, []byte(*sshKeyPassphrase))
if parseErr == nil {
auth = append(auth, ssh.PublicKeys(signer))
} else {
// 解密失败,报错
return nil, fmt.Errorf("failed to parse encrypted private key with given passphrase: %w", parseErr)
}
} else {
// 没有提供 passphrase,尝试无密码解析
signer, parseErr = ssh.ParsePrivateKey(keyBytes)
if parseErr == nil {
auth = append(auth, ssh.PublicKeys(signer))
} else {
// 可能是加密私钥但没给密码
return nil, fmt.Errorf("failed to parse private key (it may be encrypted; try --ssh-key-passphrase): %w", parseErr)
}
}
}
config := &ssh.ClientConfig{
User: p.sshUser,
Auth: auth,
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 生产环境建议验证 host key
Timeout: 10 * time.Second,
}
return ssh.Dial("tcp", p.sshHost, config)
}
// 保活协程
func (p *HTTPProxy) keepAlive(client *ssh.Client) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// 发送 keepalive 请求
_, _, err := client.SendRequest("keepalive@openssh.com", true, nil)
if err != nil {
log.Printf("SSH keepalive failed: %v", err)
client.Close()
return
}
}
}
}
func (p *HTTPProxy) checkAuth(r *http.Request) bool {
auth := r.Header.Get("Proxy-Authorization")
if auth == "" {
return false
}
// 格式应为: "Basic base64(user:pass)"
if !strings.HasPrefix(auth, "Basic ") {
return false
}
payload, err := base64.StdEncoding.DecodeString(auth[6:])
if err != nil {
return false
}
creds := string(payload)
parts := strings.SplitN(creds, ":", 2)
if len(parts) != 2 {
return false
}
username := parts[0]
password := parts[1]
// 比较(注意:避免时序攻击,但简单场景可直接比较)
return username == *httpProxyUser && password == *httpProxyPass
}
// ServeHTTP 实现 HTTP 代理逻辑
func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if *httpProxyUser != "" || *httpProxyPass != "" {
if !p.checkAuth(r) {
w.Header().Set("Proxy-Authenticate", "Basic realm=\"Proxy\"")
http.Error(w, "Proxy authentication required", http.StatusProxyAuthRequired)
return
}
}
clientIP, _, _ := net.SplitHostPort(r.RemoteAddr)
log.Printf("[HTTP] Client %s -> Target %s %s %s", clientIP, r.URL.Host, r.Method, r.URL.Path)
if r.Method == http.MethodConnect {
p.handleConnect(w, r)
} else {
p.handleHTTP(w, r)
}
}
func (p *HTTPProxy) handleConnect(w http.ResponseWriter, r *http.Request) {
// HTTPS 代理:建立隧道
dest := r.Host
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "Hijack not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
log.Printf("Hijack error: %v", err)
return
}
defer clientConn.Close()
// 发送 200 Connection Established
clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
// 获取 SSH 客户端
sshClient, err := p.getSSHClient()
if err != nil {
log.Printf("Failed to get SSH client for CONNECT: %v", err)
return
}
// 通过 SSH 打开到目标的 TCP 连接
targetConn, err := sshClient.Dial("tcp", dest)
if err != nil {
log.Printf("Failed to dial target %s via SSH: %v", dest, err)
return
}
defer targetConn.Close()
// 双向复制
go io.Copy(targetConn, clientConn)
io.Copy(clientConn, targetConn)
}
func (p *HTTPProxy) handleHTTP(w http.ResponseWriter, r *http.Request) {
// 构造目标 URL
if !strings.HasPrefix(r.URL.String(), "http") {
http.Error(w, "URL must be absolute", http.StatusBadRequest)
return
}
// 获取 SSH 客户端
sshClient, err := p.getSSHClient()
if err != nil {
http.Error(w, "SSH unavailable", http.StatusServiceUnavailable)
return
}
// 通过 SSH 打开到目标主机的连接
targetConn, err := sshClient.Dial("tcp", r.URL.Host)
if err != nil {
log.Printf("Failed to dial %s: %v", r.URL.Host, err)
http.Error(w, "Gateway error", http.StatusBadGateway)
return
}
defer targetConn.Close()
// 转发原始请求
err = r.Write(targetConn)
if err != nil {
log.Printf("Failed to write request: %v", err)
http.Error(w, "Write error", http.StatusBadGateway)
return
}
// 读取响应
resp, err := http.ReadResponse(bufio.NewReader(targetConn), r)
if err != nil {
log.Printf("Failed to read response: %v", err)
http.Error(w, "Read error", http.StatusBadGateway)
return
}
defer resp.Body.Close()
// 写回响应头
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
w.WriteHeader(resp.StatusCode)
// 写回响应体
io.Copy(w, resp.Body)
}