http - 可以有条件地转发或终止传入 TLS 连接的 Go 服务器
问题描述
我的总体目标如下:我想编写一个接受传入 TLS 连接并通过TLS SNI 扩展检查客户端指示的服务器名称的 Go 服务器。根据服务器名称,我的服务器将:
- 将 TCP 连接转发(反向代理)到不同的服务器,而不终止 TLS,或者
- 终止 TLS 并自行处理请求
这篇优秀的博客文章描述了一个反向代理,它检查 SNI 扩展并将连接转发到其他地方或终止它。基本技巧是从 TCP 连接中窥视足够的字节来解析 TLS ClientHello,如果应该转发服务器名称,反向代理会打开到最终目的地的 TCP 连接,将窥视的字节写入连接,然后设置goroutines 复制其余字节,直到在来自客户端的 TCP 连接和到最终目的地的连接之间关闭。遵循该帖子中的模型,我只需进行少量更改即可实现行为 1。
问题在于另一种情况,即行为 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 循环。
现在,我能想到的唯一前进方法是创建我自己的net.Listener
由 Go 通道支持的“合成”,我将其传递给func (srv *http.Server) ServeTLS
. 然后,当我从真正的 TCP 接收net.Listener
到服务器应自行处理的连接时,我将连接发送到合成侦听器,这会导致该侦听Accept
器将新连接返回到 waiting 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
函数以返回一个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
}
推荐阅读
- mysql - SELECT DISTINCT ON 简单表唯一值
- angular - Angular 7 Routing 给了我一个 ; 在参数中
- javascript - 使用 Rails 中的活动存储直接上传 - input.dataset.directUploadUrl "undefined"
- git - 基于单个存储库中的分支将 ElasticBeanstalk 部署到不同账户
- javascript - JS 发送带有文本的 API 消息
- ubuntu - 将 kafka 路径添加到 /etc/environment 文件,以便全局访问 kafka。将 kafka 路径添加到 bin 文件夹
- mysql - mysql -v ERROR 1045 (28000): Access denied for user root'@'localhost' (使用密码: NO)
- python-3.x - Python拼接数据
- node.js - 我正在开发一个 mern 应用程序,但遇到错误 TypeError: Cannot read property 'username' of undefined on chrome console
- node.js - 无法使用 Node.js 获取 redis 键值对