「浏览器打得开外部站点,只有业务应用到不了外部 API。」「开发机没问题,到客户网络就超时。」「手动运行会通信,一做成 Windows 服务就失败。」── 在有企业内部代理的环境跑业务应用时,这类咨询最常见。
多数情况下,原因既不是代理服务器故障,也不是应用错误。Windows 有好几套彼此分开、人们都叫「代理设置」的谱系,谁读哪一套会依应用(它用的 HTTP 栈)与运行帐户而不同── 那就是不一致。浏览器读的设置、服务读的设置、.NET HttpClient 读的设置,可以各自是不同的东西。结构一旦进了脑子,「浏览器可以,但是……」的隔离会意外地快。
本文面向中小企业 IT 人员与 Windows 应用开发者,把代理设置的三个谱系──WinINET、WinHTTP、环境变量──以及 PAC 与 WPAD 自动配置、.NET Framework 与 .NET(Core 及以后)的代理解析差异、需认证的代理(407)、TLS 检查与实务隔离步骤,收成同一张图。HttpClient 的创建模式与超时设计本身见「不要用 using 包裹 HttpClient」,本文专注于 代理解析。
1. 先讲结论
- Windows 的代理设置不是一套,至少有三个谱系。 (1) WinINET 每用户设置(设置应用的「代理」页 = 旧的 Internet 选项),(2) WinHTTP 计算机设置(
netsh winhttp),(3)HTTP_PROXY/HTTPS_PROXY环境变量。读哪一套由应用端决定。12 - 设置应用里看到的「代理」是 WinINET 的每用户设置。 浏览器与交互式应用会读;Windows 服务不会。WinINET 不支持在服务中使用,服务用途是 WinHTTP 的工作。13
- 「手动可以、做成服务就不行」最常见的原因是运行帐户不同。 LocalSystem 与服务帐户看不到管理员在自己屏幕上配好的每用户代理。34
netsh winhttp set proxy是静态设置,不处理 PAC、自动检测或代理认证。 若要按计算机配置 PAC 或 WPAD,需要netsh winhttp set advproxy这一侧。42- PAC 结果依 URL 而变。 PAC 文件的
FindProxyForURL函数接受 URL 与主机,返回代理列表或直接连接(DIRECT)。「那个站点可以,只有这个 API 不行」可能是 PAC 分支。56 - .NET(Core 及以后)的 HttpClient 以环境变量 → Windows 用户代理设置的顺序初始化默认代理。 只要定义了
HTTP_PROXY、HTTPS_PROXY或ALL_PROXY任一项,就优先于操作系统设置,因此会发生「有人把环境变量留下」的事故。7 - .NET Framework 的默认是运行帐户的 Internet 选项,可用 app.config 的
defaultProxy覆盖。 配置文件设置优先于系统设置。89 - 407 是代理认证错误,与 401(服务器认证)是两回事。 方案包含 Negotiate、NTLM、Basic,.NET 用
DefaultProxyCredentials或WebProxy.UseDefaultCredentials传入凭据。请注意,在服务帐户下「默认凭据」的内容会变。101112 - TLS 检查代理必须与内部 CA 证书分发成套才站得住。 没收到的计算机与运行时会得到证书验证错误。用分发到证书存储解决,不要在应用里关掉验证。134
一句话:每次说「我查过代理设置」时,都要能说出查的是三个谱系里的哪一个、从哪个帐户查── 这就是本文的主题。
2. Windows 有三个谱系的「代理设置」
先看整张地图。Windows 应用寻找企业内部代理的路径,落在这三个谱系。
| 设置谱系 | 设置位置 / 命令 | 范围 | 主要谁读 |
|---|---|---|---|
| (1) WinINET(Internet 选项) | 设置 → 网络和 Internet → 代理,inetcpl.cpl |
每用户(默认) | 浏览器、交互式桌面应用、.NET Framework 默认 |
| (2) WinHTTP(计算机设置) | netsh winhttp set proxy / set advproxy |
计算机 | Windows 服务、部分操作系统组件 |
| (3) 环境变量 | HTTP_PROXY / HTTPS_PROXY / ALL_PROXY / NO_PROXY |
进程(依定义位置继承) | Core 及以后的 HttpClient、curl、Node.js 与 Python 等跨平台工具 |
(1) 就是一般人认定的「Windows 代理设置」,实质是 WinINET 配置。历史上是 Internet Explorer 的 Internet 选项,默认按用户存储。4
(2) 是「没有登录用户」这类服务情境的计算机默认值。(3) 主要是来自跨平台世界的工具惯例;在 Windows 上,.NET(Core 及以后)与 curl 等也会读。7
重点是:读哪个谱系由应用端决定,不是由设置端决定。 应用内部用 WinINET 就读 (1);用 WinHTTP 就读 (2)(或应用自己的覆盖);Core 及以后的 .NET 则先 (3) 再 (1)。所以通常不是「代理设置是对的却连不上」,现实是「应用读的谱系,跟你查的那一套不是同一套」。
flowchart TB
accTitle: Windows 代理设置的三个谱系
accDescr: WinINET 是每用户的设置与 Internet 选项,WinHTTP 是经由 netsh 的计算机默认值,环境变量是进程范围。读哪个谱系由应用决定,不是由设置端决定
fam{"哪个谱系?"}
fam --> wininet["WinINET 每用户设置"]
fam --> winhttp["WinHTTP 计算机设置"]
fam --> env["HTTP_PROXY 及其同类"]
wininet -.-> r1["浏览器与桌面应用"]
winhttp -.-> r2["服务与部分操作系统"]
env -.-> r3[".NET Core+ 与 curl"]
图 1: 三个谱系并列。应用选择读哪一套。
若启用组策略「将代理设置设为计算机级(而非用户级)」,可以把 (1) 改成计算机级并套用到每位用户。用 MDM(Intune 等)可用 NetworkProxy CSP 按设备配置。4
3. WinINET 与 WinHTTP ── 交互式应用用、服务用
3.1. 角色差异
WinINET 与 WinHTTP 都是 Windows 内置的 HTTP 客户端栈,但假设的用途不同。
- WinINET:针对交互式桌面应用。它会自动继承用户的 Internet 选项(代理、Cookie、凭据缓存),必要时还能显示凭据输入 UI。不支持在服务或类似服务的进程中使用。1
- WinHTTP:针对服务与服务器端。它支持以服务帐户运行、线程模拟与会话隔离;相对地不共享用户的浏览器设置、Cookie 或凭据。也不显示 UI。3
Microsoft 自己的指引同样清楚:「除非你在服务里、或在需要会话隔离与模拟的类似服务进程里运行,否则使用 WinINET」── 反过来说,若是服务,就用 WinHTTP。1
flowchart TB
accTitle: 交互式应用用 WinINET,服务用 WinHTTP
accDescr: WinINET 继承已登录用户的 Internet 选项,且不支持在服务中使用。WinHTTP 以服务帐户、无 UI 运行,且不共享用户的浏览器设置
q{"交互式桌面应用?"}
q -->|"是"| ie["WinINET"]
q -->|"服务或类似服务"| wh["WinHTTP"]
ie -.-> ieNote["读用户的 Internet 选项"]
wh -.-> whNote["计算机设置,无 UI"]
图 2: 交互式应用用 WinINET。服务用 WinHTTP。
3.2. 基本的 netsh winhttp 操作
WinHTTP 的计算机默认代理用 netsh 操作。2
:: Display the current WinHTTP proxy settings
netsh winhttp show proxy
:: Set a static proxy (with a bypass list)
netsh winhttp set proxy proxy-server="proxy.example.co.jp:8080" bypass-list="*.example.co.jp;<local>"
:: Import the Internet Options (WinINET) settings
netsh winhttp import proxy source=ie
:: Return to the default (DIRECT)
netsh winhttp reset proxy
这里要记住两个限制。
netsh winhttp set proxy是静态设置。 它既不处理代理自动检测,也不处理指定 PAC URL,也不处理代理认证。4import proxy source=ie只复制当下那一刻的静态设置,不会跟随之后 Internet 选项端的变更。需要包含 PAC 或自动检测的计算机级配置时,用netsh winhttp set advproxy配置 JSON 形式的详细设置(Proxy、ProxyBypass、AutoconfigUrl、AutoDetect)。2
3.3. 最常见的陷阱:服务不读用户的 IE 设置
现场最常看到的模式,按时间顺序是这样。
- 开发者在自己的电脑跑工具 → 每用户代理设置 (1) 生效,于是成功
- 生产环境以 LocalSystem 做成常驻的 Windows 服务(Windows 服务的创建与运维)
- 从 LocalSystem 看得到的设置是另一套(每用户设置看不见,WinHTTP 计算机设置未配置 = DIRECT)→ 尝试直接连外部 API 并超时
不是「同一台计算机却不行」;即使同一台计算机,运行帐户不同,看得见的代理设置集合就不同。对即使没有用户登录也要通信的进程,正确做法是以该进程的 HTTP 栈实际会读的形式准备计算机级设置。使用 WinHTTP 的原生应用或 Windows 组件会套用 netsh 的 WinHTTP 设置。4 另一方面,Core 及以后的 HttpClient 不读 WinHTTP 的计算机设置(见第 5 章),因此对 .NET 服务要设系统环境变量(HTTPS_PROXY 等),或从应用设置显式指定 HttpClientHandler.Proxy。
反向事故也会发生。若用 netsh winhttp set proxy 把静态代理烤进在企业网络与外部之间移动的笔记本,公司外连不到那个代理,通信就整段死掉。把计算机静态设置当成针对网络配置不变的服务器的手段。4
flowchart TB
accTitle: 为什么服务看不到用户的 IE 设置
accDescr: 开发者运行会读每用户的 WinINET 设置并成功。以 LocalSystem 那些设置看不见。原生 WinHTTP 应用接着跟随未配置的计算机设置(DIRECT)。.NET Core+ 服务仍使用环境变量或显式的 handler.Proxy,不会改去读 netsh winhttp
dev["以用户手动运行"] --> ok["套用 WinINET 每用户设置"]
svc["LocalSystem 的 Windows 服务"] --> miss["每用户设置看不见"]
miss --> stack{"哪一套 HTTP 栈?"}
stack -->|"WinHTTP"| direct["WinHTTP 未配置 = DIRECT"]
stack -->|".NET Core+"| env["环境变量或 handler.Proxy"]
direct --> fail["外部 API 超时"]
图 3: 同一台计算机、不同帐户,看得见的代理设置集合就不同。
4. PAC 与 WPAD ── 「自动配置」实际是什么
4.1. PAC 文件与 FindProxyForURL
PAC(Proxy Auto-Configuration)文件是计算「这个 URL 该用哪个代理」的 JavaScript(ECMAScript),且一定包含名为 FindProxyForURL(url, host) 的函数。函数返回应使用的代理列表,或表示可以不经代理直接连接的特殊返回值(DIRECT)。5
function FindProxyForURL(url, host) {
// Internal domains and private addresses go direct
if (dnsDomainIs(host, ".example.co.jp") ||
isInNet(host, "10.0.0.0", "255.0.0.0")) {
return "DIRECT";
}
// Everything else goes through a proxy. Fall back to the next if the first is unavailable
return "PROXY proxy1.example.co.jp:8080; PROXY proxy2.example.co.jp:8080; DIRECT";
}
接着有两个实务后果。
- 代理解析必须按 URL 进行。 因为 PAC 可依 URL(主机)返回不同代理或直接连接,WinHTTP 的自动代理功能也设计成每次传入请求 URL 再查询。6「浏览器看得到另一个站点」并不能证明问题 API 走同一条路。
- DIRECT 是「不经代理走」的指示。 若本应走内部的流量从未出现在代理日志,先怀疑 PAC 回了 DIRECT(或命中绕过列表)。
4.2. 经由 WPAD 的自动检测
打开「自动检测设置」后,机器会用 WPAD(Web Proxy Auto-Discovery)协议寻找 PAC 文件位置。典型配置是 DHCP 发放 PAC URL,或用 DNS 查名为 wpad 的主机,再从 http://wpad/wpad.dat 这类 URL 下载 PAC。14
也就是说,「自动检测」不是魔法,而是只有在 DHCP/DNS 已经备好 WPAD 安排的网络上才会运作的机制。在没有这种安排的网络上只开自动检测,只会多出检测失败的等待时间。
flowchart TB
accTitle: PAC 按 URL 决定代理,WPAD 只找 PAC
accDescr: FindProxyForURL 接受 URL 与主机并返回代理列表或 DIRECT。WPAD 只经 DHCP 或 DNS 找出 PAC。无法评估 PAC 的客户端回落到静态代理或环境变量
url["请求 URL"] --> pac["FindProxyForURL"]
pac -->|"代理列表"| via["经由代理"]
pac -->|"DIRECT"| dir["不经代理连接"]
wpad["经 DHCP 或 DNS 的 WPAD"] -.-> pac
nopac["无法评估 PAC 的客户端"] -.-> fb["静态设置或环境变量"]
图 4: PAC 按 URL 决定。WPAD 只找 PAC 文件。
4.3. 无法评估 PAC 的客户端如何表现
不是每个客户端都能评估 PAC。
netsh winhttp set proxy的静态设置不评估 PAC。4- 使用
HTTP_PROXY环境变量风格的工具,原则上只能写固定代理 URL(没有地方写 PAC URL)。7 - 直接使用 WinHTTP 的原生应用,取决于会话怎么开。Windows 8.1 以后用
WinHttpOpen并指定WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY打开的应用,WinHTTP 会按每个请求自动解析系统/用户代理设置(含 WPAD/PAC)。15 若以较旧的WINHTTP_ACCESS_TYPE_DEFAULT_PROXY(自 8.1 起弃用)等打开,自动代理不会整合进 HTTP 栈,应用必须自己调用WinHttpGetProxyForUrl再把结果套到请求。也就是说,在较旧的实现上,PAC 可以存在却仍未被使用。5
「浏览器经 PAC 走到正确代理,业务应用不读 PAC、尝试直接连接而失败」── 这是另一个经典不一致。在以 PAC 运营的网络上,必须为无法读 PAC 的客户端决定回退──静态设置或环境变量。
5. .NET 的代理解析 ── Framework 与 Core 及以后是两回事
.NET 应用读哪一套代理设置,.NET Framework 与 .NET(Core 及以后)的默认不同。把两者搞混,就会用 Framework 时代的知识去查 .NET 8 应用而漏掉。
5.1. .NET Framework ── 默认是 Internet 选项,用 defaultProxy 覆盖
在 .NET Framework,HttpWebRequest 与坐在它上面的 HttpClient 若未显式指定 Proxy,就使用默认代理。默认代理由系统的 Internet 设置(运行帐户的 WinINET 设置)与配置文件组合决定,且配置文件设置优先。8
可用 app.config(或 machine.config)的 system.net/defaultProxy 元素控制这个默认。9
<configuration>
<system.net>
<!-- useDefaultCredentials: whether to send default credentials to an authenticating proxy -->
<defaultProxy enabled="true" useDefaultCredentials="true">
<proxy usesystemdefault="true"
proxyaddress="http://proxy.example.co.jp:8080"
bypassonlocal="true" />
<bypasslist>
<add address="[a-z]+\.example\.co\.jp$" />
</bypasslist>
</defaultProxy>
</system.net>
</configuration>
让 defaultProxy 元素为空就使用系统(Internet 选项)设置;写上 proxyaddress 等则那些优先。从程序可用 WebRequest.DefaultWebProxy 替换同一个默认。98
第 3.3 节的陷阱这里也适用。因为默认是「运行帐户的 Internet 选项」,以服务帐户运行的 .NET Framework 应用读到的是与管理员桌面看得见的不同(通常是空的)设置集合。
5.2. .NET(Core 及以后)── 环境变量优先,然后才是操作系统用户设置
Core 及以后的 HttpClient 有静态属性 HttpClient.DefaultProxy。除非处理程序显式指定代理,每个 HttpClient 实例都用它。Windows 上的初始化规则是「读环境变量,若未定义则读用户代理设置」。7
使用的环境变量如下。7
| 环境变量 | 含义 |
|---|---|
HTTP_PROXY |
HTTP 请求使用的代理 |
HTTPS_PROXY |
HTTPS 请求使用的代理 |
ALL_PROXY |
上述未定义时的回退 |
NO_PROXY |
不应使用代理的主机,逗号分隔列表 |
三件要注意的事。
- 只要定义了
HTTP_PROXY、HTTPS_PROXY或ALL_PROXY任一项,就优先于操作系统端的代理设置。 只定义NO_PROXY并不会从环境变量构成代理,在 Windows 上会继续使用操作系统用户代理设置。旧实验后把HTTPS_PROXY留成系统环境变量、或 CI/CD 模板注入它,这类「看不见的设置」是事故温床。 NO_PROXY不支持通配符(*)。 要匹配子域,放前导点(.example.com匹配www.example.com,但不匹配example.com本身)。7- 在非 Windows(Linux 容器等),若环境变量未定义,会以没有代理初始化。同一应用在 Windows 与 Linux 之间默认行为改变,是容器迁移时要确认的事。7
5.3. 显式指定 ── HttpClientHandler.Proxy 与 UseProxy
无论哪个运行时,最高优先都是处理程序上的显式指定。指定 HttpClientHandler.Proxy 优先于操作系统设置与配置文件,UseProxy = false 则完全不使用代理。14
using System.Net;
// Use a proxy read from app settings explicitly
var handler = new HttpClientHandler
{
Proxy = new WebProxy("http://proxy.example.co.jp:8080")
{
BypassProxyOnLocal = true,
BypassList = new[] { @"^intra\.example\.co\.jp$" },
UseDefaultCredentials = true // On an authenticating proxy, respond with the running account's credentials
},
UseProxy = true
};
var client = new HttpClient(handler);
// A client that never uses a proxy (for direct internal APIs)
var directHandler = new HttpClientHandler { UseProxy = false };
var directClient = new HttpClient(directHandler);
没有显式指定、跟随操作系统设置时,本地目标的自动绕过有规则。没有点的平面名称、回环地址、符合机器本身域后缀的目标等,可被当成「本地」。14「指定 IP 地址行为就变了」或「改用 FQDN 突然开始走代理」这类现象,可能由这个判断造成。
优先顺序如下。
| 优先(高 → 低) | .NET Framework | .NET(Core 及以后) |
|---|---|---|
| 1 | HttpClientHandler.Proxy 等显式指定 |
相同 |
| 2 | app.config 的 defaultProxy |
对 HttpClient.DefaultProxy 的赋值 |
| 3 | 运行帐户的 Internet 选项 | 环境变量(HTTP_PROXY 等) |
| 4 | — | Windows 用户代理设置 |
flowchart TB
accTitle: Framework 与 Core 及以后的默认代理解析
accDescr: 显式的 HttpClientHandler.Proxy 永远胜出。Framework 接着使用 app.config defaultProxy 与运行帐户的 Internet 选项。Core 及以后使用对 HttpClient.DefaultProxy 的赋值,然后环境变量,然后 Windows 用户代理设置
expl["显式的 handler.Proxy"] --> done["使用该代理"]
noexpl["没有显式 Proxy"] --> fw{"哪个运行时?"}
fw -->|"Framework"| cfg["app.config defaultProxy"]
cfg --> ie["运行帐户 Internet 选项"]
fw -->|"Core 及以后"| dp["HttpClient.DefaultProxy"]
dp --> ev["HTTP_PROXY 及其同类"]
ev --> user["Windows 用户代理设置"]
图 5: 显式指定永远胜出。默认路径依运行时而不同。
6. 需认证的代理 ── 407 是代理的认证错误
6.1. 不要把 407 与 401 搞混
当你尝试通过要求认证的代理时,代理会返回状态码 407(Proxy Authentication Required) 与列出可用方案的 Proxy-Authenticate 标头。那与目标服务器的认证要求(401 与 WWW-Authenticate)是两回事;你交出凭据的对象与配置它们的位置都不同。10
flowchart TB
accTitle: 407 是代理,401 是目标服务器
accDescr: 407 与 Proxy-Authenticate 来自代理。401 与 WWW-Authenticate 来自目标服务器。凭据与配置位置都不同
req["出站请求"] --> who{"谁要求认证?"}
who -->|"代理"| e407["407 + Proxy-Authenticate"]
who -->|"目标"| e401["401 + WWW-Authenticate"]
e407 -.-> cred["DefaultProxyCredentials"]
图 6: 407 是代理认证。401 是服务器认证。
方案包含原样送出用户名与密码的 Basic,以及 Negotiate(Kerberos/NTLM)这类质询/响应方案。质询/响应方案中密码本身不走网络,认证经数次往返完成。10 它「回退」到哪个方案的机制,在「图解 NTLM 与 Kerberos」有更细的说明。
6.2. 在 .NET 如何传入凭据
若要用操作系统设置中的默认代理、只让认证通过,使用 HttpClientHandler.DefaultProxyCredentials。这些是在 UseProxy = true 且 Proxy = null(= 系统默认代理)时,送给该默认代理的凭据。11
using System.Net;
var handler = new HttpClientHandler
{
UseProxy = true, // The default. Combined with a null Proxy, this uses the system-default proxy
Proxy = null,
// Respond to 407 with the credentials of the running account (signed-in user or service account)
DefaultProxyCredentials = CredentialCache.DefaultCredentials
};
var client = new HttpClient(handler);
显式指定代理时,把凭据放在 WebProxy 这一侧。许多客户端情境的建议是使用已登录用户的默认凭据,而不是个别用户名与密码,WebProxy.UseDefaultCredentials = true 就是那个。12
6.3. 服务帐户的 407 问题
运行帐户在这里也很重要。「默认凭据」意指正在运行该进程的帐户的凭据。以交互式用户运行,对代理的认证就是该用户;以 LocalSystem 服务运行,就是计算机帐户。
- 若代理经 Active Directory 认证用户,它无法认证计算机帐户或本地帐户,一做成服务 407 就持续
- 反过来说,有些环境在代理端对服务有认证豁免(按源 IP 或按帐户)
因此 407 调查不能只停在「应用的设置」;它与基础设施端的设计确认成套:代理能否认证运行帐户。对即将做成服务的应用,设计阶段就应决定其一:以域服务帐户(gMSA 等)运行、在代理端放认证豁免,或架设不要求认证的内部中继代理。
也有把凭据嵌进环境变量的风格,如 HTTP_PROXY=http://user:pass@proxy:80807,但明文密码会暴露在环境变量(= 进程信息)里,不建议作为常态运营。
7. HTTPS 与代理 ── CONNECT 隧道与 TLS 检查
7.1. HTTPS 以「隧道」通过代理
对 HTTPS 使用代理时,客户端先向代理送 CONNECT destination-host:443 请求,代理打开 TCP 隧道。成功时代理回 200,之后客户端与目标服务器在该隧道内进行 TLS 握手。若隧道打不开,代理回 407(需要认证)、502 等。16
这个模型里代理读不到隧道内容(加密的 HTTPS)。代理日志留下的是目标主机名与连接是否成功;URL 路径看不见── 那就是「直通」代理的行为。
flowchart TB
accTitle: 经代理的 HTTPS 是 CONNECT 隧道
accDescr: 客户端向代理送 CONNECT,代理打开 TCP 隧道并回 200,然后客户端与目标在隧道内进行 TLS 握手。代理日志看到主机,看不到 URL 路径
cli["客户端"] -->|"CONNECT host:443"| px["代理"]
px -->|"200 与 TCP 隧道"| dest["目标"]
dest -->|"隧道内的 TLS"| cli
px -.-> log["日志:只有主机与成功"]
图 7: 直通代理看得到主机,看不到加密路径。
7.2. TLS 检查代理与证书错误
另一方面,安全产品代理包含TLS 检查(SSL 解密、中断并检查)类型,它终止 TLS、检查内容、再加密后转发。这个机制里呈现给客户端的服务器证书不是真的那一张,而是被换成以代理自己的 CA 重新签名的证书。13
因此这个配置站得住的前提是「代理的 CA 证书已分发到每个客户端的受信任根」。没收到的计算机,或不看 Windows 证书存储的运行时(有自己信任存储的工具),会得到证书验证错误。在 .NET 通常表面为包着 AuthenticationException 的 HttpRequestException(「远程证书无效」那类消息)。
修正原则如下。
- 把内部 CA 证书分发到本地计算机的「受信任的根证书颁发机构」存储。 用户存储与计算机存储的分界见「Windows证书存储实务指南」。
- 不要在代码里关掉证书验证。 让
ServerCertificateCustomValidationCallback永远回 true 的权宜之计,一到外部网络就变成无法检测中间人的脆弱应用。 - 证书固定流量本来就无法检查。 像部分 Windows 组件那样验证特定 Microsoft 证书的连接,代理一换证书就失败,除了排除没有其他办法。4 对前往 Microsoft 365 等 SaaS 的流量,Microsoft 自己建议从网络层解密与检查排除。13
「每个内部站点都看得到,只有某个云服务在应用里出现证书错误」这类症状,应先怀疑 TLS 检查排除列表与固定的组合。
flowchart TB
accTitle: TLS 检查代理会重新签名证书
accDescr: 代理终止 TLS、检查内容,并呈现以自己 CA 重新签名的证书。只有该 CA 在受信任根里,验证才成立。不要在代码里关掉验证
real["真正的服务器证书"] --> px["TLS 检查代理"]
px --> fake["由代理 CA 重新签名"]
fake --> client["客户端验证"]
client -->|"CA 在受信任根"| ok["成功"]
client -->|"缺少 CA"| err["证书错误"]
err -.-> fix["把 CA 分发到存储"]
图 8: 检查只有与分发内部 CA 成套才运作。
8. 隔离步骤 ── 找出元凶的五步
按这个顺序机械地调查「连不上」。
| 步骤 | 做什么 | 学到什么 |
|---|---|---|
| (1) 复现 | 用 curl.exe -v 或 Invoke-WebRequest 访问问题 URL(最好同一台计算机、同一个帐户) |
是应用特有问题还是环境问题 |
| (2) 收集设置 | 收集三个谱系:netsh winhttp show proxy、每用户设置、环境变量 |
哪个谱系里有什么 |
| (3) 识别帐户 | 识别目标应用的运行帐户(服务、任务计划程序、其他用户) | 它以哪些设置与哪些凭据运行 |
| (4) 分类错误 | 区分 407 / 403 / 名称解析失败 / 超时 / 证书错误 | 隔离代理认证、策略拒绝、路径与 TLS 检查 |
| (5) 代理日志 | 在代理服务器访问日志核对对应时间 | 到底有没有到代理、认证成谁 |
(2) 可用 PowerShell 一次收集。
# (1) Per-user (WinINET) settings — note that this reads HKCU of the running account
Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' |
Select-Object ProxyEnable, ProxyServer, ProxyOverride, AutoConfigURL
# (2) Machine (WinHTTP) settings
netsh winhttp show proxy
# (3) Environment variables
Get-ChildItem env: | Where-Object Name -match 'proxy'
几则实务提示。
- (1) 的复现测试里,要意识工具读的是哪个设置谱系。Windows 内置
curl.exe可用-x http://proxy:8080显式指定代理,TLS 验证通常用操作系统证书存储(Schannel)。Windows PowerShell 5.1 的Invoke-WebRequest跟随 .NET Framework 这一侧(默认 Internet 选项);PowerShell 7 跟随 .NET 这一侧(环境变量优先)。「curl 可以、应用不行」本身就是设置谱系不一致的线索。 - 若 (3) 的目标是服务,在与服务相同的帐户下重查 (1) 与 (2)。管理员自己会话里的检查,不是 LocalSystem 看见什么的证据。
- (4) 的错误分类里,407 的第一候选人是第 6 章(认证),证书错误是第 7 章(TLS 检查),超时是「没到代理」(路径、名称解析、防火墙)。原因是 Windows 防火墙入站规则而不是代理的模式,见「Windows 防火墙与业务应用」。
- 若走到 (5) 代理日志仍无痕迹,流量从未到达代理。怀疑 PAC 的 DIRECT 决定、绕过列表或残留环境变量,必要时用数据包捕获确认实际目标(「Windows 的数据包捕获实务 ── 在 pktmon、netsh trace 与 Wireshark 之间选择」)。
flowchart TB
accTitle: 隔离代理故障的五个步骤
accDescr: 在同一帐户下复现、收集三个谱系的设置、识别运行帐户、分类错误,然后查代理日志
s1["用 curl 复现"] --> s2["收集三个谱系"]
s2 --> s3["识别帐户"]
s3 --> s4["分类错误"]
s4 --> s5["查代理日志"]
s4 -.-> e407["407:认证"]
s4 -.-> ecert["证书错误:检查"]
s4 -.-> eto["超时:从未到达"]
图 9: 按序走完五步。错误类别决定下一章。
9. 设计建议 ── 把应用做成「可以配置代理」的
把调查步骤反过来,就成了应用端的设计指引。对要交到有企业内部代理环境的 Windows 应用,建议如下。
- 让代理可从应用设置配置。默认是「跟随操作系统设置」。 多数环境默认就够;只有在读不到 PAC、以服务运行、特殊代理配置这些例外环境,才从设置文件指定代理 URL、绕过列表与「不使用代理」。第 5.3 节的
HttpClientHandler.Proxy/UseProxy是实现点。14 - 写下内部目标(API、数据库、许可证服务器等)如何当成代理例外处理。 以能写进部署程序的形式记录:是用 PAC DIRECT、绕过列表还是
NO_PROXY排除。NO_PROXY的匹配规则(没有通配符、前导点代表什么)被广泛误解,请附上例子。7 - 超时与重试要假设会经过代理来设计。 若代理挂掉或卡在认证,等很长默认超时的实现会让 UI 与运营一起冻结。分开较短的连接超时,并把重试限于幂等请求(设计细节见「不要用 using 包裹 HttpClient」)。
- 记录「用了哪个代理」。 让应用自己能回答故障调查的第一个问题。
像 (4) 这样的日志,只记下解析结果就已经有效。重点是从你实际用来配置客户端的设置(处理程序)推导路径。若直接记录 HttpClient.DefaultProxy,当处理程序显式指定 Proxy 或设 UseProxy = false 时,会记下与实际路径不一致的值。
using System.Net.Http;
// handler is the same instance used to create the HttpClient
// UseProxy=false is always direct. An explicit specification wins; otherwise DefaultProxy is used
var effectiveProxy = handler.UseProxy
? handler.Proxy ?? HttpClient.DefaultProxy
: null;
var target = new Uri("https://api.example.com/v1/orders");
var route = effectiveProxy is null || effectiveProxy.IsBypassed(target)
? "DIRECT"
: effectiveProxy.GetProxy(target)?.ToString() ?? "DIRECT";
logger.LogInformation("HTTP send {Target} route {Route} account {User}",
target, route, Environment.UserName);
若启动时对主要目标各记一次「路径」与「运行帐户」,第 8 章的 (1) 到 (3) 只要读日志就结束。当有人说「浏览器可以,但是……」时,能从应用端说「我用了这个设置、这条路径」,就是对代理麻烦够强的应用的条件。
flowchart TB
accTitle: 让代理可配置并记录路径
accDescr: 默认跟随操作系统设置,允许从应用设置指定显式代理 URL 或绕过或不使用代理,并把实际使用的路径与运行帐户一起记录
def["默认:跟随操作系统设置"] --> exc{"例外环境?"}
exc -->|"PAC 未读 / 服务 / 特殊"| cfg["设置 URL、绕过或不使用"]
exc -->|"一般情况"| os["使用操作系统默认"]
cfg --> log["记录路径与帐户"]
os --> log
图 10: 必须时才配置。永远记录用了哪条路径。
10. 总结
- Windows 的代理设置分成三个谱系──WinINET 每用户设置、WinHTTP 计算机设置、环境变量──读哪一套由应用(其 HTTP 栈)与运行帐户决定。
- WinINET 给交互式应用,不支持在服务中使用;服务用途是 WinHTTP(
netsh winhttp)的工作。「手动可以、做成服务就不行」先怀疑运行帐户不同。 netsh winhttp set proxy是静态设置,不处理 PAC、自动检测或认证。在以 PAC 运营的网络上,必须决定无法读 PAC 的客户端如何处理。- PAC 的
FindProxyForURL按 URL 返回代理或 DIRECT。WPAD 只在有 DHCP/DNS 安排的网络上运作。 - .NET Framework 的默认是运行帐户的 Internet 选项(可用
defaultProxy覆盖);.NET(Core 及以后)是环境变量然后用户代理设置。显式指定(HttpClientHandler.Proxy)永远最高优先。 - 407 是代理认证错误;以服务帐户运行的应用里,典型原因是「默认凭据」变成另一个人。
- TLS 检查代理以分发内部 CA 证书为前提,证书错误的正确答案是分发到证书存储,不是关掉验证。固定流量需要排除。
- 按「复现 → 收集三个谱系的设置 → 识别运行帐户 → 分类错误 → 代理日志」的顺序机械隔离。应用端,「能配置代理,并记录它用的路径」的设计是最好的预防。
下次有人来咨询「只有业务应用连不上」时,先问这个。
那个应用以谁的帐户运行,它读的是代理设置三个谱系里的哪一个?
这一问会大幅改变调查的入口。
相关文章
- 不要用 using 包裹 HttpClient —— C# 业务应用的 HTTP 通信实务(创建模式・超时设计・重试)
- Windows 的数据包捕获实务 ── 在 pktmon、netsh trace 与 Wireshark 之间选择
- Windows 防火墙与业务应用 ── 入站规则要通过安装程序注册
- Windows 服务的创建与运维 ── 从任务计划程序的取舍到 BackgroundService 服务化
- 图解 NTLM 与 Kerberos ── 为什么认证会「回退」到 NTLM
- Windows证书存储实务指南 ── 应该放入用户存储还是计算机存储
相关咨询领域
小村软件有限公司承接企业内部代理、需认证代理与 TLS 检查环境下 Windows 应用通信障碍的调查──「开发机可以、客户网络不能通信」、「做成服务后就到不了外部 API」──以及预设有代理环境的业务应用通信设计(设置项、超时、日志设计)咨询。从整理复现步骤与如何收集日志开始也可以。
参考链接
-
Microsoft Learn, WinINet vs. WinHTTP. 关于除非在服务或需要模拟与会话隔离的进程中,否则使用 WinINET 的指引,以及涵盖凭据缓存、凭据提示、服务支持、模拟、会话隔离等的功能比较表。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, netsh winhttp. 关于 netsh winhttp show/set/import/reset 的语法;set proxy 的 proxy-server 与 bypass-list;import proxy source=ie;以及经 set advproxy 以 JSON 形式配置详细代理(Proxy、ProxyBypass、AutoconfigUrl、AutoDetect)。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, About WinHTTP. 关于 WinHTTP 是为服务与服务器端用途设计的 HTTP 栈,支持以服务帐户运行与模拟,且不共享浏览器的 Cookie、缓存、凭据或用户的 Internet 选项。 ↩ ↩2 ↩3
-
Microsoft Learn, Using a proxy with Delivery Optimization. 关于 netsh winhttp set proxy 是不支持自动检测、PAC URL 或代理认证的静态设置;没有登录用户之情境的设备级代理配置(NetworkProxy CSP、「将代理设置设为计算机级」策略);以及证书固定流量在 TLS 检查下失败而需要排除。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10
-
Microsoft Learn, WinHTTP AutoProxy Support. 关于 PAC 脚本包含按请求计算代理列表的 FindProxyForURL(url, host) 函数、以特殊返回值表示直接连接,以及较旧的 AutoProxy API 不会把自动代理自动整合进 HTTP 栈,因此应用必须调用 WinHttpGetProxyForUrl。 ↩ ↩2 ↩3
-
Microsoft Learn, WinHttpGetProxyForUrl function. 关于它是 WPAD 协议的实现,因为 PAC 文件可依 URL 返回不同代理而必须按 URL 调用,以及同时支持显式 PAC URL 与从网络自动检测。 ↩ ↩2
-
Microsoft Learn, HttpClient.DefaultProxy Property. 关于 Windows 先读 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY、NO_PROXY 环境变量,未定义时读用户代理设置;Linux 在没有环境变量时以没有代理初始化;NO_PROXY 不支持通配符并使用前导点子域匹配;以及代理 URL 可包含用户名与密码。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, Configuring Internet Applications. 关于 .NET Framework 上 defaultProxy 元素定义默认代理;没有 Proxy 属性的 HttpWebRequest 使用默认代理;以及系统 Internet 设置与配置文件设置结合且配置文件端优先。 ↩ ↩2 ↩3
-
Microsoft Learn, defaultProxy element (network settings). 关于 system.net/defaultProxy 元素的 enabled 与 useDefaultCredentials 属性、proxy、bypasslist 与 module 子元素、元素为空时使用系统代理设置,以及迁移到 .NET 6 及以后时用 HttpClient.DefaultProxy 配置。 ↩ ↩2 ↩3
-
Microsoft Learn, Authentication in WinHTTP. 关于需要代理认证时返回状态码 407 与 Proxy-Authenticate 标头(服务器认证是 401 与 WWW-Authenticate);Basic 认证与 Kerberos 等质询/响应方案的差异;以及质询/响应方案代表用户名与密码不走网络。 ↩ ↩2 ↩3
-
Microsoft Learn, HttpClientHandler.DefaultProxyCredentials Property. 关于在 UseProxy 为 true 且 Proxy 为 null 而使用系统默认代理时,设置用来向该默认代理认证之凭据的属性。 ↩ ↩2
-
Microsoft Learn, WebProxy.Credentials Property. 关于 Credentials 属性是响应 HTTP 407 时送给代理的凭据,以及许多客户端情境建议将 UseDefaultCredentials 设为 true 以使用已登录用户的默认凭据。 ↩ ↩2
-
Microsoft Learn, Understanding implications when using network intermediation to decrypt or manipulate Microsoft 365 traffic at the network layer. 关于 TLS 检查(SSL 解密)是代理或防火墙解密、检查并重新加密 TLS 的配置;它可能使假设端到端 TLS 的服务故障与性能下降;以及建议将前往 Microsoft 365 的流量从网络层解密与检查排除。 ↩ ↩2 ↩3
-
Microsoft Learn, Make HTTP requests with the HttpClient class. 关于 HttpClient.DefaultProxy 与 HttpClientHandler.Proxy 两种配置方法;指定 Proxy 优先于配置文件与本地计算机设置;经 DNS 名称 wpad 或 DHCP 取得 PAC 文件(wpad.dat 等)的典型 WPAD 配置;以及按平面名称、回环与域后缀匹配的本地目标绕过判断。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, WinHttpOpen function. 关于各 dwAccessType 值的含义。WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY(Windows 8.1 及以后)从系统/用户代理设置自动决定代理,并自动处理故障转移与认证;WINHTTP_ACCESS_TYPE_DEFAULT_PROXY 自 8.1 起弃用。 ↩
-
Microsoft Learn, Work with existing on-premises proxy servers. 关于出站 HTTPS 以向代理的 CONNECT 请求建立;成功回 HTTP 200;以及 407(需要认证)或 502 等响应表示代理未允许通信,因此应与代理端团队一起推进隔离。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
不要用 using 包裹 HttpClient —— C# 业务应用的 HTTP 通信实务(创建模式・超时设计・重试)
C# 的 HttpClient 如果每次都用 using 创建会导致套接字耗尽,改成 static 又无法跟上 DNS 变更——本文从 Windows 业务应用的实务角度,整理这两大问题的成因,以及通过 PooledConnectionLifetime、IHttpClien...
Windows 数据包捕获实务 — pktmon、netsh trace、Wireshark 怎么选
应用程序日志只留下「超时」的通信故障,往下一层看实际走过线路的数据包就能调查。即使服务器不能安装 Wireshark,也能用内置的 pktmon 与 netsh trace 捕获。本文整理捕获与分析分开的现场作法。
多线程实务最佳实践 .NET 篇 ── 在增加线程之前应先确定的事
针对 .NET/C# 整理「立了线程之后,偶尔崩溃・卡死」的防范设计准则。内容涵盖不自行创建线程而改用 Task、减少共享可变状态、锁的纪律、基于 CancellationToken 的停止设计,直至 UI 线程的处理方式。
在 C# / PowerShell 中使用 WMI/CIM ── 硬件信息获取・进程监控・远程查询实务指南
获取电脑序列号、监控磁盘剩余空间、检测进程启动,这些场景的经典方案就是 WMI/CIM。本文讲解 Get-CimInstance 等 CIM cmdlet 的用法与从旧版 Get-WmiObject 的迁移、C# 中 System.Management 与 CIM API ...
Windows 防火墙与业务应用 ── 入站规则要通过安装程序注册
「开发机上正常运行,但在客户现场却无法通信」的常见原因就是 Windows 防火墙。本文讲解入站默认阻止与网络配置文件、不能把生产环境交给通知对话框处理的原因,以及通过安装程序注册入站规则的方法与排查步骤。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
故障调查 & 长期运行故障
整理间歇性故障、通信诊断、长期运行崩溃、失败路径测试基础的主题页面。
常见问题
汇总了咨询这一主题时常见的问题。
- 浏览器能连上,只有业务应用过不了企业内部代理。为什么?
- 浏览器读取的是 WinINET 的每用户代理设置,但业务应用不一定读同一套。以 Windows 服务运行、或以其他帐户运行的应用,会查询该帐户看得到的设置、WinHTTP 的计算机设置,或环境变量。先确认运行帐户,再用 netsh winhttp show proxy 与用户设置两边检查该帐户看得到的代理设置。若能在同一台计算机、同一个帐户下用 curl.exe 等复现,就可把它当成设置谱系之间的不一致,而不是应用本身的问题。
- 我设了 netsh winhttp set proxy,应用的流量却没变。为什么?
- netsh winhttp 设的是 WinHTTP 的计算机默认值。它不影响读 WinINET 的浏览器或交互式应用,也不影响优先读环境变量的 .NET(Core 及以后)HttpClient。netsh winhttp set proxy 也是静态设置,不处理 PAC 自动配置、自动检测或代理认证。你得先确认目标应用用哪一套 HTTP 栈、从哪个设置谱系解析代理。
- .NET 应用读取哪些代理设置?
- .NET Framework 默认使用运行帐户的 Internet 选项(等同 WinINET)设置,并可用 app.config 的 system.net/defaultProxy 元素覆盖。Core 及以后的 HttpClient 先读 HTTP_PROXY、HTTPS_PROXY、NO_PROXY 等环境变量,未定义时才回落到 Windows 用户代理设置。两种情况都是显式的 HttpClientHandler.Proxy 优先。Framework 与 Core 及以后的默认解析顺序不同,迁移时必须重新检查代理行为。
- 收到 407 Proxy Authentication Required 时该查什么?
- 407 表示代理本身在要求认证,与目标服务器的认证错误(401)是两回事。先从 Proxy-Authenticate 标头确认代理要求的认证方案(Negotiate、NTLM、Basic),并在 .NET 用 HttpClientHandler.DefaultProxyCredentials 或 WebProxy.UseDefaultCredentials 传入凭据。以服务帐户运行的应用里,「默认凭据」会变成该服务帐户的凭据,因此常见事故是交互式用户没问题,一做成服务就 407。也请查代理端日志它认证成谁。
- TLS 检查代理造成证书错误。可以关掉证书验证吗?
- 不建议关掉。TLS 检查代理会解密流量,再把以自己 CA 重新签名的证书呈现给客户端,若该 CA 证书不在受信任的根证书中,验证就会失败。正确做法是把内部 CA 证书分发到 Windows 证书存储(通常是本地计算机的「受信任的根证书颁发机构」)。在代码里关掉验证,代表应用拿到外部网络时无法检测中间人攻击,漏洞会一直留下。