企业内部代理与 Windows 应用 ── 理清 WinINET、WinHTTP、.NET 的代理解析

· · Windows, Proxy, WinHTTP, WinINET, .NET, HttpClient, PAC, WPAD, 网络

「浏览器打得开外部站点,只有业务应用到不了外部 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_PROXYHTTPS_PROXYALL_PROXY 任一项,就优先于操作系统设置,因此会发生「有人把环境变量留下」的事故。7
  • .NET Framework 的默认是运行帐户的 Internet 选项,可用 app.config 的 defaultProxy 覆盖。 配置文件设置优先于系统设置。89
  • 407 是代理认证错误,与 401(服务器认证)是两回事。 方案包含 Negotiate、NTLM、Basic,.NET 用 DefaultProxyCredentialsWebProxy.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)。所以通常不是「代理设置是对的却连不上」,现实是「应用读的谱系,跟你查的那一套不是同一套」。

Windows 代理设置的三个谱系WinINET 是每用户的设置与 Internet 选项,WinHTTP 是经由 netsh 的计算机默认值,环境变量是进程范围。读哪个谱系由应用决定,不是由设置端决定哪个谱系?WinINET 每用户设置WinHTTP 计算机设置HTTP_PROXY 及其同类浏览器与桌面应用服务与部分操作系统.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」── 反过来说,若是服务,就用 WinHTTP1

交互式应用用 WinINET,服务用 WinHTTPWinINET 继承已登录用户的 Internet 选项,且不支持在服务中使用。WinHTTP 以服务帐户、无 UI 运行,且不共享用户的浏览器设置服务或类似服务交互式桌面应用?WinINETWinHTTP读用户的 Internet 选项计算机设置,无 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

这里要记住两个限制。

  1. netsh winhttp set proxy 是静态设置。 它既不处理代理自动检测,也不处理指定 PAC URL,也不处理代理认证。4
  2. import proxy source=ie 只复制当下那一刻的静态设置,不会跟随之后 Internet 选项端的变更。需要包含 PAC 或自动检测的计算机级配置时,用 netsh winhttp set advproxy 配置 JSON 形式的详细设置(ProxyProxyBypassAutoconfigUrlAutoDetect)。2

3.3. 最常见的陷阱:服务不读用户的 IE 设置

现场最常看到的模式,按时间顺序是这样。

  1. 开发者在自己的电脑跑工具 → 每用户代理设置 (1) 生效,于是成功
  2. 生产环境以 LocalSystem 做成常驻的 Windows 服务(Windows 服务的创建与运维
  3. 从 LocalSystem 看得到的设置是另一套(每用户设置看不见,WinHTTP 计算机设置未配置 = DIRECT)→ 尝试直接连外部 API 并超时

不是「同一台计算机却不行」;即使同一台计算机,运行帐户不同,看得见的代理设置集合就不同。对即使没有用户登录也要通信的进程,正确做法是以该进程的 HTTP 栈实际会读的形式准备计算机级设置。使用 WinHTTP 的原生应用或 Windows 组件会套用 netsh 的 WinHTTP 设置。4 另一方面,Core 及以后的 HttpClient 不读 WinHTTP 的计算机设置(见第 5 章),因此对 .NET 服务要设系统环境变量(HTTPS_PROXY 等),或从应用设置显式指定 HttpClientHandler.Proxy

反向事故也会发生。若用 netsh winhttp set proxy 把静态代理烤进在企业网络与外部之间移动的笔记本,公司外连不到那个代理,通信就整段死掉。把计算机静态设置当成针对网络配置不变的服务器的手段。4

为什么服务看不到用户的 IE 设置开发者运行会读每用户的 WinINET 设置并成功。以 LocalSystem 那些设置看不见。原生 WinHTTP 应用接着跟随未配置的计算机设置(DIRECT)。.NET Core+ 服务仍使用环境变量或显式的 handler.Proxy,不会改去读 netsh winhttpWinHTTP.NET Core+以用户手动运行套用 WinINET 每用户设置LocalSystem 的 Windows 服务每用户设置看不见哪一套 HTTP 栈?WinHTTP 未配置 = DIRECT环境变量或 handler.Proxy外部 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 安排的网络上才会运作的机制。在没有这种安排的网络上只开自动检测,只会多出检测失败的等待时间。

PAC 按 URL 决定代理,WPAD 只找 PACFindProxyForURL 接受 URL 与主机并返回代理列表或 DIRECT。WPAD 只经 DHCP 或 DNS 找出 PAC。无法评估 PAC 的客户端回落到静态代理或环境变量代理列表DIRECT请求 URLFindProxyForURL经由代理不经代理连接经 DHCP 或 DNS 的 WPAD无法评估 PAC 的客户端静态设置或环境变量

图 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_PROXYHTTPS_PROXYALL_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 用户代理设置
Framework 与 Core 及以后的默认代理解析显式的 HttpClientHandler.Proxy 永远胜出。Framework 接着使用 app.config defaultProxy 与运行帐户的 Internet 选项。Core 及以后使用对 HttpClient.DefaultProxy 的赋值,然后环境变量,然后 Windows 用户代理设置FrameworkCore 及以后显式的 handler.Proxy使用该代理没有显式 Proxy哪个运行时?app.config defaultProxy运行帐户 Internet 选项HttpClient.DefaultProxyHTTP_PROXY 及其同类Windows 用户代理设置

图 5: 显式指定永远胜出。默认路径依运行时而不同。

6. 需认证的代理 ── 407 是代理的认证错误

6.1. 不要把 407 与 401 搞混

当你尝试通过要求认证的代理时,代理会返回状态码 407(Proxy Authentication Required) 与列出可用方案的 Proxy-Authenticate 标头。那与目标服务器的认证要求(401 与 WWW-Authenticate)是两回事;你交出凭据的对象与配置它们的位置都不同。10

407 是代理,401 是目标服务器407 与 Proxy-Authenticate 来自代理。401 与 WWW-Authenticate 来自目标服务器。凭据与配置位置都不同代理目标出站请求谁要求认证?407 + Proxy-Authenticate401 + WWW-AuthenticateDefaultProxyCredentials

图 6: 407 是代理认证。401 是服务器认证。

方案包含原样送出用户名与密码的 Basic,以及 Negotiate(Kerberos/NTLM)这类质询/响应方案。质询/响应方案中密码本身不走网络,认证经数次往返完成。10 它「回退」到哪个方案的机制,在「图解 NTLM 与 Kerberos」有更细的说明。

6.2. 在 .NET 如何传入凭据

若要用操作系统设置中的默认代理、只让认证通过,使用 HttpClientHandler.DefaultProxyCredentials。这些是在 UseProxy = trueProxy = 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 路径看不见── 那就是「直通」代理的行为。

经代理的 HTTPS 是 CONNECT 隧道客户端向代理送 CONNECT,代理打开 TCP 隧道并回 200,然后客户端与目标在隧道内进行 TLS 握手。代理日志看到主机,看不到 URL 路径CONNECT host:443200 与 TCP 隧道隧道内的 TLS客户端代理目标日志:只有主机与成功

图 7: 直通代理看得到主机,看不到加密路径。

7.2. TLS 检查代理与证书错误

另一方面,安全产品代理包含TLS 检查(SSL 解密、中断并检查)类型,它终止 TLS、检查内容、再加密后转发。这个机制里呈现给客户端的服务器证书不是真的那一张,而是被换成以代理自己的 CA 重新签名的证书13

因此这个配置站得住的前提是「代理的 CA 证书已分发到每个客户端的受信任根」。没收到的计算机,或不看 Windows 证书存储的运行时(有自己信任存储的工具),会得到证书验证错误。在 .NET 通常表面为包着 AuthenticationExceptionHttpRequestException(「远程证书无效」那类消息)。

修正原则如下。

  • 把内部 CA 证书分发到本地计算机的「受信任的根证书颁发机构」存储。 用户存储与计算机存储的分界见「Windows证书存储实务指南」。
  • 不要在代码里关掉证书验证。ServerCertificateCustomValidationCallback 永远回 true 的权宜之计,一到外部网络就变成无法检测中间人的脆弱应用。
  • 证书固定流量本来就无法检查。 像部分 Windows 组件那样验证特定 Microsoft 证书的连接,代理一换证书就失败,除了排除没有其他办法。4 对前往 Microsoft 365 等 SaaS 的流量,Microsoft 自己建议从网络层解密与检查排除。13

「每个内部站点都看得到,只有某个云服务在应用里出现证书错误」这类症状,应先怀疑 TLS 检查排除列表与固定的组合。

TLS 检查代理会重新签名证书代理终止 TLS、检查内容,并呈现以自己 CA 重新签名的证书。只有该 CA 在受信任根里,验证才成立。不要在代码里关掉验证CA 在受信任根缺少 CA真正的服务器证书TLS 检查代理由代理 CA 重新签名客户端验证成功证书错误把 CA 分发到存储

图 8: 检查只有与分发内部 CA 成套才运作。

8. 隔离步骤 ── 找出元凶的五步

按这个顺序机械地调查「连不上」。

步骤 做什么 学到什么
(1) 复现 curl.exe -vInvoke-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 之间选择」)。
隔离代理故障的五个步骤在同一帐户下复现、收集三个谱系的设置、识别运行帐户、分类错误,然后查代理日志用 curl 复现收集三个谱系识别帐户分类错误查代理日志407:认证证书错误:检查超时:从未到达

图 9: 按序走完五步。错误类别决定下一章。

9. 设计建议 ── 把应用做成「可以配置代理」的

把调查步骤反过来,就成了应用端的设计指引。对要交到有企业内部代理环境的 Windows 应用,建议如下。

  1. 让代理可从应用设置配置。默认是「跟随操作系统设置」。 多数环境默认就够;只有在读不到 PAC、以服务运行、特殊代理配置这些例外环境,才从设置文件指定代理 URL、绕过列表与「不使用代理」。第 5.3 节的 HttpClientHandler.Proxy / UseProxy 是实现点。14
  2. 写下内部目标(API、数据库、许可证服务器等)如何当成代理例外处理。 以能写进部署程序的形式记录:是用 PAC DIRECT、绕过列表还是 NO_PROXY 排除。NO_PROXY 的匹配规则(没有通配符、前导点代表什么)被广泛误解,请附上例子。7
  3. 超时与重试要假设会经过代理来设计。 若代理挂掉或卡在认证,等很长默认超时的实现会让 UI 与运营一起冻结。分开较短的连接超时,并把重试限于幂等请求(设计细节见「不要用 using 包裹 HttpClient」)。
  4. 记录「用了哪个代理」。 让应用自己能回答故障调查的第一个问题。

像 (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) 只要读日志就结束。当有人说「浏览器可以,但是……」时,能从应用端说「我用了这个设置、这条路径」,就是对代理麻烦够强的应用的条件。

让代理可配置并记录路径默认跟随操作系统设置,允许从应用设置指定显式代理 URL 或绕过或不使用代理,并把实际使用的路径与运行帐户一起记录PAC 未读 / 服务 / 特殊一般情况默认:跟随操作系统设置例外环境?设置 URL、绕过或不使用使用操作系统默认记录路径与帐户

图 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 证书为前提,证书错误的正确答案是分发到证书存储,不是关掉验证。固定流量需要排除。
  • 按「复现 → 收集三个谱系的设置 → 识别运行帐户 → 分类错误 → 代理日志」的顺序机械隔离。应用端,「能配置代理,并记录它用的路径」的设计是最好的预防。

下次有人来咨询「只有业务应用连不上」时,先问这个。

那个应用以谁的帐户运行,它读的是代理设置三个谱系里的哪一个?

这一问会大幅改变调查的入口。

相关文章

相关咨询领域

小村软件有限公司承接企业内部代理、需认证代理与 TLS 检查环境下 Windows 应用通信障碍的调查──「开发机可以、客户网络不能通信」、「做成服务后就到不了外部 API」──以及预设有代理环境的业务应用通信设计(设置项、超时、日志设计)咨询。从整理复现步骤与如何收集日志开始也可以。

参考链接

  1. Microsoft Learn, WinINet vs. WinHTTP. 关于除非在服务或需要模拟与会话隔离的进程中,否则使用 WinINET 的指引,以及涵盖凭据缓存、凭据提示、服务支持、模拟、会话隔离等的功能比较表。  2 3 4

  2. 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

  3. Microsoft Learn, About WinHTTP. 关于 WinHTTP 是为服务与服务器端用途设计的 HTTP 栈,支持以服务帐户运行与模拟,且不共享浏览器的 Cookie、缓存、凭据或用户的 Internet 选项。  2 3

  4. 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

  5. Microsoft Learn, WinHTTP AutoProxy Support. 关于 PAC 脚本包含按请求计算代理列表的 FindProxyForURL(url, host) 函数、以特殊返回值表示直接连接,以及较旧的 AutoProxy API 不会把自动代理自动整合进 HTTP 栈,因此应用必须调用 WinHttpGetProxyForUrl。  2 3

  6. Microsoft Learn, WinHttpGetProxyForUrl function. 关于它是 WPAD 协议的实现,因为 PAC 文件可依 URL 返回不同代理而必须按 URL 调用,以及同时支持显式 PAC URL 与从网络自动检测。  2

  7. 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

  8. Microsoft Learn, Configuring Internet Applications. 关于 .NET Framework 上 defaultProxy 元素定义默认代理;没有 Proxy 属性的 HttpWebRequest 使用默认代理;以及系统 Internet 设置与配置文件设置结合且配置文件端优先。  2 3

  9. Microsoft Learn, defaultProxy element (network settings). 关于 system.net/defaultProxy 元素的 enabled 与 useDefaultCredentials 属性、proxy、bypasslist 与 module 子元素、元素为空时使用系统代理设置,以及迁移到 .NET 6 及以后时用 HttpClient.DefaultProxy 配置。  2 3

  10. Microsoft Learn, Authentication in WinHTTP. 关于需要代理认证时返回状态码 407 与 Proxy-Authenticate 标头(服务器认证是 401 与 WWW-Authenticate);Basic 认证与 Kerberos 等质询/响应方案的差异;以及质询/响应方案代表用户名与密码不走网络。  2 3

  11. Microsoft Learn, HttpClientHandler.DefaultProxyCredentials Property. 关于在 UseProxy 为 true 且 Proxy 为 null 而使用系统默认代理时,设置用来向该默认代理认证之凭据的属性。  2

  12. Microsoft Learn, WebProxy.Credentials Property. 关于 Credentials 属性是响应 HTTP 407 时送给代理的凭据,以及许多客户端情境建议将 UseDefaultCredentials 设为 true 以使用已登录用户的默认凭据。  2

  13. 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

  14. Microsoft Learn, Make HTTP requests with the HttpClient class. 关于 HttpClient.DefaultProxy 与 HttpClientHandler.Proxy 两种配置方法;指定 Proxy 优先于配置文件与本地计算机设置;经 DNS 名称 wpad 或 DHCP 取得 PAC 文件(wpad.dat 等)的典型 WPAD 配置;以及按平面名称、回环与域后缀匹配的本地目标绕过判断。  2 3 4

  15. Microsoft Learn, WinHttpOpen function. 关于各 dwAccessType 值的含义。WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY(Windows 8.1 及以后)从系统/用户代理设置自动决定代理,并自动处理故障转移与认证;WINHTTP_ACCESS_TYPE_DEFAULT_PROXY 自 8.1 起弃用。 

  16. Microsoft Learn, Work with existing on-premises proxy servers. 关于出站 HTTPS 以向代理的 CONNECT 请求建立;成功回 HTTP 200;以及 407(需要认证)或 502 等响应表示代理未允许通信,因此应与代理端团队一起推进隔离。 

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

浏览器能连上,只有业务应用过不了企业内部代理。为什么?
浏览器读取的是 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 证书存储(通常是本地计算机的「受信任的根证书颁发机构」)。在代码里关掉验证,代表应用拿到外部网络时无法检测中间人攻击,漏洞会一直留下。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表