Golang中多个TLS服务器导致数据竞争问题

Golang中多个TLS服务器导致数据竞争问题 我正在尝试在同一个Go程序中运行一个HTTP服务器和一个TCP服务器(cache.RemoteTlsConnHandler)。

该程序托管一个Web服务器实例和一个TCP服务器实例。两者运行在不同的端口上,并且都启用了HTTPS。下面的代码片段展示了在主线程中运行的部分。net/http和我的TCP服务器(cache.RemoteTlsConnHandler)之间应该没有共享值(但我几乎可以确定没有?!)。接下来,它创建密钥对,初始化服务器和配置,并启动TLS连接服务器。当我移除其中一个服务器初始化(远程缓存或ListenAndServe)时,竞态条件就消失了。这个条件本身没什么用,因为读取不可用,而写入总是在我的代码之外,但在Go的net/http栈中。我该如何继续调试这类问题?(我知道没有可测试的示例,但我没能重现这个问题,这里是代码库的链接,也许能提供更多上下文。)堆栈跟踪在重复(我至少认为是这样),并且只有当我与两个实例中的一个交互时,竞态条件才会出现。

// params: port int, bindAddress string, pwHash string, dosProtection bool, serverCert string,     serverKey string
go func(cache cache.Cache, syncPort int, address string, token string, dosProtection bool, tlsCert string, tlsKey string) error {
    return cache.RemoteTlsConnHandler(syncPort, address, token, dosProtection, tlsCert, tlsKey)
}(instance.cache, instance.syncPort, instance.address, instance.token, false, instance.tlsCert, instance.tlsKey)

cert, err := tls.X509KeyPair([]byte(instance.tlsCert), []byte(instance.tlsKey))
if err != nil {
    return err
}

tlsConnServer := http.Server {
    Addr:      instance.address+":"+strconv.Itoa(instance.fileServerPort),
    TLSConfig: &tls.Config{
        Certificates: []tls.Certificate{cert},
    },
}
if err := tlsConnServer.ListenAndServeTLS("", ""); err != nil {
    return err
}

堆栈跟踪:https://pastebin.com/6V0cjHc5

问候


更多关于Golang中多个TLS服务器导致数据竞争问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中多个TLS服务器导致数据竞争问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


根据你提供的代码和堆栈跟踪,这是一个典型的TLS配置共享导致的竞态条件问题。问题在于两个服务器实例共享了同一个tls.Certificate对象,而TLS连接处理过程中会修改证书的内部状态。

问题分析

从堆栈跟踪可以看出,竞态发生在crypto/tls包的(*clientHandshakeState).doFullHandshake方法中,具体是sessionState的读写操作。当多个goroutine同时使用同一个证书进行TLS握手时,就会发生数据竞争。

解决方案

为每个服务器创建独立的TLS证书副本:

// 为TCP服务器创建证书
go func(cache cache.Cache, syncPort int, address string, token string, dosProtection bool, tlsCert string, tlsKey string) error {
    // 为TCP服务器单独创建证书
    cert, err := tls.X509KeyPair([]byte(tlsCert), []byte(tlsKey))
    if err != nil {
        return err
    }
    return cache.RemoteTlsConnHandler(syncPort, address, token, dosProtection, tlsCert, tlsKey, cert)
}(instance.cache, instance.syncPort, instance.address, instance.token, false, instance.tlsCert, instance.tlsKey)

// 为HTTP服务器创建独立的证书
httpCert, err := tls.X509KeyPair([]byte(instance.tlsCert), []byte(instance.tlsKey))
if err != nil {
    return err
}

tlsConnServer := http.Server {
    Addr:      instance.address+":"+strconv.Itoa(instance.fileServerPort),
    TLSConfig: &tls.Config{
        Certificates: []tls.Certificate{httpCert},
        // 可选:添加会话票据密钥以提高性能
        SessionTicketsDisabled: false,
    },
}
if err := tlsConnServer.ListenAndServeTLS("", ""); err != nil {
    return err
}

同时,需要修改RemoteTlsConnHandler函数签名以接收证书参数:

func (c *Cache) RemoteTlsConnHandler(port int, address, token string, dosProtection bool, tlsCert, tlsKey string, cert tls.Certificate) error {
    config := &tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientAuth:   tls.NoClientCert,
    }
    
    listener, err := tls.Listen("tcp", address+":"+strconv.Itoa(port), config)
    if err != nil {
        return err
    }
    defer listener.Close()
    
    // ... 其余处理逻辑
}

替代方案:使用证书池

如果证书较大或需要频繁创建,可以使用证书池:

// 创建证书池
certPool := x509.NewCertPool()
if ok := certPool.AppendCertsFromPEM([]byte(instance.tlsCert)); !ok {
    return errors.New("failed to parse certificate")
}

// HTTP服务器配置
tlsConnServer := http.Server {
    Addr: instance.address+":"+strconv.Itoa(instance.fileServerPort),
    TLSConfig: &tls.Config{
        GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
            cert, err := tls.X509KeyPair([]byte(instance.tlsCert), []byte(instance.tlsKey))
            return &cert, err
        },
        RootCAs: certPool,
    },
}

调试建议

添加竞态检测标志运行程序:

go run -race main.go

使用互斥锁保护共享证书(不推荐,仅用于调试):

var certMutex sync.RWMutex
var sharedCert tls.Certificate

func getCertificate() tls.Certificate {
    certMutex.RLock()
    defer certMutex.RUnlock()
    return sharedCert
}

问题的根本原因是TLS证书在握手过程中包含可变状态(如会话票据),多个goroutine共享同一证书实例会导致竞态。为每个服务器实例创建独立的证书副本是最直接的解决方案。

回到顶部