Repository navigation
fix(smtp): 出站SMTP连接增加30秒事务超时,防止下游无响应导致永久卡死 - #361
Closed
0clickjacking0 wants to merge 3 commits into
Closed
0clickjacking0 wants to merge 3 commits into
0clickjacking0 wants to merge 3 commits into
Conversation
问题: Go 标准库 net/smtp 风格的客户端在读取服务器响应时没有任何超时。 如果目标邮件服务器接受 TCP 连接后不返回任何数据(不发送 220 问候、 或收到 DATA 后不回 250),读操作会永久阻塞。 在单连接串行发送模型下,一次这样的挂死会导致整条发送通道停摆, 且期间不产生任何错误日志,难以排查。 修复: 1. 新增常量统一管理出站超时: - outboundSMTPDialTimeout = 2s(TCP 建连,沿用原值) - outboundSMTPTransactionTimeout = 30s(命令交互,新增) - outboundSMTPQuitWriteTimeout = 250ms(QUIT 写入,沿用原值) 2. 新增 newClientWithTimeout(),在建立连接后通过 conn.SetDeadline() 为整个命令交互阶段设置 30 秒截止线 3. NewClient() 默认应用该超时;明文与 TLS 两条路径均生效 4. 超时后按投递失败处理,由上层重试/退避机制恢复,连接被释放 效果:下游挂死从『永久卡死、通道停摆』变为『30 秒内失败并重试』。 测试: 新增 TestNewClientBoundsSilentServer,验证对静默服务器在 超时内失败返回,-race 通过。
0clickjacking0
force-pushed
the
fix/outbound-smtp-transaction-timeout
branch
from
August 28, 2026 12:31
334531f to
40f9959
Compare
Owner
|
感谢pr,看了下代码,这个超时可能存在风险吧。这30s是整个smtp会话的Deadline,假设邮件里面带一个非常大的附件,比如带一个1G的附件,很容易就传输超过30s了。 翻了下RFC,按https://www.rfc-editor.org/rfc/rfc5321.html#section-4.5.3.2 这个建议,最好是按命令控制超时,不然很容易造成问题 |
评审意见指出:原实现在整个 SMTP 会话上设置 30s 统一 Deadline, 携带大附件(如 1GB 附件)的邮件传输很容易超过 30s 被误杀。 RFC 5321 §4.5.3.2 明确要求: "An SMTP client MUST provide a timeout mechanism. It MUST use per-command timeouts rather than somehow trying to time the entire mail transaction." 本次修改移除会话级 Deadline,改为按阶段分别计时: 1. §4.5.3.2.1 初始 220 问候:5 分钟(NewClient,读完立即清除) 2. §4.5.3.2.2/3 单条命令(EHLO/HELO/MAIL/RCPT 等):5 分钟 (cmdWithTimeout,每条命令独立计时,结束后清除) 3. §4.5.3.2.4 DATA 等待 354:2 分钟 4. §4.5.3.2.5 数据块写入:3 分钟/块 (dataBlockWriter 按 32KB 分块刷新写超时,整体时限随 邮件大小线性扩展,大附件不再被误杀;对端挂死时最后一块 仍会在超时内失败释放连接) 5. §4.5.3.2.6 等待最终 250:10 分钟(避免过早超时引发重复投递) 超时值按 RFC 建议值取最小值,并用 var 声明便于调整。 测试: - TestNewClientBoundsSilentServer:静默服务器在问候超时内失败 - TestPhasedTimeoutsAllowSlowMultiPhaseTransaction:每阶段都慢、 总时长超过单阶段超时的健康下游不再被误杀 - TestStalledDataUploadFailsWithinDataBlockTimeout:354 后下游 停止读取,上传在数据块超时内失败而非永久阻塞 全部 -race 通过。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题:
Go 标准库 net/smtp 风格的客户端在读取服务器响应时没有任何超时。
如果目标邮件服务器接受 TCP 连接后不返回任何数据(不发送 220 问候、
或收到 DATA 后不回 250),读操作会永久阻塞。
在单连接串行发送模型下,一次这样的挂死会导致整条发送通道停摆。
修复:
效果:下游挂死从『永久卡死、通道停摆』变为『30 秒内失败并重试』。
配合并发发送(每条连接独立),单个挂死只影响一个并发槽。
测试:
新增 TestNewClientBoundsSilentServer,验证对静默服务器在
超时内失败返回,-race 通过。