正向代理

本页目录

正向代理代表客户端访问目标。客户端先决定某次连接或请求是否交给代理,再由代理按目标和策略转发。配置了一个代理地址,不意味着所有程序、DNS、UDP或系统流量自动经过它。

理解代理路径时要同时追踪三个问题:谁选择代理,谁解析目标域名,以及每一段是否加密。只看最后目标IP或一个浏览器“代理已连接”提示,无法回答这些问题。

场景客户端向代理发送什么代理的主要工作
普通HTTP代理请求带absolute-form目标的HTTP请求解析HTTP并转发
HTTP CONNECT主机与端口建立字节隧道
SOCKS5 CONNECT二进制目标地址与端口建立TCP中继
SOCKS5 UDP ASSOCIATE控制关联,随后UDP封装按数据报目标中继UDP
PAC在本地执行选择规则选择DIRECT或代理候选,不承载流量

HTTP 请求代理与 CONNECT

向普通HTTP代理请求明文站点时,请求目标通常形如GET http://example.com/index.html HTTP/1.1;代理能读取和处理HTTP。访问HTTPS目标时,客户端常先发送CONNECT example.com:443 HTTP/1.1及相应Host,收到成功2xx后,在隧道中与目标进行TLS握手。

CONNECT 建立字节隧道;TLS 是否存在由隧道内协议决定 客户端 先连接代理并提交目标 代理 按策略连接目标 目标服务器 接受代理发起的连接 HTTPS 场景:客户端 ↔ 目标的 TLS 穿过隧道 代理仍可见 CONNECT 目标与流量元数据 客户端到代理、代理到目标通常是两条 TCP 连接,并非同一个端到端 TCP 序号空间。

CONNECT成功后的字节不再按该请求的普通HTTP消息体解析。通常客户端到代理、代理到目标是两条独立TCP连接;代理拼接的是字节传输能力,不是把一条TCP连接原封不动延长到服务器。

CONNECT本身不提供加密,也不只限HTTPS。代理可以限制目标端口和目的地,客户端不能把成功连接代理等同于允许访问任何地址。代理认证失败通常对应407与Proxy-Authenticate,目标服务认证的401是另一层。RFC 9110:CONNECT

HTTPS 代理与 HTTPS 目标是两件事

http://proxy.example:8080和https://proxy.example:8443描述客户端到代理的传输方式;目标URL的https://描述目标服务使用TLS。HTTPS代理上再CONNECT到HTTPS目标,可以形成外层代理TLS与内层目标TLS,两者认证对象不同。

不终止内层TLS的代理看不到目标HTTP内容,但仍知道显式CONNECT目标、时间和流量规模。ECH可改变内层ClientHello的可见内容,却不会抹去已发给该代理的CONNECT目标。若客户端信任代理签发的替代证书并允许TLS检查,则实际身份与保密边界已经改变。

HTTP/2、HTTP/3也可承载相应CONNECT机制,不能把所有代理连接都假设为一个HTTP/1.1连接只建一个隧道。普通CONNECT的TCP字节隧道与CONNECT-UDP是不同能力;后者有专门的UDP代理协议约定。RFC 9298

SOCKS5:方法选择、认证与命令不能混作 TCP 握手

首先建立到代理的TCP连接,再协商可用认证方法;选择用户名/密码等方法时还有子协商,之后发送CONNECT、BIND或UDP ASSOCIATE命令。不能把这些阶段统称为固定“三次握手”。

字段或值含义
VER=5SOCKS版本
METHOD=0x00 / 0x02无认证 / 用户名密码方法
METHOD=0xff无可接受方法,关闭连接
CMD=1 / 2 / 3CONNECT / BIND / UDP ASSOCIATE
ATYP=1 / 3 / 4IPv4 / 域名 / IPv6
REP=0命令成功,之后再按相应语义传输

域名地址包含1 B长度与名称字节,没有末尾NUL;端口用2 B网络序。CONNECT回复的BND.ADDR/PORT是代理对目标连接使用的绑定端点,不是把DST原样回显。RFC 1928

用户名密码子协商自己的版本字段为1,不能写成SOCKS主协议版本5。该方法直接传输凭据字节,不自动提供加密;安全性取决于外层传输和部署边界。RFC 1929

完整地址算例:域名 CONNECT 与 IPv4 UDP 头

import ipaddress

name = b'example.com'
request = bytes([5, 1, 0, 3, len(name)]) + name + (443).to_bytes(2, 'big')
assert request.hex() == '050100030b6578616d706c652e636f6d01bb'
assert len(request) == 18
n = request[4]
assert request[5:5+n].decode('ascii') == 'example.com'
assert int.from_bytes(request[5+n:7+n], 'big') == 443
# 无SOCKS分片,IPv4目标。这里DATA只是教学字节,不是实际DNS报文。
udp_head = bytes([0, 0, 0, 1]) + ipaddress.ip_address('192.0.2.53').packed + (53).to_bytes(2, 'big')
packet = udp_head + b'DATA'
assert len(udp_head) == 10 and len(packet) == 14
assert udp_head.hex() == '00000001c00002350035'
assert int.from_bytes(packet[8:10], 'big') == 53
print('CONNECT 请求 / UDP封装头:', len(request), len(udp_head))

该算例只检查编码,不实现代理连接、认证或中继。SOCKS5不提供通用IP层转发,例如ICMP不是直接通过CONNECT发送的TCP内容。

UDP ASSOCIATE:控制连接不是 UDP-over-TCP

SOCKS5 UDP:TCP 控制关联与 UDP 数据通路同时存在 客户端 SOCKS5代理 UDP目标 TCP:UDP ASSOCIATE / 返回 relay 地址 UDP:SOCKS头 + 应用数据 UDP:应用数据 UDP:响应数据 UDP:SOCKS头 + 响应数据 关联依赖 TCP 控制连接存活;UDP 数据不因为有这条 TCP 连接就获得可靠传输。

客户端通过TCP命令建立关联,命令中的地址端口描述预计使用的客户端UDP端点,未知时可填零。代理回复可用relay端点,客户端随后把每个UDP数据报加SOCKS头发给relay,头中再指定实际目标;不要把关联请求的零地址当每个数据报的最终目的地。

返回数据同样由relay加SOCKS头发给客户端,用于标识远端来源。关联在TCP控制连接关闭时终止;UDP仍可能丢失、重排,TCP控制通道没有替它重传。FRAG=0表示未分片,SOCKS层分片支持可选,不支持的实现丢弃非零FRAG报文;这也不同于IP分片。

IPv4目标的SOCKS UDP头为10 B,IPv6目标为22 B,域名目标为7+名称字节数,再加外层UDP/IP开销。TCP代理能连通不证明UDP relay可达,客户端应用也可能根本不实现UDP ASSOCIATE。

DNS 在哪一侧:地址类型与客户端实现共同决定

客户端先本地解析并发送IP时,代理无法替它撤销已经发生的本地DNS查询。发送ATYP=域名时,则交给代理侧按自己的策略解析或路由;不代表代理必定使用某个固定公共DNS。

curl区分socks5://的本地解析与socks5h://的代理侧解析。以下仅供具备对应本地代理时手动观察,执行会发起真实目标请求:

curl --proxy socks5h://127.0.0.1:1080 --connect-timeout 5 https://example.com/

--connect-timeout也不是整个下载的总时限。代理主机本身的发现、应用预解析、DoH以及其他未使用此代理的进程仍需分别检查;不能只凭一条curl成功就断言整个系统没有本地DNS流量。curl代理选项

把域名改成IP,改变的不只是报文长度

两条路径最终都可以访问同一目标,但代理接收到的证据不同:IP路径没有原域名,域名路径则有长度字节和名称本身。这里展示一次命令的责任边界,不据此宣布系统所有DNS都经过代理。

正在呈现知识画面
把域名改成IP,改变的不只是报文长度

ATYP决定请求携带名称还是地址。本地已做的DNS查询不会因之后进入加密隧道而消失。

PAC:按目标选路线,DIRECT 是显式回退

PAC的FindProxyForURL返回代理候选字符串。它是受限环境中的JavaScript,不是可任意访问浏览器页面状态的普通脚本。域名规则常比调用DNS辅助函数更容易预测;isInNet等可能引发本地解析,恰好与希望避免本地解析的意图冲突。

function FindProxyForURL(url, host) {
    // 只把明确受控的内部域名交给直连,不按国家后缀猜服务器位置。
    host = host.toLowerCase();
    if (host === 'intranet.example' || host.slice(-17) === '.intranet.example') {
        return 'DIRECT';
    }
    return 'PROXY 127.0.0.1:7890; SOCKS5 127.0.0.1:1080';
}

实现所支持的PAC语法和辅助函数需按客户端核实。示例最后没有DIRECT,意味着没有明确允许“代理失败就直接访问”的候选。若追加DIRECT,就是改变了失败时的出口策略,不能把它当无害的固定模板。

PAC并不保证每次请求都完整重执行,也不能假设HTTPS URL始终包含路径。以Chromium为例,PAC可见URL有隐私裁剪、代理选择有缓存和失败处理;它的SOCKS5实现也有自己的能力范围。域名后缀.cn不证明目标物理位置,.local还涉及本地名称解析语义,不宜拿来粗略代表全部内网。Chromium代理说明

定位失败:按阶段记录证据

阶段失败信号先确认
代理选择未经过预期代理应用是否读取设置、PAC/绕过规则
到代理的连接超时、拒绝连接地址、监听、网络与外层TLS
代理认证407、SOCKS方法拒绝方法和凭据所处层次
目标建立CONNECT非2xx、SOCKS REP失败代理策略、目标解析与出口
内层TLS名称或链失败实际目标身份与信任材料
业务HTTP401、403、5xx目标服务的认证和业务处理
UDP通路TCP成功而UDP无返回关联、relay可达性、封装与应用支持

系统代理、环境变量、应用内配置可能互不覆盖;代理链里也可能有不同认证和解析位置。把路线和错误阶段分开,才能避免“所有问题都换一个DNS”或“所有流量都已经被接管”的误判。

继续查阅