本文发表于入职啦(公众号: ruzhila) 大家可以访问入职啦学习更多的编程实战。
发现问题,用工具定位和分析问题,再通过代码找到解决方案,是每个工程师必备的技能,分享一个实际的案例,通过分析golang代码,处理无法通过腾讯云SMTP发送邮件的问题。
问题描述
最近在使用golang发送邮件的时候,发现无法通过腾讯云SMTP发送邮件,代码如下:
表现就是始终会得到一个错误:
--- FAIL: TestSMTP (0.19s)
/Users/mpi/workspace/ruzhila/ruzhila-app/notify_test.go:93:
Error Trace: /Users/mpi/workspace/ruzhila/ruzhila-app/notify_test.go:93
Error: Expected nil, but got: &tls.permanentError{err:(*net.OpError)(0x14000240e60)}
Test: TestSMTP
这个错误看起来是一个tls错误,在这个之前,科普一下SMTP端口的问题:
- 25 端口是SMTP协议的默认端口,用于发送邮件, 但是不安全,是明文传输
- 465 端口是SMTP协议的加密端口,使用SSL加密,但是现在已经不推荐使用了
- 587 端口是SMTP协议的加密端口,使用TLS加密,是目前推荐使用的端口
根据腾讯云官网的配置,腾讯云的SMTP服务器是支持465的。并且不支持25端口。
实际情况呢?
腾讯云是支持465和25端口的,并且25是支持STARTTLS的,也就是可以使用TLS加密,但是这个错误看起来是一个tls错误,看起来是tls握手失败了。
定位问题
我们既然确定腾讯云支持25和465那么就简单很多了,我们分别写代码测试看看, 看看是哪个环节出问题了
我们先定几个出现问题可能性:
- 服务器就是不能工作, 就是不支持TLS
- 服务器的配置有问题
- 客户端库有问题
- 客户端的配置有问题
我们第一步先测试25端口,不加密的情况下能否正常工作,然后再测试465端口,加密的情况下能否正常工作。
并且我们要最简单的测试方式,我们先用python测试一下,这样能同时验证问题1和3
用Python验证服务正常?
- 第一步测试25端口能否正常工作, 是否支持TLS
我们发现通过25端口,不加密的情况下是可以发送邮件的,那么就是说明腾讯云的25端口是能正常工作的,那么问题就出在tls加密上了。
- 测试465端口能否正常工作
client = smtplib.SMTP_SSL("smtp.qcloudmail.com", 465)
client.login("admin@ruzhila.cn", "YYYYY")
client.sendmail("admin@ruzhila.cn", ["kui@ruzhila.cn"], "Subject: test\n\nHello, world!")
我们得到了一个异常:
self._sslobj.do_handshake()
ssl.SSLError: [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1000)
看起来是go和python的tls都不能正常工作,那么我们就是下一步,定位服务器是不是真的有问题?
现在的结论就是服务能工作,就是TLS不正常,不管是go还是python都不能正常工作,那么问题就出在TLS上了。
使用openssl测试
第一步我们先测试25的STTLS是否正常工作, 我们通过自带的openssl测试一下:
openssl s_client -connect smtp.qcloudmail.com:25 -starttls smtp
我们发现是支持的,同时也测试了465端口:
openssl s_client -connect smtp.qcloudmail.com:465
也就是说服务器的ssl是能正常工作的,那就是问题一下子范围就缩小到了go的tls库上了。
- 服务器就是不能工作, 就是不支持TLS ✅ 服务器是能工作的,并且有TLS
- 服务器的配置有问题 ✅ 服务器的配置是正常的,至少openssl是能正常工作的
- 客户端库有问题 ❌ python和go都有问题,但是这些库都是标准库,不应该怀疑标准库的实现
- 客户端的配置有问题 ❌ 客户端没有做任何的配置,那就是默认的配置出现问题
回到golang代码,我们查看与openssl的区别
我们回到golang代码,我们发现golang的代码是这样的,我们精简一下测试的代码,直接访问465端口:
这个问题依旧,也是就是默认的配置出现问题了,那么我们就要看看golang的tls配置和openssl的配置有什么不同了。
我们通过Wireshark抓包,看看golang和openssl的握手有什么不同。
有经验的工程师也可以用tcpdump, 但是还是推荐wireshark
我们对比一下,两个握手的区别:
发现最大的差别就是go的加密算法明显少了很多,我们再通过检查openssl握手之后采用的算法:
New, TLSv1/SSLv3, Cipher is AES256-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
Protocol : TLSv1.2
Cipher : AES256-SHA
Session-ID: 717C9AE5924DF91B3BD5A075FCE13A612DC07F4519E6832A680A609EF856DAA2
Session-ID-ctx:
openssl最终采用到了AES256-SHA,那么我们就要看看golang的tls配置是不是支持AES256-SHA, 根据wireshark的截图,我们并没发现AES256-SHA。
所以问题就能直接定位就是go和python默认是没有采用AES256-SHA的,导致了握手失败。
通过代码分析
如果我们手工加上AES256-SHA,是不是也能解决问题?
果然,我们将CipherSuites加上TLS_RSA_WITH_AES_128_CBC_SHA之后,问题就解决了。
那就是最终定位到是代码配置的问题,那么我们怎么解决这个问题?
解决问题
在 Go 中,TLS_RSA 密钥交换(KEX)算法的使用逐渐被弃用,主要是因为它的安全性较低。TLS_RSA 密钥交换算法在 TLS 1.3 中被完全移除,并且在 TLS 1.2 中也不再推荐使用。
TLS_RSA 密钥交换算法的主要问题在于它不提供前向安全性(Forward Secrecy)。前向安全性意味着即使服务器的长期密钥被泄露,过去的会话密钥也不会被泄露。相比之下,基于 Diffie-Hellman 的密钥交换算法(如 ECDHE)提供了前向安全性,因此被广泛推荐和使用。
也就是说,golang默认不支持TLS_RSA交换算法,我们如果手工指定也是可以的,但是这样是不推荐的,我们应该使用更加安全的算法。
我们去查看crypto/tls的代码,发现CipherSuites是一个[]uint16,如果我们不指定,那么默认是这样的: 在crypto/tls/handshake_client.go:98
也就是说握手的时候,会通过:
cipherSuites() -> defaultCipherSuites() -> cipherSuitesPreferenceOrder
最终选择默认的交互算法,也就是如果不指定tlsrsakex.Value(),默认是不会启用tls_rsa算法。
终于,我们定位到默认配置的问题,那么我们应该怎么解决这个问题呢?
现在有两种解决方案:
- 手工指定tlsConfig到TLS_RSA_WITH_AES_256_CBC_SHA
那么这样就可以确保我们的代码是能正常工作的
- 通过修改godebug的环境变量,强制启用TLS_RSA算法
export GODEBUG=tlsrsakex=1
- 代码里面设置这个变量
os.Setenv("GODEBUG", "tlsrsakex=1")
这样我们就比较少改动的方式,让代码正常工作。
总结
除了写代码,定位问题是工程师必备的技能,通过分析golang代码,处理无法通过腾讯云SMTP发送邮件的问题,所有的问题,都需要很多工具和经验。
但是通过减少问题的范围,再做diff,再去看代码实现,这个过程是非常有意思的,大家对问题要知道原因并且用最可靠的方案修改
最后,腾讯云的安全配置也缺少文档和兼容性,毕竟这个问题,他们自己写个代码就能发现,但是过了这么多年,他们也没发现和更新问题,真的是耽误了大家很多的时间,希望腾讯云能够改进。
我们构建了一个编程实战群,大家可以扫码加入,一起学习编程
也可以访问入职啦学习更多的编程实战