Rust in Everything
基于 reqwest 0.13.5

本机请求被系统代理拦走

主线位置:连接主线的第一步,连接器按代理表决定直连还是走代理。Proxy 怎么变成 Matcher、NoProxy 规则怎么解析、系统代理每次 build 读一遍,见 代理;macOS 系统设置那一侧(例外列表为什么不生效)在 hyper 卷的 本站的出站客户端 讲过。这一页从报错出发,走一遍排查和修法。

flowchart TB
  url["http 127.0.0.1"] --> no{"NO_PROXY 字面匹配"}
  no -->|命中| direct["直连"]
  no -->|没命中| px{"有代理配置"}
  px -->|环境变量| p1["HTTP_PROXY"]
  px -->|macOS 设置| p2["系统代理"]
  px -->|都没有| direct
  p1 --> drop["代理丢掉请求"]
  p2 --> drop
  drop --> err["connection closed"]

实验 crate:exp-reqwest/pita-lab,src/bin/p05_proxy.rs 和 src/bin/p05_sys.rs。p05_proxy 里的「代理」是自己写的:接受连接、读走请求头、然后挂断,模拟一个把回环请求丢掉的代理工具;HTTP_PROXY 指向它,所以请求不会离开本机。单线程运行时,set_var 时没有别的线程在读环境变量。M2 Max、macOS 27、rustc 1.99.0,release。

这台 Mac 的状态(scutil --proxy 摘录和 shell 环境):

  ExceptionsList : <array> {
    0 : captive.apple.com
    1 : localhost
    ...
    7 : 192.168.0.0/16
  }
  ExcludeSimpleHostnames : 1
  HTTPEnable : 1
  HTTPPort : 7890
  HTTPProxy : 127.0.0.1
  HTTPSEnable : 1
  ...
HTTP_PROXY=http://127.0.0.1:7890
NO_PROXY=localhost,127.0.0.1,192.168.64.0/24,.local,::1,.xmpbinfo.cn

现象

测试代码里对本机服务器发请求,用的是 Client::new():

== HTTP_PROXY=http://127.0.0.1:53990 NO_PROXY=None
  Client::new()         127.0.0.1 via proxy -> error sending request for url (http://127.0.0.1:53989/)
      <- client error (SendRequest)
      <- connection closed before message completed
  Client::new()         localhost via proxy -> error sending request for url (http://localhost:53989/)
      <- client error (SendRequest)
      <- connection closed before message completed
  builder().no_proxy()  127.0.0.1 direct    -> 200 OK
  builder().no_proxy()  localhost direct    -> 200 OK
== HTTP_PROXY=http://127.0.0.1:53990 NO_PROXY=Some("localhost")
  Client::new()         127.0.0.1 via proxy -> error sending request for url (http://127.0.0.1:53989/)
      <- client error (SendRequest)
      <- connection closed before message completed
  Client::new()         localhost direct    -> 200 OK
  builder().no_proxy()  127.0.0.1 direct    -> 200 OK
  builder().no_proxy()  localhost direct    -> 200 OK
== HTTP_PROXY=http://127.0.0.1:53990 NO_PROXY=Some("127.0.0.1")
  Client::new()         127.0.0.1 direct    -> 200 OK
  Client::new()         localhost via proxy -> error sending request for url (http://localhost:53989/)
      <- client error (SendRequest)
      <- connection closed before message completed
  builder().no_proxy()  127.0.0.1 direct    -> 200 OK
  builder().no_proxy()  localhost direct    -> 200 OK
== HTTP_PROXY=http://127.0.0.1:53990 NO_PROXY=Some("127.0.0.0/8,localhost")
  Client::new()         127.0.0.1 direct    -> 200 OK
  Client::new()         localhost direct    -> 200 OK
  builder().no_proxy()  127.0.0.1 direct    -> 200 OK
  builder().no_proxy()  localhost direct    -> 200 OK
== HTTPS_PROXY=http://127.0.0.1:53990 NO_PROXY=None
  builder()             https://localhost:54013/ -> error sending request for url (https://localhost:54013/)
      <- client error (Connect)
      <- tunnel error: unexpected end of file

(via proxy / direct 是看代理有没有收到新连接判断的。)

  • 报错里没有「proxy」这个词。明文请求报 connection closed before message completed,像是服务器崩了;HTTPS 请求报 tunnel error: unexpected end of file,CONNECT 隧道没建起来。
  • NO_PROXY=localhost 放过了 localhost,没放过 127.0.0.1;反过来也一样。
  • .no_proxy() 的客户端在每种环境下都直连。

环境变量清掉以后,请求还是会走代理,这次是 macOS 系统设置里的那个。p05_sys 不发请求,直接问 hyper-util 的 Matcher::from_system()(reqwest 的系统代理用的就是它)每个地址会被交给谁:

http://127.0.0.1:8080/     -> proxy http://127.0.0.1:7890/
http://localhost:8080/     -> proxy http://127.0.0.1:7890/
http://192.168.1.10/       -> proxy http://127.0.0.1:7890/
http://printer.local/      -> proxy http://127.0.0.1:7890/
https://localhost:8443/    -> proxy http://127.0.0.1:7890/

系统例外列表里明写着 localhost、*.local、192.168.0.0/16,一个都没起作用。

原因

默认就读系统代理。 默认 feature 里有 system-proxy,ClientBuilder::new 里 auto_sys_proxy 为真,build 时把 Matcher::system 加进代理表。它调 hyper-util 的 Matcher::from_system:先读 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY、NO_PROXY(大写优先,再试小写),环境变量没给的 scheme 再用 macOS 的系统设置补上。

系统设置只读六个键。 mac::with_system 只取 HTTP、HTTPS 两组的开关、主机、端口,ExceptionsList 和 ExcludeSimpleHostnames 都不读。排除规则只有一个来源:NO_PROXY 环境变量。

回环地址没有特殊待遇。 Matcher::intercept 第一步是 self.no.contains(dst.host()?),没命中就按 scheme 返回代理。没有任何一行检查目标是不是 127.0.0.1 或 localhost。

NO_PROXY 按 URL 里的 host 字面匹配。 NoProxy::contains 先试着把 host 解析成 IP:是 IP 就只查 IP 规则(地址或网段),不是就只查域名规则。不做 DNS,所以 localhost 这条域名规则对 http://127.0.0.1 不生效,127.0.0.1 这条 IP 规则对 http://localhost 也不生效。

报错之所以看不出代理:连接器连的是代理,hyper 在这条连接上写请求、等响应,代理一挂断,hyper 看到的就是「响应还没完连接就关了」。代理工具对回环地址怎么处理各不相同,有的转发、有的丢掉;丢掉时就是上面的报错。

修法

测试和本机调用:.no_proxy()。 它清空代理表并关掉系统代理,这个 Client 一律直连,不受机器上任何代理设置的影响:

let client = reqwest::Client::builder().no_proxy().build()?;

生产代码要保留系统代理、只放过本机: 把 IP 和名字都写进 NO_PROXY,用网段覆盖整个回环:

NO_PROXY=127.0.0.0/8,::1,localhost

上面最后一组 127.0.0.0/8,localhost 两种写法都直连了。macOS 系统设置里的例外列表对 reqwest 无效,不要指望它。

只给某些主机走代理: 显式写 Proxy::all(...) 加 .no_proxy(NoProxy::from_string(...))。注意一条显式的 .proxy(...) 会让系统代理整个不再读取。

怎么检查

  • 看环境: env | grep -i proxy,macOS 上再 scutil --proxy。只要有任何一个代理开着,用默认 Client 访问本机就要想到这个坑。

  • 看连的是谁: RUST_LOG=hyper_util=debug 下,连接日志里的端口是代理的,不是目标的。同一个实验,代理在 53980、目标在 53979:

    DEBUG hyper_util::client::legacy::connect::http: connecting to 127.0.0.1:53980
    DEBUG hyper_util::client::legacy::connect::http: connected to 127.0.0.1:53980
      Client::new()         127.0.0.1 via proxy -> error sending request for url (http://127.0.0.1:53979/)
    
  • 一个请求在 shell 里用 curl 能通、在 Rust 里不通,两者读的代理配置不一定相同;先换成 .no_proxy() 试一次,通了就是代理的问题。

小结

  • 默认 Client 读代理环境变量和 macOS 系统代理;127.0.0.1、localhost 没有任何特殊待遇。
  • macOS 系统设置里的例外列表不读,排除规则只有 NO_PROXY;它按 URL 里的 host 字面匹配,localhost 管不到 127.0.0.1。
  • 代理丢掉请求时,明文报 connection closed before message completed,HTTPS 报 tunnel error: unexpected end of file,错误里不提代理。
  • 测试和本机调用用 .no_proxy();要保留系统代理就把 127.0.0.0/8,::1,localhost 都写进 NO_PROXY。

💬 关联讨论

发起讨论
加载中...