socks5h:// 里的「h」表示让客户端把网站域名交给代理去解析,而不是先在本地把域名解析成 IP。没有它时,DNS 解析会从你的机器发出,走你自己的解析器,对方就能知道你通过代理打开了哪些网站。
不过,协议前缀只说明了一半情况,因为纯 socks5:// 到底会怎么做,取决于客户端本身。我们在 2026 年 10 月把 22 个 HTTP 客户端、库和浏览器接到自建的 SOCKS5 服务器上测试,每个都跑三轮,并记录它们发送的内容。22 个里有 8 个在使用 socks5:// 时会在本地解析域名。其余 14 个,包括我们测试过的所有浏览器,即使没有「h」也会把域名直接交给代理。
简短结论
- curl、PHP cURL、Python requests 和 urllib3、Node.js 的 socks-proxy-agent(搭配 http、axios 或 got)以及 proxy-chain 中继在使用
socks5://时会泄露 DNS。 把它们改成socks5h://后,这些工具里的泄露都会消失。 - Python httpx、aiohttp-socks、Go net/http 以及 Node 的 undici 搭配 fetch-socks 在使用普通
socks5://时会把域名发送给代理。 aiohttp-socks 完全不接受socks5h://,会抛出 ValueError 并失败。 - Chrome、Chromium、Puppeteer、Playwright、Firefox 和 Camoufox 都没有泄露。 在 Firefox 里,只有当你关闭
network.proxy.socks5_remote_dns时才会出现泄露。 - proxychains-ng 在默认配置下是安全的,默认启用了
proxy_dns。删掉这一行就会泄露。
如何修复:把域名交给代理解析的设置
下面每段示例都在我们的测试服务器上用用户名和密码验证过,并且在所有情况下,代理端收到的都是网站域名而不是 IP。请把 user:pass@host:port 替换成你的代理信息。
curl
curl -x socks5h://user:pass@host:port https://example.com
curl --socks5-hostname host:port --proxy-user user:pass https://example.com
export ALL_PROXY=socks5h://user:pass@host:port
PHP cURL
$ch = curl_init("https://example.com");
curl_setopt($ch, CURLOPT_PROXY, "host:port");
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
curl_setopt($ch, CURLOPT_PROXYUSERPWD, "user:pass");
也可以使用代理 URL,比如把 CURLOPT_PROXY 设为 socks5h://user:pass@host:port。
Python requests 和 urllib3
pip install "requests[socks]"
import requests
proxy = "socks5h://user:pass@host:port"
requests.get("https://example.com", proxies={"http": proxy, "https": proxy})
from urllib3.contrib.socks import SOCKSProxyManager
http = SOCKSProxyManager("socks5h://user:pass@host:port")
Python httpx 和 aiohttp
这两个在使用 socks5:// 时就已经会把域名交给代理了,这里保持这个前缀即可,因为 aiohttp-socks 在 socks5h:// 上会报错。
pip install "httpx[socks]" aiohttp-socks
import httpx
client = httpx.Client(proxy="socks5://user:pass@host:port")
from aiohttp_socks import ProxyConnector
connector = ProxyConnector.from_url("socks5://user:pass@host:port")
# aiohttp.ClientSession(connector=connector)
Node.js:axios、got、http
import axios from "axios"
import got from "got"
import { SocksProxyAgent } from "socks-proxy-agent"
const agent = new SocksProxyAgent("socks5h://user:pass@host:port")
await axios.get("https://example.com", { httpAgent: agent, httpsAgent: agent, proxy: false })
await got("https://example.com", { agent: { http: agent, https: agent } })
通过 proxy-chain 给 Puppeteer 或 Playwright 使用带登录的 SOCKS5
import puppeteer from "puppeteer"
import { anonymizeProxy, closeAnonymizedProxy } from "proxy-chain"
const proxyUrl = await anonymizeProxy("socks5h://user:pass@host:port")
const browser = await puppeteer.launch({ args: [`--proxy-server=${proxyUrl}`] })
// ...
await browser.close()
await closeAnonymizedProxy(proxyUrl, true)
Firefox
默认是开启的。如果某个配置文件把它关掉了,请在 about:config 或该配置文件的 user.js 中改回去:
user_pref("network.proxy.socks5_remote_dns", true);
proxychains-ng
确认配置中包含 proxy_dns,并且没有被注释掉:
strict_chain
proxy_dns
[ProxyList]
socks5 host port user pass
Go 与浏览器
无需修改。在我们的测试中,Go 的 net/http、Chrome、Chromium、不使用中继的 Puppeteer、Playwright 和 Camoufox 使用普通 socks5:// 也会把域名发给代理。
「h」到底改变了什么
客户端让 SOCKS5 代理建立连接时,有两种方式指定目标地址:使用 IP 地址或使用域名(RFC 1928 把它们称为地址类型)。用域名时,代理会在它那边解析地址。用 IP 时,域名解析已经在你本机完成,并且用的是你网络的 DNS 解析器。
socks5h 是 curl 用来表达「发送域名」的写法,Python 和 Node 的一些库沿用了这一约定。两者在传输层上都是同一种 SOCKS5 协议,差别只是那次请求里使用的地址类型不同。
我们如何测试
我们写了一个小型 SOCKS5 服务器,用来记录每个客户端在连接请求里带的是域名还是 IP。每次测试都会打开一个独一无二的主机名,格式为 <test>-<random>.127.0.0.1.sslip.io。我们的服务器会直接从这个主机名读出目标地址,不做任何 DNS 解析,因此抓包里如果出现对该名称的 DNS 查询,只可能来自客户端本身。
每个客户端每轮打开一次页面,连续三轮,于 2026 年 10 月 21 日完成。每个客户端在三轮里的结果都完全一致。

这些泄露出来的查询并没有止步于本机。curl 使用普通 socks5:// 时,针对测试域名的 A 与 AAAA 查询会发往我们托管服务商的 DNS 解析器,这一方并不是代理,也没有理由知道你通过代理访问了哪些站点。

结果:谁会把域名发给代理
| 客户端 | 我们的设置 | 发送给代理的内容 | 本地 DNS 解析 |
|---|---|---|---|
| curl | -x socks5://、--socks5、ALL_PROXY=socks5:// | IP | 是 |
| curl | -x socks5h://、--socks5-hostname | 域名 | 否 |
| PHP cURL | CURLOPT_PROXY socks5://、CURLPROXY_SOCKS5 | IP | 是 |
| PHP cURL | CURLOPT_PROXY socks5h://、CURLPROXY_SOCKS5_HOSTNAME | 域名 | 否 |
| Python requests | socks5:// | IP | 是 |
| Python requests | socks5h:// | 域名 | 否 |
| Python urllib3 | socks5:// | IP | 是 |
| Python urllib3 | socks5h:// | 域名 | 否 |
| Python httpx | socks5:// 或 socks5h:// | 域名 | 否 |
| Python aiohttp + aiohttp-socks | socks5:// | 域名 | 否 |
| Python aiohttp + aiohttp-socks | socks5h:// | 错误:不接受该 scheme | |
| Go net/http | socks5:// 或 socks5h:// | 域名 | 否 |
| Node.js http、axios、got + socks-proxy-agent | socks5:// | IP | 是 |
| Node.js http、axios、got + socks-proxy-agent | socks5h:// | 域名 | 否 |
| Node.js undici + fetch-socks | type 5 | 域名 | 否 |
| proxy-chain(Apify) | 带登录的 socks5:// | IP | 是 |
| proxy-chain(Apify) | 带登录的 socks5h:// | 域名 | 否 |
| proxychains-ng | 默认配置(proxy_dns) | 域名 | 否 |
| proxychains-ng | 不含 proxy_dns | IP | 是 |
| Google Chrome | --proxy-server=socks5:// | 域名 | 否 |
| Chromium、Puppeteer | --proxy-server=socks5:// | 域名 | 否 |
| Playwright:Chromium、Firefox、WebKit | proxy: { server: 'socks5://…' } | 域名 | 否 |
| Firefox、Firefox ESR | 连接设置里选择 SOCKS v5 | 域名 | 否 |
| Firefox | network.proxy.socks5_remote_dns = false | IP | 是 |
| Camoufox | proxy: socks5:// | 域名 | 否 |
版本:curl 8.5.0,PHP 8.5.12(libcurl 8.22.0),Python 3.15(requests 2.34.2、urllib3 2.8.0、httpx 0.28.1、aiohttp 3.14.5、aiohttp-socks 0.12.0),Go 1.27.3,Node.js 24.22(socks-proxy-agent 10.1.0、axios 1.20.0、got 16.1.0、undici 8.12.3、fetch-socks 1.3.3、proxy-chain 3.0.1),proxychains-ng 4.17,Google Chrome 156,Firefox 158 与 Firefox ESR 153,Playwright 1.62.1,puppeteer-core 25.15.0,Camoufox 0.5.8。
curl 以及所有基于 libcurl 的工具
curl 是 socks5h 的来源,也是最直观的例子。-x socks5://、--socks5 以及环境变量 ALL_PROXY=socks5://… 都会让 curl 自己解析域名,并把 IP 发给代理。-x socks5h:// 与 --socks5-hostname 则会发送域名。
PHP 的 cURL 扩展使用同一套库,行为也一致。在 PHP 代码里,代理类型通常用常量而不是 URL 来设置,所以修复方式是用 CURLPROXY_SOCKS5_HOSTNAME 替代 CURLPROXY_SOCKS5。
Python:取决于具体库
requests 和 urllib3 通过 PySocks 工作,并遵循 curl 的惯例:socks5:// 每次都会在本地解析,socks5h:// 会发送域名。这是爬虫代码中最常见的组合,也是大多数教程照搬的写法。
httpx 与 aiohttp-socks 即使使用 socks5:// 也会发送域名。反而要注意另一个方向的问题,aiohttp-socks 会拒绝 socks5h://,并报错 ValueError: Invalid scheme component,因此对 requests 正确的代理字符串,可能会让 aiohttp 爬虫直接崩掉。在 aiohttp-socks 里继续使用 socks5:// 即可,因为它本来就会在代理端解析。
Go 与 Node.js
Go 的 net/http 在使用 socks5:// 和 socks5h:// 时都会发送域名,所以 Go 程序无论是从 URL 还是从 HTTP_PROXY 读取代理,都同样安全。
在 Node.js 里,socks-proxy-agent 是给内置 http 模块、axios 或 got 配置 SOCKS5 代理的常见方案,它也遵循 curl:socks5:// 会在本地解析,socks5h:// 会发送域名。undici 搭配 fetch-socks 则不同,它以对象而不是 URL 的形式接收代理,会发送域名。
浏览器没有泄露
我们测试的每一款浏览器都会把域名发送给代理,包括使用 --proxy-server=socks5:// 的 Google Chrome 与 Chromium、Puppeteer、Playwright 的三种引擎、在连接设置里选择 SOCKS v5 的 Firefox 与 Firefox ESR,以及 Camoufox。通过 Puppeteer 驱动的 Chromium 也会把自身的后台请求,例如访问 Google 账号相关服务的请求,也以域名的方式走代理。
Firefox 有两个名字很像的设置,很多指南至今仍在指向错误的那个。network.proxy.socks_remote_dns 对 SOCKS5 已经不起作用了,我们把它设为 false,Firefox 仍然会发送域名。真正关键的是 network.proxy.socks5_remote_dns,默认开启。我们能让 Firefox 出现泄露的唯一方式,就是把它关掉。
由 Playwright 和 Camoufox 驱动的浏览器不接受带用户名和密码的 SOCKS5 代理,启动会失败,并提示「Browser does not support socks5 proxy authentication」。大多数轮换住宅代理和移动代理都需要登录,这就引出了下一个问题。
陷阱:给带密码的 SOCKS5 加一个中继
绕过这一限制的常见做法,是在本地跑一个中继,让浏览器连接到一个不需要密码的端口,然后由中继再转发到需要账号密码的 SOCKS5 代理。Puppeteer 与 Playwright 常用的选择是 Apify 的 proxy-chain。
对于 socks5://user:pass@host:port,proxy-chain 会自己解析域名,并把 IP 传给代理,尽管它后面的浏览器发送的是域名。对于 socks5h://user:pass@host:port,它会转发域名。所以,一个本来不会泄露的浏览器,只要前面加了这个中继并且 scheme 写错,就会开始泄露。
proxychains-ng
proxychains-ng 能把任意程序强制走代理,它的默认配置会启用 proxy_dns,因此域名会交给代理。某些从旧教程抄来的配置会漏掉这一行,或把它注释掉。没有它时,proxychains 后面的程序会在本地解析域名。
为什么本地解析很重要
本地解析会使用你自己网络的 DNS 解析器,因此该解析器能看到你通过代理访问的每一个站点,DNS 泄露测试也会把你的真实解析器与代理 IP 一并显示出来。
解析还会从你所在的位置发起,CDN 后面的网站可能会给你的机器返回离你更近而不是离代理更近的服务器地址,于是请求会被送到为错误国家或地区选择的服务器。
如何检查你自己的配置
对 curl 来说,可以对比用 -x socks5:// 与 -x socks5h:// 打开 DNS 泄露测试页面时的结果,它列出的解析器应该不同。对某个库来说,更可靠的检查方式是我们用的那种方法,把客户端指向你自己控制的 SOCKS5 服务器,记录连接请求里携带的是域名还是 IP。如果你只需要查看代理的出口 IP 与位置,我们的代理检测工具可以在浏览器里完成。
关于 SOCKS5 与 HTTP 代理的其他差异,包括速度、UDP 与 WebRTC,请查看我们对 11 个真实代理做的SOCKS5 与 HTTPS 代理对比测试。
FAQ
- socks5 和 socks5h 有什么区别?
- 使用 socks5h 时,客户端会把网站域名发给代理,由代理负责解析地址。使用 socks5 时,curl 以及遵循其约定的库会在本机解析地址,只把 IP 发给代理,因此你的 DNS 解析器会看到你访问的每一个站点。也有一些客户端即使用 socks5 也会发送域名,我们测试的 22 个客户端里包括 httpx、aiohttp-socks、Go、配合 fetch-socks 的 undici,以及所有浏览器。
- socks5:// 会泄露 DNS 吗?
- 我们测试的 22 个客户端里有 8 个会,包括 curl、PHP cURL、Python requests 和 urllib3、Node.js 的 socks-proxy-agent 搭配 http,以及 axios 和 got,还有 proxy-chain 中继。在这些情况下,改用 socks5h:// 都能避免泄露。
- socks5h 是不同的协议吗?
- 不是,它仍然是同一个 SOCKS5。h 只是提示客户端在连接请求里带上域名而不是 IP 地址。任何支持域名的 SOCKS5 代理都可以搭配使用。
- 浏览器通过 SOCKS5 代理会泄露 DNS 吗?
- 在我们的测试中不会。Google Chrome、Chromium、Puppeteer、Playwright 的 Chromium、Firefox 和 WebKit、Firefox、Firefox ESR 以及 Camoufox 都会把域名发送给代理。只有在我们关闭 network.proxy.socks5_remote_dns 之后,Firefox 才会发生泄露。
- Firefox 中哪个设置控制 SOCKS5 的远程 DNS?
- network.proxy.socks5_remote_dns,默认开启。许多教程提到的旧设置 network.proxy.socks_remote_dns 在 Firefox 158 和 Firefox ESR 153 中对 SOCKS5 没有任何作用。
- 为什么 aiohttp 会拒绝 socks5h://?
- aiohttp-socks 不识别 socks5h 这个 scheme,会抛出 ValueError: Invalid scheme component。使用它时请用 socks5://,在我们的测试中它本来就会把域名发送给代理。
- 在 Puppeteer 或 Playwright 中,如何在使用带密码的 SOCKS5 代理时避免 DNS 泄露?
- Playwright 下的 Chromium、Firefox 和 WebKit 都不支持 SOCKS5 登录,所以很多人会在前面加一个本地中继,比如 proxy-chain。给中继配置 socks5h://user:pass@host:port。使用 socks5:// 时,proxy-chain 会自行解析域名,并把 IP 地址发给上游代理。