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,6 +21,7 @@ CA_DIR = Path(os.environ.get("MITM_CA_DIR", | ||
| 21 | EVENT_SERVER = os.environ.get("EVENT_SERVER_URL", "http://127.0.0.1:8765/events") | 21 | EVENT_SERVER = os.environ.get("EVENT_SERVER_URL", "http://127.0.0.1:8765/events") |
| 22 | PROXY_PORT = int(os.environ.get("MITM_PORT", "8080")) | 22 | PROXY_PORT = int(os.environ.get("MITM_PORT", "8080")) |
| 23 | INTERCEPT_HOSTS = os.environ.get("MITM_INTERCEPT", "").split(",") | 23 | INTERCEPT_HOSTS = os.environ.get("MITM_INTERCEPT", "").split(",") |
| 24 | +_cert_lock = asyncio.Lock() | ||
| 24 | 25 | ||
| 25 | 26 | ||
| 26 | def ensure_ca(): | 27 | def ensure_ca(): |
| @@ -158,8 +159,9 @@ async def handle_connect(client_r: asyncio.StreamReader, | @@ -158,8 +159,9 @@ async def handle_connect(client_r: asyncio.StreamReader, | ||
| 158 | return | 159 | return |
| 159 | 160 | ||
| 160 | try: | 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 | except Exception as e: | 165 | except Exception as e: |
| 164 | print(f"[mitm] cert failed for {host}: {e}") | 166 | print(f"[mitm] cert failed for {host}: {e}") |
| 165 | try: | 167 | try: |
| @@ -178,20 +180,16 @@ async def handle_connect(client_r: asyncio.StreamReader, | @@ -178,20 +180,16 @@ async def handle_connect(client_r: asyncio.StreamReader, | ||
| 178 | ssl_cli_ctx.check_hostname = False | 180 | ssl_cli_ctx.check_hostname = False |
| 179 | ssl_cli_ctx.verify_mode = ssl.CERT_NONE | 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 | try: | 183 | try: |
| 191 | loop = asyncio.get_event_loop() | 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 | srv_r, srv_w = await asyncio.wait_for( | 194 | srv_r, srv_w = await asyncio.wait_for( |
| 197 | asyncio.open_connection(host, port, ssl=ssl_cli_ctx), timeout=10) | 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,7 +197,7 @@ async def handle_connect(client_r: asyncio.StreamReader, | ||
| 199 | req_acc: bytearray = bytearray() | 197 | req_acc: bytearray = bytearray() |
| 200 | resp_acc: bytearray = bytearray() | 198 | resp_acc: bytearray = bytearray() |
| 201 | await asyncio.gather( | 199 | await asyncio.gather( |
| 202 | - pipe(tls_r, srv_w, req_acc), | 200 | + pipe(client_r, srv_w, req_acc), |
| 203 | pipe(srv_r, tls_w, resp_acc), | 201 | pipe(srv_r, tls_w, resp_acc), |
| 204 | return_exceptions=True) | 202 | return_exceptions=True) |
| 205 | 203 | ||
| @@ -212,7 +210,7 @@ async def handle_connect(client_r: asyncio.StreamReader, | @@ -212,7 +210,7 @@ async def handle_connect(client_r: asyncio.StreamReader, | ||
| 212 | "req_preview": req_acc[:512].decode("utf-8", errors="replace"), | 210 | "req_preview": req_acc[:512].decode("utf-8", errors="replace"), |
| 213 | "ts": int(time.time()), | 211 | "ts": int(time.time()), |
| 214 | }) | 212 | }) |
| 215 | - except Exception as e: | 213 | + except Exception: |
| 216 | pass | 214 | pass |
| 217 | 215 | ||
| 218 | 216 |