Golang中TLS服务器缓冲区数据卡住问题解析
Golang中TLS服务器缓冲区数据卡住问题解析 大家好,
我在为SMTP STARTTLS功能实现测试时遇到了一个问题。具体来说,当TLS握手失败时,我想测试在优雅关闭连接之前是否发送了QUIT命令。
问题:
QUIT命令似乎卡在了TLS连接处理程序的缓冲区中- 此行为是间歇性的,似乎取决于命令发送的速度
- 在发送
QUIT命令前添加一个小的延迟可以解决此问题
为了隔离问题,我使用简单的纯文本通信(不包含SMTP特定代码)创建了一个最小化的复现案例。
package main
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"strings"
"sync"
"testing"
"time"
)
func TestTLSHandshake(t *testing.T) {
ln := newLocalListener(t)
defer ln.Close()
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
// Client
clientConn, _ := net.Dial("tcp", ln.Addr().String())
defer clientConn.Close()
_, _ = clientConn.Write([]byte("STARTTLS\r\n"))
// Wait for readiness
buf := make([]byte, 1024)
_, _ = clientConn.Read(buf)
fmt.Printf("[Client] Received reply: %s", string(buf))
// Try TLS handshake
tlsConn := tls.Client(clientConn, &tls.Config{
ServerName: "localhost",
})
// Handshake should fail because client doesn't trust self-signed cert
err := tlsConn.Handshake()
if err != nil {
fmt.Println("[Client] Handshake err: ", err)
}
// Delay to prevent QUIT from being consumed into TLS buffer
time.Sleep(100 * time.Millisecond)
_, _ = clientConn.Write([]byte("QUIT\r\n"))
wg.Done()
}()
serverConn, _ := ln.Accept()
defer serverConn.Close()
quitReceived := false
scanner := bufio.NewScanner(serverConn)
for scanner.Scan() {
input := scanner.Text()
fmt.Printf("[Server] Received: '%s'\n", input)
switch input {
case "STARTTLS":
// Generate test cert
cert, privateKey, _ := generateTestCertificatePair()
// Setup TLS
keypair, _ := tls.X509KeyPair(cert, privateKey)
config := &tls.Config{Certificates: []tls.Certificate{keypair}}
// Reply readiness
_, _ = serverConn.Write([]byte("Ready for handshake\r\n"))
tlsConn := tls.Server(serverConn, config)
handshakeErr := tlsConn.Handshake()
if handshakeErr != nil {
fmt.Println("[Server] Handshake error: ", handshakeErr)
continue
} else {
fmt.Println("[Server] Handshake completed")
t.Fatal("Handshake unexpectedly succeeded")
}
case "QUIT":
quitReceived = true
fmt.Println("[Server] Quit received")
}
}
wg.Wait()
if !quitReceived {
t.Fatal("[Server] Quit not received")
}
}
// Generate X509 encoded certificate for testing.
func generateTestCertificatePair() ([]byte, []byte, error) {
organization := "example"
host := "localhost"
validFrom := time.Now()
validTo := validFrom.Add(24 * time.Hour)
// Generate private key
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
// Create certificate serial number
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, nil, err
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{organization},
CommonName: host,
},
NotBefore: validFrom,
NotAfter: validTo,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
hosts := strings.Split(host, ",")
template.DNSNames = append(template.DNSNames, hosts...)
// Create certificate
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, nil, err
}
// Encode certificate to PEM
certBuffer := &bytes.Buffer{}
if err := pem.Encode(certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return nil, nil, err
}
// Encode private key to PEM
privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return nil, nil, err
}
keyBuffer := &bytes.Buffer{}
if err := pem.Encode(keyBuffer, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
return nil, nil, err
}
return certBuffer.Bytes(), keyBuffer.Bytes(), nil
}
func newLocalListener(t *testing.T) net.Listener {
ln, _ := net.Listen("tcp", "localhost:0")
return ln
}
上述代码也可在此处找到。请注意,如果移除延迟,服务器将不会收到QUIT命令。
我理解TCP是一个连续的流,应用程序应该自行处理消息边界。然而,我使用的是tls.Server(),并且无法控制它将如何处理TCP连接。
有什么方法可以确保服务器能够由主处理程序(而不是TLS处理程序)接收到QUIT命令吗?
感谢您的帮助!
更多关于Golang中TLS服务器缓冲区数据卡住问题解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html
哈哈。 事实上,大多数通信协议的设计都简单粗暴。按照设计,如果进入了奇偶校验和加密握手阶段,通常的做法是如果加密失败就丢弃连接。 我们举一个简单的例子:socks协议。最初,该协议也是明文传输并选择了一种奇偶校验和加密方法,但如果握手失败,客户端和服务器都会主动关闭连接并丢弃它。 原因很简单:重新建立连接的逻辑比密码学回退以重新对齐要简单得多。
更多关于Golang中TLS服务器缓冲区数据卡住问题解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
感谢您花时间查看。
我最初的意图是继续使用同一个TCP连接以明文方式发送邮件,尤其是在TLS握手失败且用户未要求加密的情况下。正如您的代码所示,这需要服务器和客户端之间进行协调,以避免应用数据和TLS握手数据混在一起。
然而,查看Go TLS连接提供的方法后,我认为我们无法精细地控制发送Closure Alerts。查看其他Go语言编写的SMTP客户端,我也发现它们在遇到TLS错误时并不会尝试以明文发送,而是直接返回错误。
因此,最实际的解决方案似乎是关闭当前连接,创建一个新的TCP连接,然后通过这个新连接以明文方式发送。
无论如何,再次感谢您的关注。
// Delay to prevent QUIT from being consumed into TLS buffer time.Sleep(100 * time.Millisecond)
你对此感到疑惑吗?
这没什么可困惑的,因为在 TLS 握手失败后,客户端需要向服务器发送一条消息,而你快速发送消息是为了抢占握手完成的过程。
试试这个:
func TestTLSHandshake(t *testing.T) {
ln := newLocalListener(t)
defer ln.Close()
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
// Client
clientConn, _ := net.Dial("tcp", ln.Addr().String())
defer clientConn.Close()
_, _ = clientConn.Write([]byte("STARTTLS\r\n"))
// Wait for readiness
buf := make([]byte, 1024)
_, _ = clientConn.Read(buf)
fmt.Printf("[Client] Received reply: %s", string(buf))
// Try TLS handshake
tlsConn := tls.Client(clientConn, &tls.Config{
ServerName: "localhost",
})
// Handshake should fail because client doesn't trust self-signed cert
err := tlsConn.Handshake()
if err != nil {
fmt.Println("[Client] Handshake err: ", err)
}
// Delay to prevent QUIT from being consumed into TLS buffer
//time.Sleep(100 * time.Millisecond)
// wait server call handshake over
_, _ = clientConn.Read(make([]byte, 4))
_, _ = clientConn.Write([]byte("QUIT\r\n"))
wg.Done()
}()
serverConn, _ := ln.Accept()
defer serverConn.Close()
quitReceived := false
scanner := bufio.NewScanner(serverConn)
for scanner.Scan() {
input := scanner.Text()
fmt.Printf("[Server] Received: '%s'\n", input)
switch input {
case "STARTTLS":
// Generate test cert
cert, privateKey, _ := generateTestCertificatePair()
// Setup TLS
keypair, _ := tls.X509KeyPair(cert, privateKey)
config := &tls.Config{Certificates: []tls.Certificate{keypair}}
// Reply readiness
_, _ = serverConn.Write([]byte("Ready for handshake\r\n"))
tlsConn := tls.Server(serverConn, config)
handshakeErr := tlsConn.Handshake()
if handshakeErr != nil {
fmt.Println("[Server] Handshake error: ", handshakeErr)
} else {
fmt.Println("[Server] Handshake completed")
t.Fatal("Handshake unexpectedly succeeded")
}
// tell client handshake over
_, _ = serverConn.Write([]byte("QUIT"))
case "QUIT":
quitReceived = true
fmt.Println("[Server] Quit received")
}
}
wg.Wait()
if !quitReceived {
t.Fatal("[Server] Quit not received")
}
}
请参考 TLS 握手过程以获取更多信息,这样你就不会有这些疑问了。
这个问题是由于TLS层和TCP层之间的缓冲区交互导致的。当TLS握手失败后,TLS连接并没有被正确清理,导致后续数据被卡在TLS缓冲区中。以下是解决方案:
// 修改服务器端的TLS握手错误处理部分
case "STARTTLS":
// Generate test cert
cert, privateKey, _ := generateTestCertificatePair()
// Setup TLS
keypair, _ := tls.X509KeyPair(cert, privateKey)
config := &tls.Config{
Certificates: []tls.Certificate{keypair},
// 添加这个配置以便更好地控制连接
MinVersion: tls.VersionTLS12,
}
// Reply readiness
_, _ = serverConn.Write([]byte("Ready for handshake\r\n"))
tlsConn := tls.Server(serverConn, config)
handshakeErr := tlsConn.Handshake()
if handshakeErr != nil {
fmt.Println("[Server] Handshake error: ", handshakeErr)
// 关键修复:在握手失败后,需要显式关闭TLS连接并恢复原始连接
// 但注意:tls.Conn在握手失败后可能处于不一致状态
// 方案1:直接使用原始连接继续读取
// 由于握手失败,tlsConn不会接管连接,可以继续使用serverConn
continue
} else {
fmt.Println("[Server] Handshake completed")
t.Fatal("Handshake unexpectedly succeeded")
}
更完整的解决方案是修改客户端代码,在握手失败后重新建立连接或确保数据发送时机:
// 客户端修改方案
err := tlsConn.Handshake()
if err != nil {
fmt.Println("[Client] Handshake err: ", err)
// 关键:在握手失败后,不要立即使用原始连接发送数据
// 等待服务器端清理完成
time.Sleep(50 * time.Millisecond)
// 确保连接仍然有效
if clientConn != nil {
// 刷新缓冲区
clientConn.SetDeadline(time.Now().Add(100 * time.Millisecond))
// 发送QUIT命令
_, _ = clientConn.Write([]byte("QUIT\r\n"))
// 确保数据被发送
_ = clientConn.(*net.TCPConn).SetLinger(0)
}
}
另一个更可靠的方法是使用连接包装器来跟踪连接状态:
type trackedConn struct {
net.Conn
tlsActive bool
}
func (c *trackedConn) Write(b []byte) (int, error) {
if c.tlsActive {
// 如果TLS激活,可能需要特殊处理
}
return c.Conn.Write(b)
}
// 在服务器端使用
serverConn := &trackedConn{Conn: conn}
根本原因是当TLS握手失败时,tls.Server()创建的连接可能没有正确清理内部状态。在Go的TLS实现中,握手失败后连接可能处于半初始化状态,导致后续数据被缓冲在TLS层而不是传递给应用层。
可以通过设置更短的超时来避免这个问题:
// 在服务器端设置读取超时
serverConn.SetReadDeadline(time.Now().Add(5 * time.Second))
defer serverConn.SetReadDeadline(time.Time{}) // 清除超时
// 或者在scanner循环中
for scanner.Scan() {
// 每次读取前重置超时
serverConn.SetReadDeadline(time.Now().Add(2 * time.Second))
// ... 处理逻辑
}
对于SMTP STARTTLS场景,正确的处理模式应该是:
// SMTP服务器处理STARTTLS的标准模式
func handleSTARTTLS(conn net.Conn) {
// 发送220 Ready to start TLS
conn.Write([]byte("220 Ready to start TLS\r\n"))
// 执行TLS握手
tlsConn := tls.Server(conn, tlsConfig)
if err := tlsConn.Handshake(); err != nil {
// 握手失败,记录日志但保持原始连接开放
log.Printf("TLS handshake failed: %v", err)
// 可以继续使用原始连接或关闭
return
}
// 握手成功,切换到TLS连接
conn = tlsConn
}
在测试中,确保在握手失败后显式关闭TLS连接并恢复使用原始TCP连接:
// 测试中的修复
handshakeErr := tlsConn.Handshake()
if handshakeErr != nil {
fmt.Println("[Server] Handshake error: ", handshakeErr)
// 尝试关闭TLS连接以释放资源
tlsConn.Close()
// 继续使用原始连接
// 注意:此时可能需要重新创建scanner,因为底层连接可能被修改
scanner = bufio.NewScanner(serverConn)
continue
}
这些修改可以确保在TLS握手失败后,QUIT命令能够被服务器正确接收和处理。

