【问题标题】:Go server that can conditionally forward or terminate incoming TLS connections可以有条件地转发或终止传入 TLS 连接的 Go 服务器
【发布时间】:2021-12-17 10:56:09
【问题描述】:

我的总体目标如下:我想编写一个接受传入 TLS 连接并检查客户端通过 TLS SNI extension 指示的服务器名称的 Go 服务器。根据服务器名称,我的服务器将:

  1. 将 TCP 连接转发(反向代理)到不同的服务器,而不终止 TLS,或者
  2. 终止 TLS 并自行处理请求

This excellent blog post 描述了一个反向代理,它检查 SNI 扩展并将连接转发到其他地方或终止它。基本技巧是从 TCP 连接中窥视足够的字节来解析 TLS ClientHello,如果应该转发服务器名称,反向代理会打开到最终目的地的 TCP 连接,将窥视的字节写入连接,然后设置goroutines 复制其余字节,直到在来自客户端的 TCP 连接和到最终目的地的连接之间关闭。按照那篇文章中的模型,我只需进行少量更改即可实现行为 1。

问题在于 other 情况,即行为 2,我的服务器应该终止 TLS 并自行处理应用层 HTTP 请求。我正在使用 Go 标准库的 HTTP 服务器,但它的 API 没有我需要的东西。具体来说,在我查看了 ClientHello 并确定连接应该由我的服务器处理后,无法将 net.Conn 传递给现有的 http.Server。我需要一个类似的 API:

// Does not actually exist
func (srv *http.Server) HandleConnection(c net.Conn) error

但我能得到的最接近的是

func (srv *http.Server) Serve(l net.Listener) error

或等效的 TLS,

func (srv *http.Server) ServeTLS(l net.Listener, certFile, keyFile string) error

两者都接受net.Listener,并在内部执行自己的for-accept loop。

目前,我能想到的唯一方法是创建我自己的“合成”net.Listener,由 Go 频道支持,我将其传递给 func (srv *http.Server) ServeTLS。然后,当我从真正的 TCP net.Listener 接收到服务器应自行处理的连接时,我将连接发送到合成侦听器,这导致该侦听器的Accept 将新连接返回到等待的http.Server。不过,这个解决方案感觉不太好,我正在寻找能够更干净地实现我的总体目标的解决方案。


这是我正在尝试做的简化版本。 TODO 标记了我不知道如何进行的部分。

func main() {
    l, _ := net.Listen("tcp", ":443")
    // Server to handle request that should be handled directly
    server := http.Server{
        // Config omitted for brevity
    }
    for {
        conn, err := l.Accept()
        if err != nil {
            continue
        }
        go handleConnection(conn, &server)
    }
}

func handleConnection(clientConn net.Conn, server *http.Server) {
    defer clientConn.Close()

    clientHello, clientReader, _ := peekClientHello(clientConn)

    if shouldHandleServerName(clientHello.ServerName) {
        // Terminate TLS and handle it ourselves
        // TODO: How to use `server` to handle `clientConn`?
        return
    }

    // Else, forward to another server without terminating TLS
    backendConn, _ := net.DialTimeout("tcp", net.JoinHostPort(clientHello.ServerName, "443"), 5*time.Second)
    defer backendConn.Close()

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        io.Copy(clientConn, backendConn)
        clientConn.(*net.TCPConn).CloseWrite()
        wg.Done()
    }()
    go func() {
        io.Copy(backendConn, clientReader)
        backendConn.(*net.TCPConn).CloseWrite()
        wg.Done()
    }()

    wg.Wait()
}

// Returns true if we should handle this connection, and false if we should forward
func shouldHandleServerName(serverName string) bool {
    // Implementation omitted for brevity
}

// Reads bytes from reader until it can parse a TLS ClientHello. Returns the
// parsed ClientHello and a new io.Reader that contains all the bytes from the
// original reader, including those that made up the ClientHello, so that the
// connection can be transparently forwarded.
func peekClientHello(reader io.Reader) (*tls.ClientHelloInfo, io.Reader, error) {
    // Implementation omitted for brevity, mostly identical to
    // https://www.agwa.name/blog/post/writing_an_sni_proxy_in_go
}

【问题讨论】:

    标签: http go ssl


    【解决方案1】:

    最干净的解决方案可能是您通过实施自定义net.Listener 建议的方式。

    我会修改peekClientHello 函数以返回一个net.Conn,它实际上只是现有net.Conn 和io.TeeReader 的包装,就像现有函数已经使用的一样。现在我们有了一个新对象,它可以复制到后端或由Accept 函数返回。您现在可以将net.Listener、CustomListener 和tls.Listener 分层。

    你最终会得到这样的结果:

    func main() {
        // Server to handle request that should be handled directly
        server := http.Server{
            // Config omitted for brevity
        }
    
        tcpListener, _ := net.Listen("tcp", ":443")
        l := tls.NewListener(
            &CustomListener{
                InnerListener: tcpListener,
            },
            nil, // some custom tls config
        )
    
        server.Serve(l)
    }
    
    type CustomListener struct {
        InnerListener net.Listener
        // TODO add settings to be used by shouldHandleServerName
    }
    
    // Accept waits for and returns the next connection to the listener.
    func (cl *CustomListener) Accept() (net.Conn, error) {
        for {
            clientConn, err := cl.InnerListener.Accept()
            if err != nil {
                return nil, err
            }
    
            clientHello, teeConn, _ := peekClientHello(clientConn)
    
            // Terminate TLS and handle it ourselves
            if !cl.shouldHandleServerName(clientHello.ServerName) {
                return teeConn, err
            }
    
            go forwardConnection(clientHello.ServerName, teeConn)
        }
    }
    
    func forwardConnection(serverName string, clientConn net.Conn) {
        defer clientConn.Close()
        // Else, forward to another server without terminating TLS
        backendConn, _ := net.DialTimeout("tcp", net.JoinHostPort(serverName, "443"), 5*time.Second)
        defer backendConn.Close()
    
        var wg sync.WaitGroup
        wg.Add(2)
    
        go func() {
            io.Copy(clientConn, backendConn)
            clientConn.(*net.TCPConn).CloseWrite()
            wg.Done()
        }()
        go func() {
            io.Copy(backendConn, clientConn)
            backendConn.(*net.TCPConn).CloseWrite()
            wg.Done()
        }()
    
        wg.Wait()
    }
    
    // Close closes the listener.
    // Any blocked Accept operations will be unblocked and return errors.
    func (cl *CustomListener) Close() error {
        return cl.InnerListener.Close()
    }
    
    // Addr returns the listener's network address.
    func (cl *CustomListener) Addr() net.Addr {
        return cl.InnerListener.Addr()
    }
    
    // Returns true if we should handle this connection, and false if we should forward
    func (cl *CustomListener) shouldHandleServerName(serverName string) bool {
        // Implementation omitted for brevity
    }
    
    // Reads bytes from reader until it can parse a TLS ClientHello. Returns the
    // parsed ClientHello and a new net.Conn that contains all the bytes from the
    // original reader, including those that made up the ClientHello, so that the
    // connection can be transparently forwarded.
    func peekClientHello(reader io.Reader) (*tls.ClientHelloInfo, net.Conn, error) {
        // Implementation omitted for brevity, mostly identical to
        // https://www.agwa.name/blog/post/writing_an_sni_proxy_in_go
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-23
      • 1970-01-01
      • 2017-11-08
      • 1970-01-01
      • 1970-01-01
      • 2010-10-04
      相关资源
      最近更新 更多