Commit 4dc3e4d0a0410b99d1c4c95d1cc6d998b3588d14

Authored by yibo
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(&quot;MITM_CA_DIR&quot;,
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  
... ...