Python asyncio连接升级TLS时的顺序问题及解决咨询
无需自定义asyncio Protocol的MySQL协议TLS中途升级方案
针对你遇到的客户端先发起TLS请求时握手失败的问题,无需自定义asyncio.Protocol,直接用Python 3.7+内置的asyncio.start_tls()即可解决,核心是严格遵循MySQL有线协议的TLS升级顺序:客户端发送SSL请求→服务器返回允许应答→双向TLS握手。
关键解决步骤
- 服务器先读取客户端的SSL请求标识(MySQL协议中为特定格式的包,此处简化为1字节标识)
- 服务器返回允许SSL的应答包
- 调用
asyncio.start_tls()将现有TCP流升级为TLS加密流 - 使用升级后的流进行后续加密通信
完整代码示例
服务器端代码
import asyncio import ssl async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): # 读取客户端的SSL请求(模拟MySQL的SSL请求标识) try: ssl_request = await reader.readexactly(1) except asyncio.IncompleteReadError: writer.close() await writer.wait_closed() return if ssl_request == b'\x01': # 发送允许SSL的应答(模拟MySQL的同意标识) writer.write(b'\x00') await writer.drain() # 初始化服务器SSL上下文 ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_context.load_cert_chain(certfile="server.crt", keyfile="server.key") # 升级为TLS流,核心调用:asyncio.start_tls tls_reader, tls_writer = await asyncio.start_tls( reader, writer, ssl_context, server_side=True, ssl_handshake_timeout=10 ) # 加密通信逻辑 try: data = await tls_reader.read(1024) print(f"收到加密数据: {data.decode('utf-8')}") tls_writer.write(b"已收到加密请求,这是加密响应") await tls_writer.drain() finally: tls_writer.close() await tls_writer.wait_closed() else: # 处理非SSL请求 writer.write(b"无效请求,请先发起SSL升级") await writer.drain() writer.close() await writer.wait_closed() async def main(): server = await asyncio.start_server(handle_client, '127.0.0.1', 8888) async with server: await server.serve_forever() if __name__ == "__main__": asyncio.run(main())
客户端代码(模拟MySQL先发起TLS请求的行为)
import asyncio import ssl async def main(): reader, writer = await asyncio.open_connection('127.0.0.1', 8888) # 发送SSL升级请求 writer.write(b'\x01') await writer.drain() # 读取服务器的同意应答 ssl_ack = await reader.readexactly(1) if ssl_ack == b'\x00': # 初始化客户端SSL上下文(测试环境禁用证书验证,生产环境需开启) ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE # 升级为TLS流 tls_reader, tls_writer = await asyncio.start_tls( reader, writer, ssl_context, server_side=False, server_hostname='127.0.0.1' ) # 发送加密数据 tls_writer.write(b"Hello MySQL 协议客户端(加密)") await tls_writer.drain() # 接收加密响应 data = await tls_reader.read(1024) print(f"收到加密响应: {data.decode('utf-8')}") tls_writer.close() await tls_writer.wait_closed() asyncio.run(main())
注意事项
- 顺序严格性:必须先处理明文的SSL请求/应答,再触发TLS握手,否则会导致明文数据被当作TLS握手数据解析,引发握手失败
- 证书准备:服务器端需要生成自签名证书(执行命令:
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes) - 流的替换:升级后必须使用
start_tls()返回的tls_reader和tls_writer,原有的流对象已被TLS层接管,不可再使用 - 超时设置:添加
ssl_handshake_timeout避免因网络问题导致的握手阻塞
内容的提问来源于stack exchange,提问作者Barak Alon
相关产品推荐
相关产品推荐

