通过SSH实现HTTP支持的Golang方案

通过SSH实现HTTP支持的Golang方案 大家好,

请原谅我问这些可能很傻的问题,因为我是 Go 语言的新手。😊

我正在尝试使用 Go 的 ssh 包(https://godoc.org/golang.org/x/crypto/ssh)通过端口转发来实现 SSH 隧道。

我已经实现了一个 SSH 客户端和一个 SSH 服务器。使用私钥/公钥进行认证的 SSH 握手是成功的。 在运行 SSH 服务器的远程机器上,我需要将请求转发到一个在本地暴露且位于 Apache 服务器后面的端口。 据我所知,Apache 只接受 HTTP/HTTPS 请求。而另一方面,通过 SSH 客户端 - SSH 服务器隧道的请求是 “TCP” 类型的。

如果有人能帮助我解决这个问题,我将不胜感激!

提前感谢。

此致, Akshat


更多关于通过SSH实现HTTP支持的Golang方案的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

为什么你想通过原始TCP请求来访问HTTP服务器?为什么不直接使用HTTP协议与其通信呢?

更多关于通过SSH实现HTTP支持的Golang方案的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我想将 UltraVNC 查看器连接到 UltraVNC 中继器。由于未实现 SSL/SSH,此连接是不安全的。为此,我使用了 SSH 端口转发。UltraVNC 中继器位于 Apache 之后,因为我不想将其暴露给外部网络。

Akshat:

据我所知,Apache 只接受 http/https 请求。另一方面,通过 ssh 客户端 - sshd 隧道传输的请求是 “tcp” 类型的。

HTTP 或 HTTPS 连接就是一种 TCP 连接。因此,如果你在第 3 层进行端到端转发(即不解析协议),那么它应该可以正常工作。无论如何,ssh -L 转发就是这样工作的。

SSH隧道实现HTTP转发的Golang方案

你的问题并不傻,这是SSH隧道应用的典型场景。你需要通过SSH隧道将TCP流量转换为HTTP请求。以下是完整的解决方案:

核心方案:SSH端口转发 + HTTP代理

package main

import (
    "fmt"
    "io"
    "log"
    "net"
    "net/http"
    "time"

    "golang.org/x/crypto/ssh"
)

// SSH隧道客户端
type SSHTunnel struct {
    LocalAddr  string
    RemoteAddr string
    SSHClient  *ssh.Client
}

// 创建SSH客户端配置
func createSSHConfig(privateKeyPath string) (*ssh.ClientConfig, error) {
    key, err := ssh.ParsePrivateKey([]byte(privateKeyPath))
    if err != nil {
        return nil, err
    }

    config := &ssh.ClientConfig{
        User: "your_username",
        Auth: []ssh.AuthMethod{
            ssh.PublicKeys(key),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        Timeout:         30 * time.Second,
    }
    return config, nil
}

// 启动SSH隧道
func (tunnel *SSHTunnel) Start() error {
    listener, err := net.Listen("tcp", tunnel.LocalAddr)
    if err != nil {
        return err
    }
    defer listener.Close()

    for {
        localConn, err := listener.Accept()
        if err != nil {
            return err
        }
        go tunnel.forward(localConn)
    }
}

// 转发连接
func (tunnel *SSHTunnel) forward(localConn net.Conn) {
    remoteConn, err := tunnel.SSHClient.Dial("tcp", tunnel.RemoteAddr)
    if err != nil {
        log.Printf("远程连接失败: %v", err)
        localConn.Close()
        return
    }

    // 双向数据转发
    go copyConn(localConn, remoteConn)
    go copyConn(remoteConn, localConn)
}

func copyConn(dst, src net.Conn) {
    defer dst.Close()
    defer src.Close()
    io.Copy(dst, src)
}

// HTTP代理处理器
func handleHTTPProxy(w http.ResponseWriter, r *http.Request) {
    // 通过SSH隧道转发HTTP请求
    sshClient, err := ssh.Dial("tcp", "ssh-server:22", sshConfig)
    if err != nil {
        http.Error(w, "SSH连接失败", http.StatusBadGateway)
        return
    }
    defer sshClient.Close()

    // 通过SSH连接到目标HTTP服务
    conn, err := sshClient.Dial("tcp", "localhost:80")
    if err != nil {
        http.Error(w, "目标服务连接失败", http.StatusBadGateway)
        return
    }
    defer conn.Close()

    // 转发HTTP请求
    err = r.Write(conn)
    if err != nil {
        http.Error(w, "请求转发失败", http.StatusBadGateway)
        return
    }

    // 读取响应并返回
    resp, err := http.ReadResponse(bufio.NewReader(conn), r)
    if err != nil {
        http.Error(w, "响应读取失败", http.StatusBadGateway)
        return
    }
    defer resp.Body.Close()

    // 复制响应头
    for k, v := range resp.Header {
        w.Header()[k] = v
    }
    w.WriteHeader(resp.StatusCode)
    io.Copy(w, resp.Body)
}

// 完整示例:SSH隧道HTTP代理
func main() {
    // 1. 建立SSH连接
    sshConfig := &ssh.ClientConfig{
        User: "username",
        Auth: []ssh.AuthMethod{
            ssh.Password("password"),
            // 或使用公钥认证
            // ssh.PublicKeys(privateKey),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }

    sshClient, err := ssh.Dial("tcp", "remote-server:22", sshConfig)
    if err != nil {
        log.Fatal("SSH连接失败:", err)
    }
    defer sshClient.Close()

    // 2. 创建本地HTTP代理服务器
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 通过SSH隧道转发到远程Apache
        remoteConn, err := sshClient.Dial("tcp", "localhost:80")
        if err != nil {
            http.Error(w, "隧道连接失败", http.StatusBadGateway)
            return
        }
        defer remoteConn.Close()

        // 修改请求头以通过Apache
        r.URL.Scheme = "http"
        r.URL.Host = "localhost:80"
        r.RequestURI = ""

        // 发送请求
        err = r.Write(remoteConn)
        if err != nil {
            http.Error(w, "请求发送失败", http.StatusBadGateway)
            return
        }

        // 读取响应
        resp, err := http.ReadResponse(bufio.NewReader(remoteConn), r)
        if err != nil {
            http.Error(w, "响应读取失败", http.StatusBadGateway)
            return
        }
        defer resp.Body.Close()

        // 返回响应
        for k, v := range resp.Header {
            w.Header()[k] = v
        }
        w.WriteHeader(resp.StatusCode)
        io.Copy(w, resp.Body)
    })

    // 3. 启动本地代理服务器
    log.Println("HTTP代理服务器在 :8080 启动")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

// 替代方案:使用net/http/httputil进行反向代理
func createReverseProxy(sshClient *ssh.Client) *httputil.ReverseProxy {
    director := func(req *http.Request) {
        req.URL.Scheme = "http"
        req.URL.Host = "localhost:80"
        
        // 通过SSH隧道建立连接
        dialer := func(network, addr string) (net.Conn, error) {
            return sshClient.Dial("tcp", "localhost:80")
        }
        
        transport := &http.Transport{
            Dial: dialer,
        }
        
        httputil.NewSingleHostReverseProxy(req.URL).Transport = transport
    }
    
    return &httputil.ReverseProxy{Director: director}
}

简化版本:直接端口转发

package main

import (
    "io"
    "log"
    "net"
    
    "golang.org/x/crypto/ssh"
)

func main() {
    // SSH配置
    config := &ssh.ClientConfig{
        User: "username",
        Auth: []ssh.AuthMethod{
            ssh.Password("password"),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }
    
    // 连接SSH服务器
    client, err := ssh.Dial("tcp", "remote-server:22", config)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()
    
    // 监听本地端口
    listener, err := net.Listen("tcp", "localhost:8888")
    if err != nil {
        log.Fatal(err)
    }
    
    for {
        localConn, err := listener.Accept()
        if err != nil {
            log.Printf("接受连接失败: %v", err)
            continue
        }
        
        go func() {
            // 通过SSH连接到远程Apache
            remoteConn, err := client.Dial("tcp", "localhost:80")
            if err != nil {
                log.Printf("远程连接失败: %v", err)
                localConn.Close()
                return
            }
            
            // 双向转发
            go func() {
                defer localConn.Close()
                defer remoteConn.Close()
                io.Copy(localConn, remoteConn)
            }()
            
            go func() {
                defer localConn.Close()
                defer remoteConn.Close()
                io.Copy(remoteConn, localConn)
            }()
        }()
    }
}

使用示例

  1. 启动SSH隧道代理
go run ssh-http-proxy.go
  1. 配置浏览器或应用

    • 代理服务器:localhost:8080
    • 所有HTTP流量将通过SSH隧道转发到远程Apache
  2. 验证连接

// 测试代码
resp, err := http.Get("http://localhost:8888/path")
// 请求将通过SSH隧道转发到远程服务器的Apache

这个方案的关键点:

  1. SSH隧道处理TCP层连接
  2. HTTP代理在应用层处理HTTP协议
  3. 保持HTTP头信息完整传递
  4. 支持HTTPS需要额外处理TLS握手

你的实现方向是正确的,只需要在SSH隧道之上添加HTTP协议处理层即可。

回到顶部