某台透過 .NET WebClient 物件爬網頁抓資料排程忽然出現:

基礎連接已關閉: 傳送時發生未預期的錯誤。 ---> System.IO.IOException: 驗證失敗,因為遠端群體已經關閉傳輸資料流。
The underlying connection was closed: An unexpected error occurred on a send. ---> System.IO.IOException: Authentication failed because the remote party has closed the transport stream

有趣的是,上回同一排程就發生過類似狀況,原本早上還執行得好好的,近中午時開始出錯。二次出錯的時點幾乎相同,推測是該網站的固定換版上線時間。

對照測試,使用 IE 或 Chrome 開啟網頁正常,只有透過 WebClient 取回 HTML 內容時才出錯,在本機測試也發生相同錯誤。印出 Exception 內容如下:

ERROR=System.Net.WebException: 基礎連接已關閉: 傳送時發生未預期的錯誤。 ---> System.IO.IOException: 驗證失敗,因為遠端群體已經關閉傳輸資料流。
   於 System.Net.Security.SslState.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
   於 System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
   於 System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
   於 System.Net.Security.SslState.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
   於 System.Net.Security.SslState.ForceAuthentication(Boolean receiveFirst, Byte[] buffer, AsyncProtocolRequest asyncRequest)
   於 System.Net.Security.SslState.ProcessAuthentication(LazyAsyncResult lazyResult)
   於 System.Net.TlsStream.CallProcessAuthentication(Object state)
   於 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   於 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   於 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   於 System.Net.TlsStream.ProcessAuthentication(LazyAsyncResult result)
   於 System.Net.TlsStream.Write(Byte[] buffer, Int32 offset, Int32 size)
   於 System.Net.PooledStream.Write(Byte[] buffer, Int32 offset, Int32 size)
   於 System.Net.ConnectStream.WriteHeaders(Boolean async)
   --- 內部例外狀況堆疊追蹤的結尾 ---
   於 System.Net.WebClient.DownloadDataInternal(Uri address, WebRequest& request)
   於 System.Net.WebClient.DownloadString(Uri address)
   於 System.Net.WebClient.DownloadString(String address)

錯誤訊息明確指向 SSL,爬文後恍然大悟,原來跟上回研究過的 TLS 1.0 停用議題有關,研判對方網站調整系統停用了較不安全的 TLS 1.0,依上回心得,.NET 客戶端使用 WebClient、WCF 以 HTTPS 連線遠端主機,也會涉及 TLS 1.0/1.1/1.2 版本議題,不同版本 .NET 的處理方式不同:

  • .NET 4.6 內建支援且預設使用 TLS 1.2
  • .NET 4.5 內建支援,但需透過 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 設為預設協定
  • .NET 4 本身不支援,但安裝 .NET 4.5 後即可使用 TLS 1.2,指定 TLS 1.2 的寫法為 ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;

我的程式是 .NET 4.5,在調整 ServicePointManager.SecurityProtocol 設定擴大 SSL/TLS 版本範圍後,問題排除:(若改程式不方便,亦有修改 Registry 的解法,請參考前文

        static void Main(string[] args)
        {
            try
            {
                WebClient wc = new WebClient();
                //REF: https://stackoverflow.com/a/39534068/288936
                ServicePointManager.SecurityProtocol = 
                    SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | 
                    SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
                string res = wc.DownloadString("httqs://some-server/test");
                Console.WriteLine(res);
            }
            catch (Exception ex)
            {
                Console.WriteLine("ERROR=" + ex.ToString());
            }
            Console.ReadLine();
        }

TLS 1.0 已被視為不安全,近期應會被各大網站陸續停用,使用 .NET 讀取網站資料遲早要對類似問題,宜多加留意。


Comments

# by Alex

有考慮改用這樣的方法嗎? ServicePointManager.SecurityProtocol = GetSecurityProtocol(); private static SecurityProtocolType GetSecurityProtocol() { var result = 0; foreach(var value in Enum.GetValues(typeof(SecurityProtocolType))) result += (int) value; return (SecurityProtocolType) result; }

# by Jeffrey

to Alex, 這方法挺好,但因為涉及資安,個人覺得正向表列想開放的項目更安全一些。

# by FalseApple

感謝大大, 今天也採到這個坑了

# by Roger

今天換我踩到這個坑,一改程式就解決了,感謝大大,黑暗執行緒這邊的文章幫我解決了不少怪問題!

# by 好奇

今天也遇到這個問題~謝謝大大提供解法

# by ivan

3Q 黑大

# by Link

最近真的是一直在踩TLS1.2的坑,上網一查就又是黑大的文章,真是太感謝了

# by Suming

果然也遇到了, 感謝黑大的分享, 受益良多

# by Yirung

Thanks!!

# by 小螺絲

黑大您好, 小弟我也遇到這個問題, 透過您的方式順利解決了 (開發環境: Win10). 但發現該api使用TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256這個Cipher suite, 僅Win10/Server 2016以後才有支援 在Server 2012 r2 上測試發現狀況依舊, ie也無法順利開啟. 但經測試, python所寫的測試code跟chrome在Server 2012 r2是能順利交握取得api資料的. 不知黑大是否有遇過類似的狀況?

# by Jeffrey

to 小螺絲,確認一下,你的狀況是「某 API Client 與 IE 在 Windows 2012 R2 上無法連上某 API Server,但使用 Python 及 Chrome 在該 2012R2 上連線同一台 API Server 卻沒問題?」 2012R2 的 IE 有升級到 IE11 嗎?API Client 限定只能用特定 Cipher 連 TLS 的案例我倒沒聽過(我以為會支援多種,視狀況選用其一),應該不是 .NET 程式吧?

# by 小螺絲

是的, 業主端與公司這邊的測試機都有升級到ie11. 用Postman測試, 也可以連得到並取得資料, 並得知應該是架在iis上的.NET... 因為只有撰寫的.net程式跟ie無法順利交握, 所以感到很神奇@@""

# by Jeffrey

to 小螺絲,聽起來像是 2012R2 的作業系統或 .NET 設定沒設好造成的,Chrome 跟 Postman、Python 用自己的 TLS 連線程式庫沒問題,IE 跟 .NET Client 都依賴 Windows 的設定因此壞掉。另一種可能是反過來,2012R2 只留了 TLS 1.2,而 API Server 沒設定好不支援 TLS 1.2,Chrome/Postman/Python 還 Support TLS 1.0/1.1 反而可以通。那個限定 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 Cipher 的資訊是從哪裡來的?

# by Bryan

to 小螺絲: 我也碰到相同問題,對方的網管指定只開tls1.2 + 特定的cipher,目前我的.NET程式也無法連至對方API,但可以連上同主機走同樣protocal的網站(用chrome跟IE都可以)

# by Jason Cheng

感謝這篇,今天也受用了!

# by Troy Chi

救了我一天,這篇到今天還是持續在救人^0^

# by OeOb

感謝!

# by Lance

請問黑大,我目前也遇到一樣的問題,原排程打API(使用HttpClient)都正常,但發布新版本之後(打API的code都沒異動過),打API就會出現"System.Net.WebException: 要求已經中止: 無法建立 SSL/TLS 的安全通道。",但是退版後就又正常,您覺得可能是什麼問題?

# by Jeffrey

to Lance,這種案例算單純,保證有解,全力比對新舊版差異找線索,查不出來還有大絕 - 一點一點把差異加進舊版,一定可破案。

# by 阿光

我在.Net Framework 4.8用XmlDocument.Load一個RSS網站也會有問題,後來用.Net core 6一樣的方法就過了,真是太坑了。

# by YU

您好~ .NET CLR 版本是4.0 改這個會影響嗎?

# by Jeffrey

to YU,接近文末有 .NET 4/4.5/4.6 的處理方式,不知你說的"改"指的是什麼?

# by max

您好,.NET CLR 版本是2.0 的話,有處理的方式嗎?

# by Jeffrey

to max, 我沒有處理過 2.0,但 3.5 的經驗是安裝最新版 .NET Framework Update + 設 Registry 搞定。https://blog.darkthread.net/blog/net35-tls12-issue/

Post a comment