前情提要:突如其來的測試環境危機

最近台電因為白帽滲透測試的關係,需要暫時關閉測試機。但開發團隊偶爾還是需要測試環境來驗證功能,於是我們決定把程式架在其他環境上。原本想說簡單設個 IIS IP 白名單就能解決,沒想到踩到了一個大坑...

問題出現:IP 白名單完全失效

當我興高采烈地在 IIS 管理器中設定「IP 位址及網域限制」,把信任的 IP 加入白名單後,發現一個詭異的現象:

  • 設定為「允許所有」→ 正常運作 ✅
  • 設定為「拒絕未指定,只允許白名單」→ 所有人都 403 錯誤 ❌

檢查 IIS Log 後發現真相大白:

1
c-ip: 192.168.0.28  # 所有請求都顯示這個 IP!

原來所有外部請求都是透過 192.168.0.28 這台 Proxy Relay 伺服器轉發過來的。IIS 只看得到代理伺服器的 IP,完全拿不到真實的客戶端 IP。

深入追查:尋找真實 IP 的蹤跡

建立一個簡單的測試頁面來檢查所有可能的 HTTP 標頭:

1
2
3
4
5
6
7
8
9
10
11
12
<%@ Page Language="C#" %>
<!DOCTYPE html>
<html>
<head><title>IP 偵測</title></head>
<body>
<%
Response.Write("REMOTE_ADDR: " + Request.ServerVariables["REMOTE_ADDR"] + "<br>");
Response.Write("HTTP_X_FORWARDED_FOR: " + Request.ServerVariables["HTTP_X_FORWARDED_FOR"] + "<br>");
Response.Write("HTTP_X_REAL_IP: " + Request.ServerVariables["HTTP_X_REAL_IP"] + "<br>");
%>
</body>
</html>

測試結果發現關鍵線索:

1
2
REMOTE_ADDR: 192.168.0.28
HTTP_X_FORWARDED_FOR: 125.228.130.66:12872

太好了!代理伺服器有正確傳送真實 IP,只是格式是 IP:PORT

解決方案一:URL Rewrite 改寫 REMOTE_ADDR

首先確認 IIS 已安裝 URL Rewrite 模組,然後在 web.config 中加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<system.webServer>
<rewrite>
<rules>
<rule name="Extract Real IP" stopProcessing="false">
<match url=".*" />
<conditions>
<add input="{HTTP_X_FORWARDED_FOR}" pattern="^([^:]+)" />
</conditions>
<serverVariables>
<set name="REMOTE_ADDR" value="{C:1}" />
</serverVariables>
<action type="None" />
</rule>
</rules>
</rewrite>
</system.webServer>

重要: 需要在 IIS 的「URL 重寫」功能中先新增允許修改的伺服器變數:

  1. 點選「URL 重寫」
  2. 右側「檢視伺服器變數」
  3. 新增 REMOTE_ADDR

設定完成後測試:

1
REMOTE_ADDR: 125.228.130.66  # 成功改寫!

意外發現:IIS IP 限制的執行時機陷阱

興奮地以為問題解決了,結果發現 IIS 的「IP 位址及網域限制」還是不行!

深入研究後發現:IIS 的 IP 限制功能執行在請求處理管道的早期階段,在 URL Rewrite 之前。所以它仍然看到的是原始的 c-ip,而不是改寫後的 REMOTE_ADDR

這解釋了為什麼 IIS Log 中:

  • c-ip 仍然是 192.168.0.28
  • X-Forwarded-For 正確顯示真實 IP
  • URL Rewrite 成功但 IP 限制失效

解決方案二:程式碼層面的 IP 控制

既然 IIS 內建功能不給力,我們就自己來!在 Global.asax.cs 中實作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected void Application_BeginRequest(object sender, EventArgs e)
{
// 檢查是否啟用 IP 限制
bool enableIPRestriction = ConfigurationManager.AppSettings["EnableIPRestriction"] == "true";

if (enableIPRestriction && Request.Url.AbsolutePath.ToLower().StartsWith("/stationmanager"))
{
string allowedIPsConfig = ConfigurationManager.AppSettings["AllowedIPs"] ?? "";
string[] allowedIPs = allowedIPsConfig.Split(',').Select(ip => ip.Trim()).ToArray();

string forwardedFor = Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
string clientIP = "";

if (!string.IsNullOrEmpty(forwardedFor))
{
clientIP = forwardedFor.Split(':')[0]; // 移除 port 部分
}

if (!allowedIPs.Contains(clientIP))
{
Response.StatusCode = 403;
Response.Write("Access Denied");
Response.End();
return;
}
}

// 其他處理邏輯...
}

最佳實務:環境分離的設定管理

為了避免正式機和測試機搞混,在 web.config 中加入可設定的開關:

1
2
3
4
5
<appSettings>
<!-- IP 限制設定 -->
<add key="EnableIPRestriction" value="true" />
<add key="AllowedIPs" value="125.228.130.66,另一個IP" />
</appSettings>

這樣就能:

  • 測試機EnableIPRestriction = true
  • 正式機EnableIPRestriction = false

加碼:IIS Log 顯示真實 IP

為了方便追蹤,可以讓 IIS Log 也顯示真實 IP:

  1. IIS 管理器 → 網站 → 「記錄」
  2. 「選取欄位」→ 「自訂欄位」
  3. 新增:
    • 欄位名稱:X-Forwarded-For
    • 來源:Request Header
    • 來源名稱:X-Forwarded-For

之後 Log 就會顯示:

1
c-ip: 192.168.0.28  X-Forwarded-For: 125.228.130.66:14185

踩坑總結

這次解決問題的過程學到幾個重點:

網路架構很重要
有代理伺服器的環境下,要搞清楚真實 IP 的傳遞方式。

IIS 功能的執行時機
不是所有 IIS 功能都能配合 URL Rewrite,要了解請求處理管道的順序。

設定檔化設計
多環境部署時,要設計成可設定的方式,避免程式碼硬編碼。

善用除錯工具
建立簡單的測試頁面可以快速定位問題,比盲目調整設定有效多了。

後記

原本以為是個簡單的 IP 白名單設定,結果挖出了代理伺服器、IIS 請求管道、URL Rewrite 等一堆技術細節。雖然過程有點曲折,但最終找到了一個既實用又彈性的解決方案。