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,6 +21,7 @@ CA_DIR = Path(os.environ.get(&quot;MITM_CA_DIR&quot;,
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