Rust in Everything
基于 hyper-util 0.1.21

SOCKS error: failed to create underlying connection

主线位置:客户端主线上建连接的一步。SocksV5 / SocksV4 先用内层连接器连到代理,这一步失败时报这句话;SOCKS 握手根本还没开始。

flowchart TB
  call["SocksV5::call(dst)"] --> inner["内层 HttpConnector::call(socks5://代理)"]
  inner --> chk{"enforce_http?"}
  chk -->|"开着(默认)"| e1["ConnectError:invalid URL, scheme is not http"]
  chk -->|关掉| port{"代理 URI 有端口?"}
  port -->|没有| e2["按 80 连:Connection refused"]
  port -->|有| ok["连上代理,开始 SOCKS 握手"]
  e1 --> wrap["SocksError::Inner"]
  e2 --> wrap
  wrap --> show["Display:failed to create underlying connection;source() 为空"]

复现

完整程序和代理的写法见 代理连接器 的「先跑一遍」。这里只看两种写法:

// A: the obvious way
let c = Client::builder(TokioExecutor::new())
  .build(SocksV5::new("socks5://127.0.0.1:1080".parse().unwrap(), HttpConnector::new()));

// B2: enforce_http turned off, but the proxy URI has no port
let mut hc = HttpConnector::new();
hc.enforce_http(false);
let c = Client::builder(TokioExecutor::new())
  .build(SocksV5::new("socks5://127.0.0.1".parse().unwrap(), hc));

请求 http://example.test/,分别打印 {}、沿 source() 走的链、{:?}:

A: SocksV5 over default HttpConnector (enforce_http=true)
  err: client error (Connect) | chain: client error (Connect) <- SOCKS error: failed to create underlying connection | debug: hyper_util::client::legacy::Error(Connect, Inner(ConnectError("invalid URL, scheme is not http")))
B2: proxy uri without port socks5://127.0.0.1
  err: client error (Connect) <- SOCKS error: failed to create underlying connection | debug hyper_util::client::legacy::Error(Connect, Inner(ConnectError("tcp connect error", 127.0.0.1:80, Os { code: 61, kind: ConnectionRefused, message: "Connection refused" })))

两次的 Display 链一模一样,真正的原因(scheme 被拒、去连了 80 端口)只在 Debug 里。

在哪里产生

SocksV5::call 先把代理的 URI 交给内层连接器,内层的错误原样包进 SocksError::Inner(v5/mod.rs 第 270 行):

            let conn = connecting.await.map_err(SocksError::Inner)?;
            config.execute(conn, host, port).await

Display 对 Inner 只写一句固定的话,第 85 行:

            Self::Inner(_) => f.write_str("failed to create underlying connection"),

而 impl Error for SocksError 是空的,没有 source(),链就断在这里。legacy Client 再在外面包一层 client error (Connect)。

内层为什么失败,两种情况都在 HttpConnector 里:

  • enforce_http 默认为真,HttpConnector 只接受 http:// 的 URI,碰到 socks5:// 直接报 invalid URL, scheme is not http。TLS 连接器包 HttpConnector 时会关掉它,SOCKS 连接器不会替你关。
  • URI 没写端口时,HttpConnector 按 scheme 补默认端口:https 是 443,其他都是 80。socks5 不是 https,于是连 80,而不是 SOCKS 惯用的 1080。

Tunnel 不受第一条影响:HTTP 代理的 URI 本来就是 http://。

修法

三件事一起做:

let mut hc = HttpConnector::new();
hc.enforce_http(false);                                   // let the inner connector accept socks5://
let socks = SocksV5::new("socks5://127.0.0.1:1080".parse()?, hc); // always write the proxy port
let client = Client::builder(TokioExecutor::new()).build(socks);

// and log errors with {:?}, not {}
if let Err(e) = client.request(req).await {
  tracing::warn!(error = ?e, "request through SOCKS failed");
}

前两条改完,同一个请求成功(代理连接器 里的 B):

B: enforce_http(false), target http://example.test/ (no port)
  socks: greeting [5, 1, 0]
  socks: CONNECT host=example.test port=443
  ok 200 OK

注意 SOCKS 请求里的目标端口是 443:目标 URI 没写端口时,SocksV5 不看 scheme,一律补 443(第 263 行)。上面的假代理不在乎,真代理会把明文 HTTP 送到目标的 443 端口上。所以第四件事:目标 URI 也把端口写全,http://example.test:80/。

reqwest 用户不会遇到这几件事:reqwest 用自己的 HttpConnector,自己补代理端口 1080 和目标端口,见 reqwest 卷的 代理。但 reqwest 外面看到的同样是 error connecting to socks proxy <- SOCKS error: ...,SocksError 里面的原因同样只在 {:?} 里。

小结

  • failed to create underlying connection 说明还没连上代理,SOCKS 握手没有开始。
  • 常见原因:内层 HttpConnector 的 enforce_http 拒绝 socks5://;代理 URI 没写端口,被按 80 连。
  • SocksError 没有 source(),{} 和沿链打印都看不到原因,用 {:?}。
  • 修法:enforce_http(false),代理 URI 和目标 URI 都写全端口。

💬 关联讨论

发起讨论
加载中...