企業內部 Proxy 與 Windows 應用程式 ── 整理 WinINET、WinHTTP、.NET 的 Proxy 解析

· · Windows, Proxy, WinHTTP, WinINET, .NET, HttpClient, PAC, WPAD, 網路

「瀏覽器開得了外部網站,只有業務應用程式到不了外部 API。」「開發機沒問題,到客戶網路就逾時。」「手動執行會通訊,一做成 Windows 服務就失敗。」── 在有企業內部 Proxy 的環境跑業務應用程式時,這類諮詢最常見。

多數情況下,原因既不是 Proxy 伺服器故障,也不是應用程式錯誤。Windows 有好幾套彼此分開、人們都叫「Proxy 設定」的系譜,誰讀哪一套會依應用程式(它用的 HTTP 堆疊)與執行帳戶而不同── 那就是不一致。瀏覽器讀的設定、服務讀的設定、.NET HttpClient 讀的設定,可以各自是不同的東西。結構一旦進了腦子,「瀏覽器可以,但是……」的隔離會意外地快。

本文面向中小企業 IT 人員與 Windows 應用程式開發者,把 Proxy 設定的三個系譜──WinINET、WinHTTP、環境變數──以及 PAC 與 WPAD 自動設定、.NET Framework 與 .NET(Core 以後)的 Proxy 解析差異、需驗證的 Proxy(407)、TLS 檢查與實務隔離步驟,收成同一張圖。HttpClient 的建立模式與逾時設計本身見「不要用 using 包住 HttpClient」,本文專注於 Proxy 解析

1. 先講結論

  • Windows 的 Proxy 設定不是一套,至少有三個系譜。 (1) WinINET 每位使用者設定(設定應用程式的「Proxy」頁 = 舊的網際網路選項),(2) WinHTTP 電腦設定(netsh winhttp),(3) HTTP_PROXY / HTTPS_PROXY 環境變數。讀哪一套由應用程式端決定。12
  • 設定應用程式裡看到的「Proxy」是 WinINET 的每位使用者設定。 瀏覽器與互動式應用程式會讀;Windows 服務不會。WinINET 不支援在服務中使用,服務用途是 WinHTTP 的工作。13
  • 「手動可以、做成服務就不行」最常見的原因是執行帳戶不同。 LocalSystem 與服務帳戶看不到管理員在自己畫面上設好的每位使用者 Proxy。34
  • netsh winhttp set proxy 是靜態設定,不處理 PAC、自動偵測或 Proxy 驗證。 若要依電腦設定 PAC 或 WPAD,需要 netsh winhttp set advproxy 這一側。42
  • PAC 結果依 URL 而變。 PAC 檔的 FindProxyForURL 函式接受 URL 與主機,回傳 Proxy 清單或直接連線(DIRECT)。「那個網站可以,只有這個 API 不行」可能是 PAC 分支。56
  • .NET(Core 以後)的 HttpClient 以環境變數 → Windows 使用者 Proxy 設定的順序初始化預設 Proxy。 只要定義了 HTTP_PROXYHTTPS_PROXYALL_PROXY 任一項,就優先於作業系統設定,因此會發生「有人把環境變數留著」的事故。7
  • .NET Framework 的預設是執行帳戶的網際網路選項,可用 app.config 的 defaultProxy 覆寫。 設定檔設定優先於系統設定。89
  • 407 是 Proxy 驗證錯誤,與 401(伺服器驗證)是兩件事。 配置包含 Negotiate、NTLM、Basic,.NET 用 DefaultProxyCredentialsWebProxy.UseDefaultCredentials 傳入認證。請注意,在服務帳戶下「預設認證」的內容會變。101112
  • TLS 檢查 Proxy 必須與內部 CA 憑證發佈成套才站得住。 沒收到的電腦與執行階段會得到憑證驗證錯誤。用發佈到憑證存放區解決,不要在應用程式裡關掉驗證。134

一句話:每次說「我查過 Proxy 設定」時,都要能說出查的是三個系譜裡的哪一個、從哪個帳戶查── 這就是本文的主題。

2. Windows 有三個系譜的「Proxy 設定」

先看整張地圖。Windows 應用程式尋找企業內部 Proxy 的路徑,落在這三個系譜。

設定系譜 設定位置 / 命令 範圍 主要誰讀
(1) WinINET(網際網路選項) 設定 → 網路和網際網路 → Proxy,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 Proxy 設定」,實質是 WinINET 組態。歷史上是 Internet Explorer 的網際網路選項,預設依使用者儲存。4

(2) 是「沒有登入使用者」這類服務情境的電腦預設值。(3) 主要是來自跨平台世界的工具慣例;在 Windows 上,.NET(Core 以後)與 curl 等也會讀。7

重點是:讀哪個系譜由應用程式端決定,不是由設定端決定。 應用程式內部用 WinINET 就讀 (1);用 WinHTTP 就讀 (2)(或應用程式自己的覆寫);Core 以後的 .NET 則先 (3) 再 (1)。所以通常不是「Proxy 設定是對的卻連不上」,現實是「應用程式讀的系譜,跟你查的那一套不是同一套」。

Windows Proxy 設定的三個系譜WinINET 是每位使用者的設定與網際網路選項,WinHTTP 是經由 netsh 的電腦預設值,環境變數是處理程序範圍。讀哪個系譜由應用程式決定,不是由設定端決定哪個系譜?WinINET 每位使用者設定WinHTTP 電腦設定HTTP_PROXY 與其同類瀏覽器與桌面應用程式服務與部分作業系統.NET Core+ 與 curl

圖 1: 三個系譜並列。應用程式選擇讀哪一套。

若啟用群組原則「將 Proxy 設定設為電腦層級(而非使用者層級)」,可以把 (1) 改成電腦層級並套用到每位使用者。用 MDM(Intune 等)可用 NetworkProxy CSP 依裝置設定。4

3. WinINET 與 WinHTTP ── 互動式應用程式用、服務用

3.1. 角色差異

WinINET 與 WinHTTP 都是 Windows 內建的 HTTP 用戶端堆疊,但假設的用途不同。

  • WinINET:針對互動式桌面應用程式。它會自動繼承使用者的網際網路選項(Proxy、Cookie、認證快取),必要時還能顯示認證輸入 UI。不支援在服務或類似服務的處理程序中使用1
  • WinHTTP:針對服務與伺服器端。它支援以服務帳戶執行、執行緒模擬與工作階段隔離;相對地不分享使用者的瀏覽器設定、Cookie 或認證。也不顯示 UI。3

Microsoft 自己的指引同樣清楚:「除非你在服務裡、或在需要工作階段隔離與模擬的類似服務處理程序裡執行,否則使用 WinINET」── 反過來說,若是服務,就用 WinHTTP1

互動式應用程式用 WinINET,服務用 WinHTTPWinINET 繼承已登入使用者的網際網路選項,且不支援在服務中使用。WinHTTP 以服務帳戶、無 UI 執行,且不分享使用者的瀏覽器設定服務或類似服務互動式桌面應用程式?WinINETWinHTTP讀使用者的網際網路選項電腦設定,無 UI

圖 2: 互動式應用程式用 WinINET。服務用 WinHTTP。

3.2. 基本的 netsh winhttp 操作

WinHTTP 的電腦預設 Proxy 用 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 是靜態設定。 它既不處理 Proxy 自動偵測,也不處理指定 PAC URL,也不處理 Proxy 驗證。4
  2. import proxy source=ie 只複製當下那一刻的靜態設定,不會跟隨之後網際網路選項端的變更。需要包含 PAC 或自動偵測的電腦層級組態時,用 netsh winhttp set advproxy 設定 JSON 形式的詳細設定(ProxyProxyBypassAutoconfigUrlAutoDetect)。2

3.3. 最常見的陷阱:服務不讀使用者的 IE 設定

現場最常看到的模式,依時間順序是這樣。

  1. 開發者在自己的電腦跑工具 → 每位使用者 Proxy 設定 (1) 生效,於是成功
  2. 正式環境以 LocalSystem 做成常駐的 Windows 服務(Windows 服務的建立與維運
  3. 從 LocalSystem 看得到的設定是另一套(每位使用者設定看不見,WinHTTP 電腦設定未設定 = DIRECT)→ 嘗試直接連外部 API 並逾時

不是「同一台電腦卻不行」;即使同一台電腦,執行帳戶不同,看得見的 Proxy 設定集合就不同。對即使沒有使用者登入也要通訊的處理程序,正確作法是以該處理程序的 HTTP 堆疊實際會讀的形式準備電腦層級設定。使用 WinHTTP 的原生應用程式或 Windows 元件會套用 netsh 的 WinHTTP 設定。4 另一方面,Core 以後的 HttpClient 不讀 WinHTTP 的電腦設定(見第 5 章),因此對 .NET 服務要設系統環境變數(HTTPS_PROXY 等),或從應用程式設定明確指定 HttpClientHandler.Proxy

反向事故也會發生。若用 netsh winhttp set proxy 把靜態 Proxy 烤進在企業網路與外部之間移動的筆電,公司外連不到那個 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: 同一台電腦、不同帳戶,看得見的 Proxy 設定集合就不同。

4. PAC 與 WPAD ── 「自動設定」實際是什麼

4.1. PAC 檔與 FindProxyForURL

PAC(Proxy Auto-Configuration)檔是計算「這個 URL 該用哪個 Proxy」的 JavaScript(ECMAScript),且一定包含名為 FindProxyForURL(url, host) 的函式。函式回傳應使用的 Proxy 清單,或表示可以不經 Proxy 直接連線的特殊回傳值(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";
}

接著有兩個實務後果。

  • Proxy 解析必須依 URL 進行。 因為 PAC 可依 URL(主機)回傳不同 Proxy 或直接連線,WinHTTP 的自動 Proxy 功能也設計成每次傳入請求 URL 再查詢。6「瀏覽器看得到另一個網站」並不能證明問題 API 走同一條路。
  • DIRECT 是「不經 Proxy 走」的指示。 若本應走內部的流量從未出現在 Proxy 記錄,先懷疑 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 決定 Proxy,WPAD 只找 PACFindProxyForURL 接受 URL 與主機並回傳 Proxy 清單或 DIRECT。WPAD 只經 DHCP 或 DNS 找出 PAC。無法評估 PAC 的用戶端回落到靜態 Proxy 或環境變數Proxy 清單DIRECT請求 URLFindProxyForURL經由 Proxy不經 Proxy 連線經 DHCP 或 DNS 的 WPAD無法評估 PAC 的用戶端靜態設定或環境變數

圖 4: PAC 依 URL 決定。WPAD 只找 PAC 檔。

4.3. 無法評估 PAC 的用戶端如何表現

不是每個用戶端都能評估 PAC。

  • netsh winhttp set proxy 的靜態設定不評估 PAC。4
  • 使用 HTTP_PROXY 環境變數風格的工具,原則上只能寫固定 Proxy URL(沒有地方寫 PAC URL)。7
  • 直接使用 WinHTTP 的原生應用程式,取決於工作階段怎麼開。Windows 8.1 以後用 WinHttpOpen 並指定 WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY 開啟的應用程式,WinHTTP 會依每個請求自動解析系統/使用者 Proxy 設定(含 WPAD/PAC)。15 若以較舊的 WINHTTP_ACCESS_TYPE_DEFAULT_PROXY(自 8.1 起淘汰)等開啟,自動 Proxy 不會整合進 HTTP 堆疊,應用程式必須自己呼叫 WinHttpGetProxyForUrl 再把結果套到請求。也就是說,在較舊的實作上,PAC 可以存在卻仍未被使用5

「瀏覽器經 PAC 走到正確 Proxy,業務應用程式不讀 PAC、嘗試直接連線而失敗」── 這是另一個經典不一致。在以 PAC 營運的網路上,必須為無法讀 PAC 的用戶端決定後援──靜態設定或環境變數。

5. .NET 的 Proxy 解析 ── Framework 與 Core 以後是兩回事

.NET 應用程式讀哪一套 Proxy 設定,.NET Framework 與 .NET(Core 以後)的預設不同。把兩者搞混,就會用 Framework 時代的知識去查 .NET 8 應用程式而漏掉。

5.1. .NET Framework ── 預設是網際網路選項,用 defaultProxy 覆寫

在 .NET Framework,HttpWebRequest 與坐在它上面的 HttpClient 若未明確指定 Proxy,就使用預設 Proxy。預設 Proxy 由系統的網際網路設定(執行帳戶的 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 元素為空就使用系統(網際網路選項)設定;寫上 proxyaddress 等則那些優先。從程式可用 WebRequest.DefaultWebProxy 取代同一個預設。98

第 3.3 節的陷阱這裡也適用。因為預設是「執行帳戶的網際網路選項」,以服務帳戶執行的 .NET Framework 應用程式讀到的是與管理員桌面看得見的不同(通常是空的)設定集合

5.2. .NET(Core 以後)── 環境變數優先,然後才是作業系統使用者設定

Core 以後的 HttpClient 有靜態屬性 HttpClient.DefaultProxy。除非處理常式明確指定 Proxy,每個 HttpClient 執行個體都用它。Windows 上的初始化規則是「讀環境變數,若未定義則讀使用者 Proxy 設定」7

使用的環境變數如下。7

環境變數 意義
HTTP_PROXY HTTP 請求使用的 Proxy
HTTPS_PROXY HTTPS 請求使用的 Proxy
ALL_PROXY 上述未定義時的後援
NO_PROXY 不應使用 Proxy 的主機,逗號分隔清單

三件要注意的事。

  • 只要定義了 HTTP_PROXYHTTPS_PROXYALL_PROXY 任一項,就優先於作業系統端的 Proxy 設定。 只定義 NO_PROXY 並不會從環境變數構成 Proxy,在 Windows 上會繼續使用作業系統使用者 Proxy 設定。舊實驗後把 HTTPS_PROXY 留成系統環境變數、或 CI/CD 範本注入它,這類「看不見的設定」是事故溫床。
  • NO_PROXY 不支援萬用字元(*)。 要比對子網域,放前導點(.example.com 比對 www.example.com,但不比對 example.com 本身)。7
  • 在非 Windows(Linux 容器等),若環境變數未定義,會以沒有 Proxy初始化。同一應用程式在 Windows 與 Linux 之間預設行為改變,是容器遷移時要確認的事。7

5.3. 明確指定 ── HttpClientHandler.Proxy 與 UseProxy

無論哪個執行階段,最高優先都是處理常式上的明確指定。指定 HttpClientHandler.Proxy 優先於作業系統設定與設定檔,UseProxy = false 則完全不使用 Proxy。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 突然開始走 Proxy」這類現象,可能由這個判斷造成。

優先順序如下。

優先(高 → 低) .NET Framework .NET(Core 以後)
1 HttpClientHandler.Proxy 等明確指定 相同
2 app.config 的 defaultProxy HttpClient.DefaultProxy 的指派
3 執行帳戶的網際網路選項 環境變數(HTTP_PROXY 等)
4 Windows 使用者 Proxy 設定
Framework 與 Core 以後的預設 Proxy 解析明確的 HttpClientHandler.Proxy 永遠勝出。Framework 接著使用 app.config defaultProxy 與執行帳戶的網際網路選項。Core 以後使用對 HttpClient.DefaultProxy 的指派,然後環境變數,然後 Windows 使用者 Proxy 設定FrameworkCore 以後明確的 handler.Proxy使用該 Proxy沒有明確 Proxy哪個執行階段?app.config defaultProxy執行帳戶網際網路選項HttpClient.DefaultProxyHTTP_PROXY 與其同類Windows 使用者 Proxy 設定

圖 5: 明確指定永遠勝出。預設路徑依執行階段而不同。

6. 需驗證的 Proxy ── 407 是 Proxy 的驗證錯誤

6.1. 不要把 407 與 401 搞混

當你嘗試通過要求驗證的 Proxy 時,Proxy 會回傳狀態碼 407(Proxy Authentication Required) 與列出可用配置的 Proxy-Authenticate 標頭。那與目的地伺服器的驗證要求(401 與 WWW-Authenticate)是兩件事;你交出認證的對象與設定它們的位置都不同。10

407 是 Proxy,401 是目的地伺服器407 與 Proxy-Authenticate 來自 Proxy。401 與 WWW-Authenticate 來自目的地伺服器。認證與設定位置都不同Proxy目的地對外請求誰要求驗證?407 + Proxy-Authenticate401 + WWW-AuthenticateDefaultProxyCredentials

圖 6: 407 是 Proxy 驗證。401 是伺服器驗證。

配置包含原樣送出使用者名稱與密碼的 Basic,以及 Negotiate(Kerberos/NTLM)這類挑戰/回應配置。挑戰/回應配置中密碼本身不走網路,驗證經數次往返完成。10 它「回退」到哪個配置的機制,在「圖解 NTLM 與 Kerberos」有更細的說明。

6.2. 在 .NET 如何傳入認證

若要用來自作業系統設定的預設 Proxy、只讓驗證通過,使用 HttpClientHandler.DefaultProxyCredentials。這些是在 UseProxy = trueProxy = null(= 系統預設 Proxy)時,送給該預設 Proxy 的認證。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);

明確指定 Proxy 時,把認證放在 WebProxy 這一側。許多用戶端情境的建議是使用已登入使用者的預設認證,而不是個別使用者名稱與密碼,WebProxy.UseDefaultCredentials = true 就是那個。12

6.3. 服務帳戶的 407 問題

執行帳戶在這裡也很重要。「預設認證」意指正在執行該處理程序的帳戶的認證。以互動式使用者執行,對 Proxy 的驗證就是該使用者;以 LocalSystem 服務執行,就是電腦帳戶。

  • 若 Proxy 經 Active Directory 驗證使用者,它無法驗證電腦帳戶或本機帳戶,一做成服務 407 就持續
  • 反過來說,有些環境在 Proxy 端對服務有驗證豁免(依來源 IP 或依帳戶)

因此 407 調查不能只停在「應用程式的設定」;它與基礎建設端的設計確認成套:Proxy 能否驗證執行帳戶。對即將做成服務的應用程式,設計階段就應決定其一:以網域服務帳戶(gMSA 等)執行、在 Proxy 端放驗證豁免,或架設不要求驗證的內部轉送 Proxy。

也有把認證嵌進環境變數的風格,如 HTTP_PROXY=http://user:pass@proxy:80807,但明文密碼會暴露在環境變數(= 處理程序資訊)裡,不建議作為常態營運。

7. HTTPS 與 Proxy ── CONNECT 通道與 TLS 檢查

7.1. HTTPS 以「通道」通過 Proxy

對 HTTPS 使用 Proxy 時,用戶端先向 Proxy 送 CONNECT destination-host:443 請求,Proxy 開啟 TCP 通道。成功時 Proxy 回 200,之後用戶端與目的地伺服器在該通道內進行 TLS 交握。若通道打不開,Proxy 回 407(需要驗證)、502 等。16

這個模型裡 Proxy 讀不到通道內容(加密的 HTTPS)。Proxy 記錄留下的是目的地主機名稱與連線是否成功;URL 路徑看不見── 那就是「直通」Proxy 的行為。

經 Proxy 的 HTTPS 是 CONNECT 通道用戶端向 Proxy 送 CONNECT,Proxy 開啟 TCP 通道並回 200,然後用戶端與目的地在通道內進行 TLS 交握。Proxy 記錄看到主機,看不到 URL 路徑CONNECT host:443200 與 TCP 通道通道內的 TLS用戶端Proxy目的地記錄:只有主機與成功

圖 7: 直通 Proxy 看得到主機,看不到加密路徑。

7.2. TLS 檢查 Proxy 與憑證錯誤

另一方面,資安產品 Proxy 包含TLS 檢查(SSL 解密、中斷並檢查)類型,它終止 TLS、檢查內容、再加密後轉送。這個機制裡呈現給用戶端的伺服器憑證不是真的那一張,而是被換成以 Proxy 自己的 CA 重新簽署的憑證13

因此這個組態站得住的前提是「Proxy 的 CA 憑證已發佈到每個用戶端的信任根」。沒收到的電腦,或不看 Windows 憑證存放區的執行階段(有自己信任存放區的工具),會得到憑證驗證錯誤。在 .NET 通常表面為包著 AuthenticationExceptionHttpRequestException(「遠端憑證無效」那類訊息)。

修正原則如下。

  • 把內部 CA 憑證發佈到本機電腦的「受信任的根憑證授權單位」存放區。 使用者存放區與電腦存放區的分界見「Windows 憑證存放區實務指南」。
  • 不要在程式裡關掉憑證驗證。ServerCertificateCustomValidationCallback 永遠回 true 的權宜之計,一到外部網路就變成無法偵測中間人的脆弱應用程式。
  • 憑證釘選流量本來就無法檢查。 像部分 Windows 元件那樣驗證特定 Microsoft 憑證的連線,Proxy 一換憑證就失敗,除了排除沒有其他辦法。4 對前往 Microsoft 365 等 SaaS 的流量,Microsoft 自己建議從網路層解密與檢查排除。13

「每個內部網站都看得到,只有某個雲端服務在應用程式裡出現憑證錯誤」這類症狀,應先懷疑 TLS 檢查排除清單與釘選的組合。

TLS 檢查 Proxy 會重新簽署憑證Proxy 終止 TLS、檢查內容,並呈現以自己 CA 重新簽署的憑證。只有該 CA 在信任根裡,驗證才成立。不要在程式裡關掉驗證CA 在信任根缺少 CA真正的伺服器憑證TLS 檢查 Proxy由 Proxy CA 重新簽署用戶端驗證成功憑證錯誤把 CA 發佈到存放區

圖 8: 檢查只有與發佈內部 CA 成套才運作。

8. 隔離步驟 ── 找出元兇的五步

依這個順序機械地調查「連不上」。

步驟 做什麼 學到什麼
(1) 重現 curl.exe -vInvoke-WebRequest 存取問題 URL(最好同一台電腦、同一個帳戶) 是應用程式特有問題還是環境問題
(2) 收集設定 收集三個系譜:netsh winhttp show proxy、每位使用者設定、環境變數 哪個系譜裡有什麼
(3) 識別帳戶 識別目標應用程式的執行帳戶(服務、工作排程器、其他使用者) 它以哪些設定與哪些認證執行
(4) 分類錯誤 區分 407 / 403 / 名稱解析失敗 / 逾時 / 憑證錯誤 隔離 Proxy 驗證、原則拒絕、路徑與 TLS 檢查
(5) Proxy 記錄 在 Proxy 伺服器存取記錄核對對應時間 到底有沒有到 Proxy、驗證成誰

(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 明確指定 Proxy,TLS 驗證通常用作業系統憑證存放區(Schannel)。Windows PowerShell 5.1 的 Invoke-WebRequest 跟隨 .NET Framework 這一側(預設網際網路選項);PowerShell 7 跟隨 .NET 這一側(環境變數優先)。「curl 可以、應用程式不行」本身就是設定系譜不一致的線索。
  • 若 (3) 的目標是服務,在與服務相同的帳戶下重查 (1) 與 (2)。管理員自己工作階段裡的檢查,不是 LocalSystem 看見什麼的證據。
  • (4) 的錯誤分類裡,407 的第一候選人是第 6 章(驗證),憑證錯誤是第 7 章(TLS 檢查),逾時是「沒到 Proxy」(路徑、名稱解析、防火牆)。原因是 Windows 防火牆傳入規則而不是 Proxy 的模式,見「Windows 防火牆與業務應用程式」。
  • 若走到 (5) Proxy 記錄仍無痕跡,流量從未到達 Proxy。懷疑 PAC 的 DIRECT 決定、略過清單或殘留環境變數,必要時用封包擷取確認實際目的地(「Windows 的封包擷取實務 ── 在 pktmon、netsh trace 與 Wireshark 之間選擇」)。
隔離 Proxy 故障的五個步驟在同一帳戶下重現、收集三個系譜的設定、識別執行帳戶、分類錯誤,然後查 Proxy 記錄用 curl 重現收集三個系譜識別帳戶分類錯誤查 Proxy 記錄407:驗證憑證錯誤:檢查逾時:從未到達

圖 9: 依序走完五步。錯誤類別決定下一章。

9. 設計建議 ── 把應用程式做成「可以設定 Proxy」的

把調查步驟反過來,就成了應用程式端的設計指引。對要交到有企業內部 Proxy 環境的 Windows 應用程式,建議如下。

  1. 讓 Proxy 可從應用程式設定組態。預設是「跟隨作業系統設定」。 多數環境預設就夠;只有在讀不到 PAC、以服務執行、特殊 Proxy 組態這些例外環境,才從設定檔指定 Proxy URL、略過清單與「不使用 Proxy」。第 5.3 節的 HttpClientHandler.Proxy / UseProxy 是實作點。14
  2. 寫下內部目的地(API、資料庫、授權伺服器等)如何當成 Proxy 例外處理。 以能寫進部署程序的形式記錄:是用 PAC DIRECT、略過清單還是 NO_PROXY 排除。NO_PROXY 的比對規則(沒有萬用字元、前導點代表什麼)被廣泛誤解,請附上例子。7
  3. 逾時與重試要假設會經過 Proxy 來設計。 若 Proxy 掛掉或卡在驗證,等很長預設逾時的實作會讓 UI 與營運一起凍結。分開較短的連線逾時,並把重試限於冪等請求(設計細節見「不要用 using 包住 HttpClient」)。
  4. 記錄「用了哪個 Proxy」。 讓應用程式自己能回答故障調查的第一個問題。

像 (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) 只要讀記錄就結束。當有人說「瀏覽器可以,但是……」時,能從應用程式端說「我用了這個設定、這條路徑」,就是對 Proxy 麻煩夠強的應用程式的條件。

讓 Proxy 可組態並記錄路徑預設跟隨作業系統設定,允許從應用程式設定指定明確 Proxy URL 或略過或不使用 Proxy,並把實際使用的路徑與執行帳戶一起記錄PAC 未讀 / 服務 / 特殊一般情況預設:跟隨作業系統設定例外環境?設定 URL、略過或不使用使用作業系統預設記錄路徑與帳戶

圖 10: 必須時才組態。永遠記錄用了哪條路徑。

10. 總結

  • Windows 的 Proxy 設定分成三個系譜──WinINET 每位使用者設定、WinHTTP 電腦設定、環境變數──讀哪一套由應用程式(其 HTTP 堆疊)與執行帳戶決定。
  • WinINET 給互動式應用程式,不支援在服務中使用;服務用途是 WinHTTP(netsh winhttp)的工作。「手動可以、做成服務就不行」先懷疑執行帳戶不同。
  • netsh winhttp set proxy 是靜態設定,不處理 PAC、自動偵測或驗證。在以 PAC 營運的網路上,必須決定無法讀 PAC 的用戶端如何處理。
  • PAC 的 FindProxyForURL 依 URL 回傳 Proxy 或 DIRECT。WPAD 只在有 DHCP/DNS 安排的網路上運作。
  • .NET Framework 的預設是執行帳戶的網際網路選項(可用 defaultProxy 覆寫);.NET(Core 以後)是環境變數然後使用者 Proxy 設定。明確指定(HttpClientHandler.Proxy)永遠最高優先。
  • 407 是 Proxy 驗證錯誤;以服務帳戶執行的應用程式裡,典型原因是「預設認證」變成另一個人。
  • TLS 檢查 Proxy 以發佈內部 CA 憑證為前提,憑證錯誤的正確答案是發佈到憑證存放區,不是關掉驗證。釘選流量需要排除。
  • 依「重現 → 收集三個系譜的設定 → 識別執行帳戶 → 分類錯誤 → Proxy 記錄」的順序機械隔離。應用程式端,「能設定 Proxy,並記錄它用的路徑」的設計是最好的預防。

下次有人來諮詢「只有業務應用程式連不上」時,先問這個。

那個應用程式以誰的帳戶執行,它讀的是 Proxy 設定三個系譜裡的哪一個?

這一問會大幅改變調查的入口。

相關文章

相關諮詢領域

小村軟體有限公司承接企業內部 Proxy、需驗證 Proxy 與 TLS 檢查環境下 Windows 應用程式通訊障礙的調查──「開發機可以、客戶網路不能通訊」、「做成服務後就到不了外部 API」──以及預設有 Proxy 環境的業務應用程式通訊設計(設定項目、逾時、記錄設計)諮詢。從整理重現步驟與如何收集記錄開始也可以。

參考連結

  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(Proxy、ProxyBypass、AutoconfigUrl、AutoDetect)。  2 3 4

  3. Microsoft Learn, About WinHTTP. 關於 WinHTTP 是為服務與伺服器端用途設計的 HTTP 堆疊,支援以服務帳戶執行與模擬,且不分享瀏覽器的 Cookie、快取、認證或使用者的網際網路選項。  2 3

  4. Microsoft Learn, Using a proxy with Delivery Optimization. 關於 netsh winhttp set proxy 是不支援自動偵測、PAC URL 或 Proxy 驗證的靜態設定;沒有登入使用者之情境的裝置層級 Proxy 組態(NetworkProxy CSP、「將 Proxy 設定設為電腦層級」原則);以及憑證釘選流量在 TLS 檢查下失敗而需要排除。  2 3 4 5 6 7 8 9 10

  5. Microsoft Learn, WinHTTP AutoProxy Support. 關於 PAC 指令碼包含依請求計算 Proxy 清單的 FindProxyForURL(url, host) 函式、以特殊回傳值表示直接連線,以及較舊的 AutoProxy API 不會把自動 Proxy 自動整合進 HTTP 堆疊,因此應用程式必須呼叫 WinHttpGetProxyForUrl。  2 3

  6. Microsoft Learn, WinHttpGetProxyForUrl function. 關於它是 WPAD 通訊協定的實作,因為 PAC 檔可依 URL 回傳不同 Proxy 而必須依 URL 呼叫,以及同時支援明確 PAC URL 與從網路自動偵測。  2

  7. Microsoft Learn, HttpClient.DefaultProxy Property. 關於 Windows 先讀 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY、NO_PROXY 環境變數,未定義時讀使用者 Proxy 設定;Linux 在沒有環境變數時以沒有 Proxy 初始化;NO_PROXY 不支援萬用字元並使用前導點子網域比對;以及 Proxy URL 可包含使用者名稱與密碼。  2 3 4 5 6 7 8 9

  8. Microsoft Learn, Configuring Internet Applications. 關於 .NET Framework 上 defaultProxy 元素定義預設 Proxy;沒有 Proxy 屬性的 HttpWebRequest 使用預設 Proxy;以及系統網際網路設定與設定檔設定結合且設定檔端優先。  2 3

  9. Microsoft Learn, defaultProxy element (network settings). 關於 system.net/defaultProxy 元素的 enabled 與 useDefaultCredentials 屬性、proxy、bypasslist 與 module 子元素、元素為空時使用系統 Proxy 設定,以及遷移到 .NET 6 以後時用 HttpClient.DefaultProxy 組態。  2 3

  10. Microsoft Learn, Authentication in WinHTTP. 關於需要 Proxy 驗證時回傳狀態碼 407 與 Proxy-Authenticate 標頭(伺服器驗證是 401 與 WWW-Authenticate);Basic 驗證與 Kerberos 等挑戰/回應配置的差異;以及挑戰/回應配置代表使用者名稱與密碼不走網路。  2 3

  11. Microsoft Learn, HttpClientHandler.DefaultProxyCredentials Property. 關於在 UseProxy 為 true 且 Proxy 為 null 而使用系統預設 Proxy 時,設定用來向該預設 Proxy 驗證之認證的屬性。  2

  12. Microsoft Learn, WebProxy.Credentials Property. 關於 Credentials 屬性是回應 HTTP 407 時送給 Proxy 的認證,以及許多用戶端情境建議將 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 解密)是 Proxy 或防火牆解密、檢查並重新加密 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 以後)從系統/使用者 Proxy 設定自動決定 Proxy,並自動處理容錯移轉與驗證;WINHTTP_ACCESS_TYPE_DEFAULT_PROXY 自 8.1 起淘汰。 

  16. Microsoft Learn, Work with existing on-premises proxy servers. 關於對外 HTTPS 以向 Proxy 的 CONNECT 請求建立;成功回 HTTP 200;以及 407(需要驗證)或 502 等回應表示 Proxy 未允許通訊,因此應與 Proxy 端團隊一起推進隔離。 

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

瀏覽器連得上,只有業務應用程式過不了企業內部 Proxy。為什麼?
瀏覽器讀的是 WinINET 的每位使用者 Proxy 設定,但業務應用程式不一定讀同一套。以 Windows 服務執行、或以其他帳戶執行的應用程式,會查詢該帳戶看得到的設定、WinHTTP 的電腦設定,或環境變數。先確認執行帳戶,再用 netsh winhttp show proxy 與使用者設定兩邊檢查該帳戶看得到的 Proxy 設定。若能在同一台電腦、同一個帳戶下用 curl.exe 等重現,就可把它當成設定系譜之間的不一致,而不是應用程式本身的問題。
我設了 netsh winhttp set proxy,應用程式的流量卻沒變。為什麼?
netsh winhttp 設的是 WinHTTP 的電腦預設值。它不影響讀 WinINET 的瀏覽器或互動式應用程式,也不影響優先讀環境變數的 .NET(Core 以後)HttpClient。netsh winhttp set proxy 也是靜態設定,不處理 PAC 自動設定、自動偵測或 Proxy 驗證。你得先確認目標應用程式用哪一套 HTTP 堆疊、從哪個設定系譜解析 Proxy。
.NET 應用程式讀哪些 Proxy 設定?
.NET Framework 預設使用執行帳戶的網際網路選項(等同 WinINET)設定,並可用 app.config 的 system.net/defaultProxy 元素覆寫。Core 以後的 HttpClient 先讀 HTTP_PROXY、HTTPS_PROXY、NO_PROXY 等環境變數,未定義時才回落到 Windows 使用者 Proxy 設定。兩種情況都是明確的 HttpClientHandler.Proxy 優先。Framework 與 Core 以後的預設解析順序不同,遷移時必須重新檢查 Proxy 行為。
收到 407 Proxy Authentication Required 時該查什麼?
407 表示 Proxy 本身在要求驗證,與目的地伺服器的驗證錯誤(401)是兩件事。先從 Proxy-Authenticate 標頭確認 Proxy 要求的驗證配置(Negotiate、NTLM、Basic),並在 .NET 用 HttpClientHandler.DefaultProxyCredentials 或 WebProxy.UseDefaultCredentials 傳入認證。以服務帳戶執行的應用程式裡,「預設認證」會變成該服務帳戶的認證,因此常見事故是互動式使用者沒問題,一做成服務就 407。也請查 Proxy 端記錄它驗證成誰。
TLS 檢查 Proxy 造成憑證錯誤。可以關掉憑證驗證嗎?
不建議關掉。TLS 檢查 Proxy 會解密流量,再把以自己 CA 重新簽署的憑證呈現給用戶端,若該 CA 憑證不在信任的根憑證中,驗證就會失敗。正確作法是把內部 CA 憑證發佈到 Windows 憑證存放區(通常是本機電腦的「受信任的根憑證授權單位」)。在程式裡關掉驗證,代表應用程式拿到外部網路時無法偵測中間人攻擊,漏洞會一直留下。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽