英文:
Go server that can conditionally forward or terminate incoming TLS connections
问题
我的总体目标是:我想编写一个Go服务器,接受传入的TLS连接,并通过TLS SNI扩展检查客户端指示的服务器名称。根据服务器名称,我的服务器将执行以下操作之一:
- 将TCP连接转发(反向代理)到另一台服务器,而不终止TLS,或者
- 终止TLS并自己处理请求
这篇优秀的博文描述了一个检查SNI扩展并将连接转发到其他位置或终止连接的反向代理。基本的技巧是从TCP连接中窥探足够的字节来解析TLS ClientHello,如果应该转发服务器名称,反向代理会打开到最终目标的TCP连接,将窥探的字节写入连接,然后设置goroutine来在客户端的TCP连接和最终目标的连接之间复制其余的字节,直到关闭。按照该文章中的模型,我能够以很少的更改实现行为1。
问题出在另一种情况,即行为2,当我的服务器应该终止TLS并自己处理应用层的HTTP请求时。我正在使用Go标准库的HTTP服务器,但其API没有我需要的功能。具体来说,在我窥探了ClientHello并确定连接应该由我的服务器处理之后,没有办法将net.Conn
传递给现有的http.Server
。我需要类似以下的API:
// 实际上不存在
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循环。
目前,我能想到的唯一方法是创建一个由Go通道支持的“合成”net.Listener
,将其传递给func (srv *http.Server) ServeTLS
。然后,当我从真实的TCP net.Listener
接收到服务器应该自己处理的连接时,我将连接发送到合成侦听器,这会导致该侦听器的Accept
返回新的连接给等待的http.Server
。然而,这种解决方案并不完美,我正在寻找一种更清晰地实现我的总体目标的方法。
以下是我尝试做的简化版本。TODO
标记了我不知道如何继续的部分。
func main() {
l, _ := net.Listen("tcp", ":443")
// 用于处理应该直接处理的请求的服务器
server := http.Server{
// 省略配置以简洁起见
}
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) {
// 终止TLS并自己处理
// TODO: 如何使用`server`来处理`clientConn`?
return
}
// 否则,转发到另一台服务器而不终止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()
}
// 如果我们应该处理此连接,则返回true;如果应该转发,则返回false
func shouldHandleServerName(serverName string) bool {
// 省略实现以简洁起见
}
// 从读取器中读取字节,直到可以解析TLS ClientHello。返回解析的ClientHello和一个新的io.Reader,其中包含原始读取器中的所有字节,包括组成ClientHello的字节,以便可以透明地转发连接。
func peekClientHello(reader io.Reader) (*tls.ClientHelloInfo, io.Reader, error) {
// 省略实现以简洁起见,与https://www.agwa.name/blog/post/writing_an_sni_proxy_in_go大致相同
}
英文:
My overall goal is the following: I would like to write a Go server that accepts incoming TLS connections and examines the server name indicated by the client via the TLS SNI extension. Depending on the server name, my server will either:
- forward (reverse-proxy) the TCP connection to a different server, without terminating TLS, or
- terminate TLS and handles the request itself
This excellent blog post describes a reverse proxy that examines the SNI extension and either forwards the connection elsewhere or else terminates it. The basic trick is to peek enough bytes from the TCP connection to parse the TLS ClientHello, and if the server name should be forwarded, the reverse proxy opens a TCP connection to the final destination, writes the peeked bytes to the connection, then sets up goroutines to copy the rest of the bytes until close between the TCP connection from the client and the connection to the final destination. Following the model in that post, I'm able to implement behavior 1 with few changes.
The problem is with the other case, behavior 2, when my server should terminate TLS and handle the application-layer HTTP request itself. I'm using the Go standard library's HTTP server, but its APIs don't have what I need. Specifically, after I've peeked the ClientHello and determined the connection should be handled by my server, there's no way to pass the net.Conn
to an existing http.Server
. I need an API something like:
// Does not actually exist
func (srv *http.Server) HandleConnection(c net.Conn) error
but the closest I can get is
func (srv *http.Server) Serve(l net.Listener) error
or the TLS equivalent,
func (srv *http.Server) ServeTLS(l net.Listener, certFile, keyFile string) error
both of which accept net.Listener
, and do their own for-accept loop internally.
Right now, the only way forward I can think of is to create my own "synthetic" net.Listener
backed by a Go channel, which I pass to func (srv *http.Server) ServeTLS
. Then, when I receive a connection from the real TCP net.Listener
that the server should handle itself, I send the connection to the synthetic listener, which causes that listener's Accept
to return the new connection to the waiting http.Server
. This solution doesn't feel great, though, and I'm looking for something that will achieve my overall goal more cleanly.
Here's a simplified version of what I'm trying to do. The TODO
marks the part where I don't know how to proceed.
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
}
答案1
得分: 1
最干净的解决方案可能是你已经建议的通过实现自定义的net.Listener
来实现。
我会修改peekClientHello
函数,使其返回一个net.Conn
,实际上只是一个包装了现有net.Conn
和io.TeeReader
的对象,就像现有函数已经使用的那样。现在我们有了一个新的对象,可以复制到后端或由Accept
函数返回。现在你可以层叠一个net.Listener
、CustomListener
和tls.Listener
。
你最终会得到这样的代码:
func main() {
// 用于处理直接处理的请求的服务器
server := http.Server{
// 省略配置
}
tcpListener, _ := net.Listen("tcp", ":443")
l := tls.NewListener(
&CustomListener{
InnerListener: tcpListener,
},
nil, // 一些自定义的tls配置
)
server.Serve(l)
}
type CustomListener struct {
InnerListener net.Listener
// TODO 添加用于shouldHandleServerName的设置
}
// Accept 等待并返回监听器的下一个连接。
func (cl *CustomListener) Accept() (net.Conn, error) {
for {
clientConn, err := cl.InnerListener.Accept()
if err != nil {
return nil, err
}
clientHello, teeConn, _ := peekClientHello(clientConn)
// 终止TLS并自己处理
if !cl.shouldHandleServerName(clientHello.ServerName) {
return teeConn, err
}
go forwardConnection(clientHello.ServerName, teeConn)
}
}
func forwardConnection(serverName string, clientConn net.Conn) {
defer clientConn.Close()
// 否则,转发到另一个服务器而不终止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 关闭监听器。
// 任何被阻塞的Accept操作将被解除阻塞并返回错误。
func (cl *CustomListener) Close() error {
return cl.InnerListener.Close()
}
// Addr 返回监听器的网络地址。
func (cl *CustomListener) Addr() net.Addr {
return cl.InnerListener.Addr()
}
// 如果我们应该处理此连接,则返回true,如果应该转发,则返回false
func (cl *CustomListener) shouldHandleServerName(serverName string) bool {
// 省略实现以保持简洁
}
// 从读取器中读取字节,直到可以解析TLS ClientHello。返回解析的ClientHello和一个新的net.Conn,其中包含原始读取器中的所有字节,包括组成ClientHello的字节,以便可以透明地转发连接。
func peekClientHello(reader io.Reader) (*tls.ClientHelloInfo, net.Conn, error) {
// 省略实现以保持简洁,大部分与 https://www.agwa.name/blog/post/writing_an_sni_proxy_in_go 相同
}
希望对你有帮助!
英文:
The cleanest solution is likely the way you already suggested by implementing a custom net.Listener
.
I would modify peekClientHello
function to return a net.Conn
which in actuality is just a wrapper around an existing net.Conn
and a io.TeeReader
like the existing function already uses. Now we have a new object which can be copied to the the backend or returned by the Accept
function. You can now layer a net.Listener
, CustomListener
, and tls.Listener
.
You would end up with something like this:
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
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论