Commit 4dc3e4d0a0410b99d1c4c95d1cc6d998b3588d14
1 parent
d59e4e1c
fix(mitm_proxy): 修复 HTTPS 拦截两个 bug,导致全局代理下 Chrome 无法上网
全局 http_proxy 指向此代理后,Chrome 等遵守系统代理的 app 的 HTTPS 请求 全部失败,而不读系统代理的 Termux 工具表现正常。 - TLS 握手改用 asyncio 原生 loop.start_tls(),去掉手动 dup socket 同步 握手的旧实现:cert 生成 await 让出控制权时,asyncio 事件循环会把刚到的 ClientHello 字节读进自己的缓冲区,导致握手线程永远等不到数据。 - 证书生成加 _cert_lock,避免同一域名并发首次访问时生成的 cert/key 文件 互相踩踏导致 KEY_VALUES_MISMATCH,此后该域名永久失败。 已在设备上实测验证:单域名 + 5 并发首次访问全部通过。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Showing
1 changed file
with
14 additions
and
16 deletions
phoneuse/mitm_proxy.py
| ... | ... | @@ -21,6 +21,7 @@ CA_DIR = Path(os.environ.get("MITM_CA_DIR", |
| 21 | 21 | EVENT_SERVER = os.environ.get("EVENT_SERVER_URL", "http://127.0.0.1:8765/events") |
| 22 | 22 | PROXY_PORT = int(os.environ.get("MITM_PORT", "8080")) |
| 23 | 23 | INTERCEPT_HOSTS = os.environ.get("MITM_INTERCEPT", "").split(",") |
| 24 | +_cert_lock = asyncio.Lock() | |
| 24 | 25 | |
| 25 | 26 | |
| 26 | 27 | def ensure_ca(): |
| ... | ... | @@ -158,8 +159,9 @@ async def handle_connect(client_r: asyncio.StreamReader, |
| 158 | 159 | return |
| 159 | 160 | |
| 160 | 161 | try: |
| 161 | - cert_path, key_path = await asyncio.get_event_loop().run_in_executor( | |
| 162 | - None, make_host_cert, host) | |
| 162 | + async with _cert_lock: | |
| 163 | + cert_path, key_path = await asyncio.get_event_loop().run_in_executor( | |
| 164 | + None, make_host_cert, host) | |
| 163 | 165 | except Exception as e: |
| 164 | 166 | print(f"[mitm] cert failed for {host}: {e}") |
| 165 | 167 | try: |
| ... | ... | @@ -178,20 +180,16 @@ async def handle_connect(client_r: asyncio.StreamReader, |
| 178 | 180 | ssl_cli_ctx.check_hostname = False |
| 179 | 181 | ssl_cli_ctx.verify_mode = ssl.CERT_NONE |
| 180 | 182 | |
| 181 | - transport = client_w.transport | |
| 182 | - sock = transport.get_extra_info("socket") | |
| 183 | - if sock is None: | |
| 184 | - return | |
| 185 | - | |
| 186 | - import socket as _socket | |
| 187 | - raw_sock = sock.dup() | |
| 188 | - transport.close() | |
| 189 | - | |
| 190 | 183 | try: |
| 191 | 184 | loop = asyncio.get_event_loop() |
| 192 | - tls_sock = await loop.run_in_executor( | |
| 193 | - None, lambda: ssl_srv_ctx.wrap_socket(raw_sock, server_side=True)) | |
| 194 | - tls_r, tls_w = await asyncio.open_connection(sock=tls_sock) | |
| 185 | + old_transport = client_w.transport | |
| 186 | + protocol = old_transport.get_protocol() | |
| 187 | + # Upgrade the existing client connection to TLS in place (asyncio's | |
| 188 | + # native STARTTLS support) instead of detaching the raw fd — keeps | |
| 189 | + # the handshake inside the event loop with no socket hand-off race. | |
| 190 | + tls_transport = await loop.start_tls( | |
| 191 | + old_transport, protocol, ssl_srv_ctx, server_side=True) | |
| 192 | + tls_w = asyncio.StreamWriter(tls_transport, protocol, client_r, loop) | |
| 195 | 193 | |
| 196 | 194 | srv_r, srv_w = await asyncio.wait_for( |
| 197 | 195 | asyncio.open_connection(host, port, ssl=ssl_cli_ctx), timeout=10) |
| ... | ... | @@ -199,7 +197,7 @@ async def handle_connect(client_r: asyncio.StreamReader, |
| 199 | 197 | req_acc: bytearray = bytearray() |
| 200 | 198 | resp_acc: bytearray = bytearray() |
| 201 | 199 | await asyncio.gather( |
| 202 | - pipe(tls_r, srv_w, req_acc), | |
| 200 | + pipe(client_r, srv_w, req_acc), | |
| 203 | 201 | pipe(srv_r, tls_w, resp_acc), |
| 204 | 202 | return_exceptions=True) |
| 205 | 203 | |
| ... | ... | @@ -212,7 +210,7 @@ async def handle_connect(client_r: asyncio.StreamReader, |
| 212 | 210 | "req_preview": req_acc[:512].decode("utf-8", errors="replace"), |
| 213 | 211 | "ts": int(time.time()), |
| 214 | 212 | }) |
| 215 | - except Exception as e: | |
| 213 | + except Exception: | |
| 216 | 214 | pass |
| 217 | 215 | |
| 218 | 216 | ... | ... |