可以有条件地转发或终止传入 TLS 连接的 Go 服务器
Go server that can conditionally forward or terminate incoming TLS connections
我的总体目标如下:我想编写一个 Go 服务器,它接受传入的 TLS 连接并检查客户端通过 TLS SNI extension 指示的服务器名称。根据服务器名称,我的服务器将:
- 将 TCP 连接转发(反向代理)到不同的服务器,而不终止 TLS,或者
- 终止 TLS 并自行处理请求
This excellent blog post 描述了一个反向代理,它检查 SNI 扩展并将连接转发到其他地方或终止它。基本技巧是从 TCP 连接中窥视足够的字节来解析 TLS ClientHello,如果应该转发服务器名称,则反向代理打开到最终目的地的 TCP 连接,将窥视的字节写入连接,然后设置goroutines 复制其余字节,直到来自客户端的 TCP 连接与到最终目的地的连接之间关闭。遵循 post 中的模型,我只需稍作更改即可实现行为 1。
问题出在 其他 情况下,行为 2,此时我的服务器应该终止 TLS 并自行处理应用程序层 HTTP 请求。我正在使用 Go 标准库的 HTTP 服务器,但它的 APIs 没有我需要的东西。具体来说,在我查看了 ClientHello 并确定连接应由我的服务器处理后,无法通过 net.Conn
to an existing 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
,由我传递给 func (srv *http.Server) ServeTLS
的 Go 频道支持。然后,当我从真正的 TCP net.Listener
接收到服务器应该自行处理的连接时,我将连接发送到合成侦听器,这会导致该侦听器的 Accept
到 return 新连接等待 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
}
最干净的解决方案可能是您通过实施自定义建议的方式 net.Listener
。
我会将 peekClientHello
函数修改为 return 一个 net.Conn
,实际上它只是对现有 net.Conn
和 io.TeeReader
的包装现有功能已经使用。现在我们有了一个新对象,可以将其复制到后端或由 Accept
函数 returned。您现在可以分层 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
}
我的总体目标如下:我想编写一个 Go 服务器,它接受传入的 TLS 连接并检查客户端通过 TLS SNI extension 指示的服务器名称。根据服务器名称,我的服务器将:
- 将 TCP 连接转发(反向代理)到不同的服务器,而不终止 TLS,或者
- 终止 TLS 并自行处理请求
This excellent blog post 描述了一个反向代理,它检查 SNI 扩展并将连接转发到其他地方或终止它。基本技巧是从 TCP 连接中窥视足够的字节来解析 TLS ClientHello,如果应该转发服务器名称,则反向代理打开到最终目的地的 TCP 连接,将窥视的字节写入连接,然后设置goroutines 复制其余字节,直到来自客户端的 TCP 连接与到最终目的地的连接之间关闭。遵循 post 中的模型,我只需稍作更改即可实现行为 1。
问题出在 其他 情况下,行为 2,此时我的服务器应该终止 TLS 并自行处理应用程序层 HTTP 请求。我正在使用 Go 标准库的 HTTP 服务器,但它的 APIs 没有我需要的东西。具体来说,在我查看了 ClientHello 并确定连接应由我的服务器处理后,无法通过 net.Conn
to an existing 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
,由我传递给 func (srv *http.Server) ServeTLS
的 Go 频道支持。然后,当我从真正的 TCP net.Listener
接收到服务器应该自行处理的连接时,我将连接发送到合成侦听器,这会导致该侦听器的 Accept
到 return 新连接等待 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
}
最干净的解决方案可能是您通过实施自定义建议的方式 net.Listener
。
我会将 peekClientHello
函数修改为 return 一个 net.Conn
,实际上它只是对现有 net.Conn
和 io.TeeReader
的包装现有功能已经使用。现在我们有了一个新对象,可以将其复制到后端或由 Accept
函数 returned。您现在可以分层 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
}