如何用Go实现按SNI选择转发或终止TLS连接的服务?
解决方案
方案1:使用Go 1.21+原生公开的ServeConnAPI
你要的HandleConnection能力其实标准库已经提供,Go 1.21正式导出了http.Server.ServeConn方法,专门用于将单条连接交给HTTP服务器处理,完全匹配你的需求。
你只需要把peekClientHello返回的带缓冲区的reader包装成net.Conn,直接调用该方法即可:
// 实现带缓冲区的Conn,保证已经读取的ClientHello字节能被后续TLS握手读到 type bufferedConn struct { net.Conn r io.Reader } func (bc bufferedConn) Read(p []byte) (int, error) { return bc.r.Read(p) }
你的TODO部分可以直接替换为:
// 注意:要把开头的defer clientConn.Close()移到转发分支,这里连接交给HTTP服务器管理,不需要手动关闭 server.ServeConn(bufferedConn{ Conn: clientConn, r: clientReader, }, http.ServeConnOpts{ TLSConfig: &server.TLSConfig, // 传入你的TLS证书配置即可,自动完成TLS握手 }) return
该方案最简洁,所有标准库HTTP服务器的原生能力(超时、连接池、优雅关闭等)都可以正常使用。
方案2:兼容旧版本Go的合成Listener实现
你之前想到的合成Listener方案是工业界非常成熟的常用实现,一点都不冗余,代码实现非常轻量:
type ChanListener struct { connChan chan net.Conn closed chan struct{} } func NewChanListener() *ChanListener { return &ChanListener{ connChan: make(chan net.Conn), closed: make(chan struct{}), } } func (l *ChanListener) Accept() (net.Conn, error) { select { case conn := <-l.connChan: return conn, nil case <-l.closed: return nil, errors.New("listener closed") } } func (l *ChanListener) Close() error { close(l.closed) return nil } func (l *ChanListener) Addr() net.Addr { return &net.TCPAddr{IP: net.IPv4zero, Port: 0} } // 自定义方法:将需要本地处理的连接喂给HTTP服务器 func (l *ChanListener) FeedConn(c net.Conn) { select { case l.connChan <- c: case <-l.closed: c.Close() } }
改造你的main函数,单独启动goroutine跑HTTP服务:
func main() { l, _ := net.Listen("tcp", ":443") chanListener := NewChanListener() server := http.Server{ TLSConfig: &tls.Config{ // 填入你的证书配置 }, } // 后台启动HTTP服务 go func() { log.Fatal(server.ServeTLS(chanListener, "", "")) }() for { conn, err := l.Accept() if err != nil { continue } go handleConnection(conn, chanListener) } }
TODO部分替换为:
// 同样需要先包装bufferedConn chanListener.FeedConn(bufferedConn{Conn: clientConn, r: clientReader}) return
该方案兼容所有Go版本,完全解耦SNI路由逻辑和HTTP服务逻辑,适合需要兼容旧版本的场景。
内容的提问来源于stack exchange,提问作者ravron
相关产品推荐
相关产品推荐

