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 都写全端口。