编程语言

[SSL: WRONG_VERSION_NUMBER]:Python 报错原因与修法

作者: · 最后核实

报错信息ssl.SSLError: [SSL: WRONG_VERSION_NUMBER] wrong version number

[SSL: WRONG_VERSION_NUMBER] wrong version number 的意思是:Python 发起了 TLS 握手,但对方回的不是 TLS。 那个端口上跑的是明文 HTTP 或其他明文协议,最常见的情况是代理地址写成了 https://、代码用错了端口,或者服务器在你以为的端口上没有启用 TLS。

快速修复

多数情况把代理地址从 https:// 改成 http:// 就能修好。先确认目标端口实际在说什么协议,以及 Python 用了哪个代理:

# 这个端口是不是在回明文 HTTP?
curl -sv http://example.com:443/ -o /dev/null

# Python 会使用哪些代理?
python3 -c "import urllib.request; print(urllib.request.getproxies())"
env | grep -i _proxy
  • 代理地址以 https:// 开头的,改成 http://。这里的协议指的是“你和代理之间怎么通信”,而不是目标网站用什么协议:
export HTTPS_PROXY=http://proxy.example.com:8080
export HTTP_PROXY=http://proxy.example.com:8080
  • curl http://...:443/ 能拿到 HTTP 响应,说明这个端口在跑明文,去修服务器或换正确的端口。
  • 邮件等其他协议,要让客户端类和端口对应(见下文)。

原因

requests、pip 等基于 urllib3 的工具最常见的原因是明文 HTTP 代理被写成了 https:// 地址,另外还有 3 种情况。

1. 明文 HTTP 代理用了 https:// 地址

公司代理、抓包工具(Fiddler、Charles、mitmproxy)以及各种本地代理软件,端口上一般接受的是明文 HTTP,再用 CONNECT 隧道转发 HTTPS。

  • 确认: getproxies() 输出里有 'https': 'https://...',或者设置 NO_PROXY=example.com 后报错消失。
  • 修复: HTTPS_PROXY、代码里的 proxies= 字典、pip --proxy 都改用 http://。Windows 上 Python 还会从注册表读取系统代理设置,本地代理软件开启“系统代理”时也要检查那里写的格式。

2. 用 TLS 客户端连了明文端口

直接 TLS 的客户端连到“先明文再 STARTTLS”的端口,会立刻失败。

  • 确认: URL 或代码用的是 80、25、587、143、5432 等端口,却用了只会 TLS 的客户端。
  • 修复: 按端口选用对应写法:
import smtplib
# 465 端口:直接 TLS
with smtplib.SMTP_SSL("smtp.example.com", 465) as s:
    s.login(user, password)
# 587 端口:先明文,再 STARTTLS
with smtplib.SMTP("smtp.example.com", 587) as s:
    s.starttls()
    s.login(user, password)

IMAP 同理:imaplib.IMAP4_SSL 对应 993 端口,IMAP4 加 starttls() 对应 143 端口。

3. 服务器在这个端口上没开 TLS

Web 服务器在 443 端口监听却没启用 TLS,或者负载均衡把明文 HTTP 转发到客户端以为是 HTTPS 的端口,也会报同样的错。

  • 确认: openssl s_client -connect example.com:443 -servername example.com 同样报 wrong version number,而 curl http://example.com:443/ 返回了 HTML。
  • 修复: Nginx 的 listen 要带 ssl 参数(listen 443 ssl;),并配置 ssl_certificate 和 ssl_certificate_key。宝塔面板里开启 SSL 后会自动写好这一行,手工改过配置文件的要重点检查。阿里云 SLB、腾讯云 CLB 的监听协议选成 HTTP 而前端用 HTTPS 访问,也是同样的现象。

4. 认证门户或中间设备用明文应答

酒店、机场 Wi-Fi 的登录页和一些过滤网关会拦截 443 端口,并返回 HTTP 跳转。

  • 修复: 先完成门户登录,或换一个网络测试确认。

各环境修法

客户端工具的修法一致:代理地址用 http://,目标地址保持 https://;服务器端要在监听端口上启用 TLS。

pip: pip install --proxy http://proxy.example.com:8080 package,或一次性写入配置 pip config set global.proxy http://proxy.example.com:8080。使用清华、阿里云等国内镜像源时,镜像地址本身保持 https://,只有代理地址用 http://。

requests: requests.get(url, proxies={"https": "http://proxy.example.com:8080"})。requests 默认也会读取环境变量,除非设置 session.trust_env = False。

Docker 和 CI: 代理变量常由 Runner 或 Docker 守护进程配置自动注入。在容器里打印环境变量,看 Python 实际拿到的值。

服务器端: 修好监听配置后,用 SSL 检测工具复查;邮件服务器用 STARTTLS 检测工具。

不要这样做

这三种做法要么让数据明文传输,要么对这个错误无效。

  • 把地址改成 http:// 让报错消失。 请求会以明文传输,token、密码一并暴露。
  • verify=False。 对这个错误没有作用,反而让同一段代码之后的所有请求都失去证书校验。
  • 强制使用旧协议,例如 ssl.PROTOCOL_TLSv1。问题不在 TLS 版本,而在对方没有 TLS;降级只会削弱本来正常的连接。

常见问题

verify=False 能解决 WRONG_VERSION_NUMBER 吗?

不能。握手在服务器发证书之前就失败了,没有证书可校验。关掉校验只会多一个安全问题。应该去改代理地址、端口或服务器配置。

为什么升级 pip 或 requests 后才开始报这个错?

pip 和 requests 依赖的 urllib3 新版本支持 HTTPS 代理,https:// 开头的代理地址会被当真:Python 先和代理本身建立 TLS 连接。代理如果只监听明文 HTTP,返回的就不是 TLS,于是报 WRONG_VERSION_NUMBER。把变量改成 http://proxy.example.com:8080 即可。

smtplib 哪个类配哪个端口?

465 端口是直接 TLS,用 smtplib.SMTP_SSL(host, 465);587 端口(以及通常的 25 端口)先明文再用 STARTTLS 升级,用 smtplib.SMTP(host, 587) 后调用 starttls()。QQ 邮箱、163 邮箱、阿里云企业邮箱的 SMTP 都支持 465 端口;另外很多云服务器默认封禁出站 25 端口,发信时优先用 465。

curl 遇到同样问题时是什么报错?

curl 会输出 curl: (35),OpenSSL 信息里带 wrong version number,原因相同,见 curl (35) SSL connect error。

资料来源

  1. Python docs: ssl — TLS/SSL wrapper for socket objects
  2. Requests: Advanced Usage (Proxies)
  3. RFC 8314: Cleartext Considered Obsolete (implicit TLS for mail)
  4. RFC 3207: SMTP Service Extension for Secure SMTP over TLS
  5. nginx: Configuring HTTPS servers