第七章﹕架設 NAT


當您好不容易把 Linux 機器連上 Internet 之後﹐別以為就只有它一台機器能上網哦。下面教您的法子是如何用一台 Linux 機器讓本地網路中所有的機器都能同時上網﹗

設定 Proxy

我們在“網路基礎”中的“防火牆”那邊已經知道﹐能做到以上要求的方法很多﹐之一是使用 proxy 。至於它的好處﹐前文已經說了許多﹐這裡就不再重複了。假如您只想讓其他機器到 Internet 上流覽一下網頁或透過 HTTP 下載檔案﹐無疑 proxy 是個不錯的主意﹐而且要設定這樣一個 proxy 也是非常容易的﹐您只需確定 squid 套件有裝好、並且能在開機的時候啟動(執行 ntsysv 來選擇)就行了。

然後﹐您要做的只是修改 /etc/squid/squid.conf 這個檔案。雖然這個檔案看上去很長﹐但是﹐如果僅是用來做 HTTP proxy 的話﹐您要修改的句子不超過兩句﹕

# 在第 1163 行下面增加一行﹕
acl siyongc src 192.168.100.0/255.255.255.0 # 請修改為您自己的實際設定

# 然後在第 1197 行下面再增加一行﹕
http_access allow siyongc	# 請修改為上一行定義的 acl 名稱

然後重新跑 squid 就好了﹕

service squid restart

接下來﹐您將其他機器的流覽器上之 proxy 功能打開﹐並將伺服器 IP 指向 squid 主機﹐同時將 port 設定為 3128 就可以了。如果您用 IE 的話﹐按‘工具’--> ‘Internet 選項’--> ‘連線’ --> ‘區域網路設定’ --> ‘使用 Proxy 伺服器’ --> 然後填好‘網址’與‘連接埠’﹕


圖一

這裡只是最單純的設定而已﹐事實上﹐ squid 的設定非常多樣和複雜﹐如果您有興趣的話﹐可以參考‘ 優客筆記簿 ’中的文件。

正如您剛纔發現的﹕在 Linux 上面設定一個簡單的 proxy 看來是很容易的事情﹐但畢竟使用 proxy 是非常有限制的﹐例如﹐您要玩 ICQ、要使用 telnet、要上傳 FTP 、要使用外面的 POP 信箱、要閱覽新聞組或 BBS、等等﹐恐怕 proxy 就未必能全部應付得來了。不過﹐假如您使用 IP Masquerading ( IP 偽裝 --- NAT 的一種)技術又如何呢﹖那情形幾乎是﹕您在那台用來上網的 Linux 機器上所使用的 internet 功能﹐在其他機器也一樣擁有﹗

前提準備

要設定 NAT ( Network Address Translation ) 就牽涉到防火牆裝置了。老實說﹐要設定一個稱職的防火牆並不是一件容易的事情﹐它要求您有非常豐富的 TCP/IP 基礎、和嚴密的邏輯頭腦、還得加上無以複加的細密測試。但是﹐我在這裡教您的﹐並不是要真正的設定一個火牆﹐只是想讓其他機器上網罷了。如果您真想設定防火牆﹐您可以看看 Firewall、IP Masquerade 和 IPchains 這幾篇 HOWTO 文件﹐或許才會有些概念(但未必就設定得來)。而且﹐在閱讀的時候﹐要注意文章引用的版本是否和您目前機器使用的相同﹐因為不同的版本所使用的工具和命令格式都相差很遠﹐但基本的原理是不變的。

首先﹐您要知道﹐在 Linux 上面的防火牆程式都與核心版本非常密切﹕

  • 如果您是用 2.0.x 的核心﹐那您只能使用 ipfwadm 程式﹔
  • 如果是 2.2.x 的核心﹐則使用 ipchains 程式﹔
  • 如果是 2.4.x 的核心﹐則使用 iptables 程式( ipchains 仍可以透過 modules 方式載入使用)。

因為 RedHat 7.1 預設是使用 2.4.2 核心﹐所以我們這裡打算以 iptables 來做範例。如果您的核心參考 system 系列之‘ 編譯核心 ’文章編的﹐那就已經具備防火牆功能了。如果您目前的版本是使用 2.2.x 核心的話﹐請參考 RH6.2 版本的 舊文章 ﹐這裡將不再以 ipchains 來作說明了。

Tips﹕如果您對 ipchains 比較熟識、而決定繼續在 2.4.x 核心上面使用 ipchains 的話﹐那也可以執行 modprobe ipchains 將 ipchains 的模組載入(但無論如何﹐ipchains 與 iptables 模組不能同時載入)。不過﹐2.2.x 核心原來提供的一些模組(如 ip_masq_*.o )就沒辦法使用了﹐這點請您留意。

另外﹐您還可以執行 ntsysv 將 ipchains 選擇起來﹐並將 ipchains 的設定寫進 /etc/sysconfig/ipchains 檔案中(可執行 setup 選擇 ‘Firewall Configuration’來設定這個檔)﹐那麼在開機的時候就會將 ipchains 跑起來了。

不過﹐我們這裡只打算使用 iptables ﹐所以﹐以上方法僅供已有 ipchains 經驗的朋友們參考﹐請勿和下面的說明搞混了。

好了﹐下面我跟大家說說如何在 RedHat7.1 上面用 iptables 來做一些防火牆和 NAT 的功能。但我必須再一次指出﹕設定防火牆需要您具備非常豐富的 TCP/IP 基礎。最起碼﹐您能理解如下的概念﹕

  • IP subneting 與 IP routing 的概念﹔
  • TCP 與 IP 封包 header 的各個欄位之名稱與意義﹔
  • Socket Pair (Source Socket 與 Destination Socket) 的定義﹔
  • TCP 封包之 Sequence Number 與 Acknowledgement Number 的關係﹔
  • IP fragment 與 MTU/MRU 的關係﹔
  • ICMP 協定的 type & error code 之名稱與意義﹔
  • Three-way Handshake 的過程與 SYNC 封包的意義﹔
  • TCP/IP 連線的不同連線狀態(例如 NEW、WAITED、ESTABLISEHED、等)
  • 各種服務的連線特徵(尤以 FTP 服務為代表)。

假如您弄不懂以上所說的概念而硬要設定防火牆﹐那將是一件非常痛苦的事情(呵﹐我可把話說在前面了哦~~)。如果您真的決心弄明白這些概念﹐那您可以參考如下文章所列的書目清單﹕請推薦有關網路的書.... ﹐花點時間了解 TCP/IP 這對寶貝。要不然﹐當問題發生的時候﹐就算您要上來和大家請教和討論﹐人家也不知道如何幫您解釋才好、或是人家說了您卻聽不懂﹐因為都必須要牽涉到底層的 TCP/IP 基礎知識。

好吧﹐我假設您已經補習好基礎了哦~~ 然則的話﹐Let's go!

設定 IP Masquerading

我們這裡的網路環境基本上與第一章所介紹的環境一樣﹕


圖二

這裡﹐作為 NAT 機器的主機就是 RH71 ﹐它目前有三張網路卡﹐而我這裡是將 adsl 接到 eth1 上面去﹐並且完成了 adsl-setup 、也已經順利連上 Internet 了﹔執行 ipconfig 確定 ppp0 界面已經起來。

註﹕我們在接下來的敘述中﹐都一律以 ppp0 作為對外界面﹔如果您不是使用撥接連接﹐或是撥接界面不是 ppp0 的話﹐請您自行以該界面代替 ppp0 。

Tips﹕如果您使用撥接式 ADSL ﹐並向內部網路提供 NAT 服務的話﹐用一張網路卡就夠了﹐反正 adsl 會使用 ppp0 而非 eth0 作為對外界面。但如果您是使用固接式 ADSL﹐ 並且不是以 ppp 而是實體網路卡( ethX 界面)的話﹐那我建議您最好使用多片網路卡﹐分別用來連接外部和內部網路(甚至 DMZ 網路)。如果您用一張網路﹐然後以 IP Alias 方式同時連接外部和內部網路﹐單純提供 IP 偽裝服務而不設定封包過濾的話﹐那是可以的﹐但我還是不鼓勵如此做法。

這時候在內部網路準備一台測試主機(跑 client 端程式)﹐並確定它能夠連上 RH71 主機、而且 default gw 也指向 RH71 的內部界面。

當我們的環境準備好之後﹐接下來就可以開始 NAT 的設定了﹕

  1. 無論如何﹐第一件事情要做的是確定您的系統上有安裝 iptables 套件﹐否則請用 RPM 從 CD 裡面安裝(RH7.1 CD1)。

  2. 然後﹐您必須將 Firewall 功能編進核心裡面去。如果您的核心是按照‘ 編譯核心 ’文章的例子編的﹐應該就不成問題了:

    Networking options  --->
        [*] Network packet filtering (replaces ipchains)
        [*] TCP/IP networking 
        IP: Netfilter Configuration  ---> 
    	<M>   Connection tracking (required for masq/NAT) (NEW) 
    	<M>   FTP protocol support (NEW)
    	<M>   IP tables support (required for filtering/masq/NAT) (NEW)
    	<M>   limit match support (NEW)
    	<M>   MAC address match support (NEW)
    	<M>   netfilter MARK match support (NEW)
    	<M>   Multiple port match support (NEW)
    	<M>   TOS match support (NEW)
    	<M>   tcpmss match support (NEW)
    	<M>   Connection state match support (NEW)
    	<M>   Packet filtering (NEW)
    	<M>   REJECT target support (NEW) 
    	<M>   Full NAT (NEW)
    	<M>   MASQUERADE target support (NEW) 
    	<M>   REDIRECT target support (NEW) 
    	<M>   Packet mangling (NEW)
    	<M>   TOS target support (NEW)
    	<M>   MARK target support (NEW)
    	<M>   LOG target support (NEW)
    	<M>   TCPMSS target support (NEW)  
    	<M>   ipchains (2.2-style) support (NEW) 
    	< >   ipfwadm (2.0-style) support (NEW) 
    

    提示﹕假如您的系統並沒作過任何的核心修改﹐換句話說﹕您一直都是使用系統安裝時所準備的預設核心﹐那麼這個步驟可以省略了。除非您發現某些模組確實需要重編才有。

  3. 然後﹐檢查一下 /etc/sysctl.conf 這個檔案﹐看看 ‘net.ipv4.ip_forward=’是否為‘1’。如果是﹐跳到下一步﹔如果不是﹐將之改成‘1’﹐存檔﹐然後執行 ﹕

    sysctl -w net.ipv4.ip_forward=1

    Tips﹕上面這步驟是將 ip_forward 功能打開﹐您也可以用如下句子來完成﹕

    echo "1" > /proc/sys/net/ipv4/ip_forward

  4. 執行 ntsysv 將 iptalbes 選擇起來﹐並且確定 ipchains 沒有選擇。

  5. 檢查一下 ipchains 的模組是否被‘意外’載入了﹕

    lsmod | grep ipchains

    如果輸出結果沒有發現 ipchains 的話﹐跳到下一步驟﹔如果有發現﹐則執行﹕

    service ipchains stop
    rmmod ipchains

    並重複執行 lsmod 確定 ipchains 模組已經移除。

  6. 檢查 iptables 的模組是否已經載入﹕

    lsmod | grep ip_tables

    如果有請跳到下一步驟﹔否則執行﹕

    modprobe ip_tables

    並再次執行 lsmod 確定 iptables 模組已經成功載入。

  7. 如果您不考慮任何安全性﹐現在執行﹕

    iptables -t nat -A POSTROUTING -o ppp0 \
        -s 192.168.100.0/24 -j MASQUERADE

就這樣﹐您就可以讓 NAT 後面的內部網路的機器連上 Internet 了﹗現在﹐您可以取消 proxy 設定並嘗試一下 web 連線﹐或用郵件程式上網收發信件看看﹖甚至還可以打開 ICQ 和外面的朋友聊天呢﹗當然了﹐什麼 BBS 或新聞群組這些也都可以啦~~ ^_^

設定 Transparent Proxy

您或許會覺得繼續使用 proxy 仍不失為一個好主意﹐尤其是當您考量到頻寬問題的時候﹐甚至您還可以在使用者在不知情的情況下用 proxy 來瀏灠 www 網站。這時候﹐您就可以考慮建立一個 Transparent Proxy 了﹕

  1. 首先﹐修改 /etc/squid/squid.conf﹐找到下面幾行﹐並修改為如下樣子﹕

    httpd_accel_host rh71.siyongc.domain	# 請修改為您的 squid 主機名稱
    httpd_accel_port 80
    httpd_accel_with_proxy on
    httpd_accel_uses_host_header on
    

    並重新啟動 squid 服務﹕

    service squid restart

  2. 然後執行﹕

    iptables -t nat -A PREROUTING -i eth0 -p tcp \
        -s 192.168.100.0/24 \
        --dport 80 -j REDIRECT --to-ports 3128

這時候﹐您的 Transparent Proxy 就起來了﹗如果您要測試它﹐可以取消 client 端的 proxy 設定﹐並將 squid 關閉﹐然後測試是否不能連線(建議用一個未曾瀏覽過的網址來測試)﹖然則﹐再將 squid 打開﹐如果能這樣又能恢復連線的話﹐那就已經成功了﹗這樣有一個好處是﹕以後您再也不必跑到 client 那邊設定 proxy﹔而且﹐更好的地方在於﹕重複性的連線再也無需佔用寶貴的對外頻寬﹐速遞當然也能‘假性’的獲得提高。

工作原理

然而﹐以目前的設定來說﹐可以說是完全開放的﹐毫無安全性可言的。雖然我在這裡並不想設定一個真正火牆﹐但起碼的安全也是應該考慮的。當我們提到防火牆的時候﹐大部份都是指 Packet Filtering (封包過濾)﹐這和前面提到的封包偽裝(也就是 NAT 的一種技術)可不是一樣的。這裡面﹐我們不妨先了解一下封包過濾的工作原理﹐這對於我們日後的防火牆管理非常重要﹗

讓我們回到 TCP/IP 的基礎知識吧(順便檢查一下您是否能夠正確理解我在前面列出的概念)﹕

Socket Pair

首先﹐讓我們了解一下什麼是 Socket Pair 。所謂 socket 就是一個‘ IP 位址’加上一個‘ TCP/UDP Port ’﹐代表了一個連線與哪台機器( IP 位址)、及與機器上那一隻程式( Port ) 相連的。我們同時也知道﹕一個連線必須有兩個端點﹕來源地( Source ) 和 目的地( Destination ) 。換句話說﹐我們一個連線就是與一對 socket 相連著﹕分別是 Source Socket ( Source Address & Source Port) 與 Destination Socket ( Destination Address & Destination Port ) ﹐合起來我們稱之為 Socket Pair ﹕

Tips﹕如果您了解 TCP 與 IP 封包 header 結構的話﹐都非常容易找到上述提到的欄位。一般而言﹐防火牆程式大都會檢查這些欄位來進行判斷﹐然後根據您的設計要求來處理封包。當然﹐事實上﹐防火牆程式除了根據 Socket 資訊來判決封包命運之外﹐幾乎封包的每一個欄位都可以作為判斷依據。這也就是防火牆(或封包過濾)程式能耐之處了。

事實上﹐ Socket 的功能就是讓網路程式存取網路使用的﹐對程式而言﹐關鍵是它所被分配的 Port 數值。而 port 也有分兩種﹕ TCPUDP ﹔至於哪些服務用哪種 port﹐端視服務程式的要求(您都可以在 /etc/services 這個檔案中找到)。一般而言﹐單一的機器最多能使用的 port 範圍是在 1 到 65535 之間。其中的 1 ~ 1023 這段範圍只能由 root 權限的服務程式使用﹐通常是一些常用的服務程式﹐如 HTTP、FTP、SMTP、等。我們稱這些常用的 port 為 Well-known Port﹐基本上大家都會採用相同的數值﹐如 80、21、25 等(事實上是未必的﹐只要能讓 client 找到就好﹐如地下網站就通常使用不為人知的 port 數值)。但是﹐作為 client 端程式所使用的 port ﹐我們沒辦法事先知道它會用哪一個 port 。因為 client 程式所使用的 port 會隨機的從 1024 ~ 65535 之間挑選﹐只要那個 port 沒被其它程式使用就行。

請您記住這些 port 的使用特徵﹐我們在防火牆設計中常會用到。

連線方向

我們已經知道 Socket Pair 事實上就是兩個 socket ﹕一個是 Source、另一個是 Destination。但﹐這是相對而言的﹐為什麼這麼說﹖

因為﹐我們要成功的建立一個 TCP/IP 連線﹐其連線必須是雙向的﹕假設連線是 A 與 B 之間的兩台機器。當封包從 A 送往 B 的時候﹐A 的 Scocket 是 Source Socket、而 B 則是 Destination Socket ﹔反過來﹐當封包從 B 送回 A 的時候﹐那麼 A 的 Socket 就變成 Destination、而 B 則變成 Source 。


圖三

請您務必搞懂這對 Socket 於連線方向時所使用的名稱。這樣﹐當您在防火牆牆上處理這些封包的時候﹐才知道哪邊是 Source 、哪邊是 Destination 。

連線界面

與剛纔提到的連線方向相關的﹐就是連線界面了。一般情況下﹐防火牆都是處於內部網路與外部網路之間的位置﹐換句話說﹕它通常有不同的界面來連接不同的網路。

讓我們再以圖三為例﹕當封包從 A 送往 B 的時候﹐在防火牆左邊的界面屬於 incoming 界面(在 iptables 中以 -i 表示)、而右邊的界面則是 outgoing 界面(在 iptables 以 -o 表示)﹔但是﹐當封包從 B 送回 A 的時候﹐右邊界面則成為 incoming 界面、左邊的則成為 outgoing 界面。

Tips﹕請那些習慣了 ipchains 語法的朋友務必注意﹕在 ipchains 裡面不管界面是屬於 incoming 還是 outgoing ﹐一律以 -i 表示﹔但在 iptables 中﹐-i 只能代表傳入界面、傳出界面只能以 -o 表示。所以﹐在 iptables 中的 INPUT 不可能使用 -o 來指定界面﹔相反﹐-i 界面也不可能出現在 OUTPUT 中。

封包於防火牆中的流向

當您搞懂了 Source Socket 與 Destination Socket 、以及各界面之間的關係之後﹐接下來﹐再讓我們仔細看看封包在防火牆中的流向。

當一個封包抵達防火牆的時候﹐必須從一個界面進入﹐然後經過路由判斷之後﹐再從路由判斷所指定的界面送出去。您或許很容易的認為﹕從界面傳入的封包就是 INPUT ﹔從界面從出去的封包就是 OUTPUT﹔而從一個界面送到另一個界面就是 FORWARD


圖四

事實上是否如此呢﹖

哈﹐如果您對 ipchains 程式夠了解的話﹐您肯定會認為如此。但﹐到了 iptables 的時候﹐卻未必哦~~ ^_^ 在 iptables 中﹐所謂的 INPUT 與 OUTPUT﹐只對那些與本機 Local Process 相關的封包而言才是成立的(這也是 iptables 與 ipchains 最大分別之一)。也就是說﹕對於一個從界面傳入的封包﹐如果它是送給在本機的﹐那才算是 INPUT﹐否則不算﹔至於將從一個界面送出的封包﹐如果這個封包是從本機產生的﹐才算 OUTPUT﹐否則不算﹔那些傳入與傳出都與本機無關的封包﹐則屬於 FORWARD 了(注意﹕既不是 INPUT 也不是 OUTPUT 哦)﹕


圖五

事實上﹐圖五中的三個圓角方塊( INPUT、OUTPUT、FORWARD )﹐ 分別是在 iptables 程式裡面的三個內建防火鏈( Built-in Chains﹐我們後面再說明) 。

NAT 的原理

我們一般所說的 NAT 事實上只是一個 socket 替換機制﹕要麼將 Source Socket 換掉、要麼將 Destination Socket 換掉、要麼兩個同時換掉。就這麼簡單~~ ^_^

通常而言﹐當一個 NAT 在替換 Source Socket 的時候﹐我們稱之為 SNAT ( Source NAT )﹔如果在替換 Destination Socket 的時候﹐則為 DNAT ( Destination NAT )﹔那如果 Source 與 Destination 一起換呢﹖哈~~ 這個留給聰明的您自己思考吧~~

究竟 NAT 是根據什麼來替換這些 socket 呢﹖這就是我們要花心思去設計的了。在前面我們曾經示範過所謂的‘IP 偽裝 ( Masquerading )’設定範例﹕

iptables -t nat -A POSTROUTING -o ppp0 \
    -s 192.168.100.0/24 -j MASQUERADE

事實上﹐這是一個 SNAT 的經典例子了﹕當 NAT 處理一個封包的時候﹐如果發現它是來自 192.168.100.0/24 這個網路﹐並且經由 ppp0 送出的話﹐那麼它的來源位址則換成 ppp0 的 IP 位址( socket 的元素之一)。至於 socket 的另一元素 --- TCP/UDP port --- 是否也需要換掉呢﹖得視 NAT 當時的 port 使用情況而定。聰明的 NAT 都會‘儘量’的保留原有的 port 數值﹐除非這個 port 已經被別的 Socket 使用了。不過﹐也有些 NAT 不管怎樣﹐都同時將 IP 位址與 Port 給換掉。然而﹐不管哪種情形﹐只要 Socket 元素之一經過修改﹐那就是另外一個新的 Socket 。並且﹐NAT 必須為這個動作進行記錄 (通常會保存在 NAT Table 之中)。

當被修改過 Socket 的封包送給遠端主機的時候﹐對方只認得被替換之後的 Socket ﹐換句話說﹕對方的回應只會送回 NAT 的外部 Socket 那邊去(記住﹐此時﹐回應封包的 Source 與 Destination 位置會顛倒過來)。因為 NAT 已經有這個 socket 的 NAT 記錄﹐所以﹐當這個回應送回到該 socket 的時候﹐NAT 會根據記錄將 Destination 修改為原來的 Socket (也就是被修改了的 Source Socket)﹐然後再送回給原來的內部主機﹕

圖六

您會發現﹕在整個連線過程中﹐無論內部的主機或是外部的遠端主機﹐事實上都不必知道 NAT 主機的存在﹗只要各自的程式在檢查 Source Socket 與 Destination Socket 的時候﹐與自己所期待的設定一致就行了。

當您了解 SNAT 是怎麼一回事之後﹐再來理解 DNAT 就沒什麼困難了﹕當 NAT 主機收到一個封包之後﹐按要求修改其 Destination Scocket 、並做好記錄﹔完成後根據修改後的路由判斷送到修改後的位址﹔然後﹐當目標主機處理完畢所送回的封包再回到 NAT 主機的時候﹐根據 NAT 記錄再一次偷龍轉鳳、完成後送回原主機就好了。

事實上﹐NAT 所牽涉的知識面很廣﹐需要您花大量時間學習的。我發現下面這個網頁很不錯﹕

http://www.ncu.edu.tw/~center5/livecd/ipnat/

有空不妨去看看﹐尤其是如果您使用 FreeBSD 系統的話。

連線之建立

在這裡﹐讓我們認識一個 TCP 封包的特殊旗標( flag ) --- SYN bit 。究竟它是什麼東西呢﹖這就要我們回到 Three-Way Handshake 的工作原理去了﹕


圖七

每一個 TCP 連線﹐都必須由一端(通常為 client )發起請求﹐其 TCP 封包會將(且只將) SYN 旗標設定起來﹐這是整個連線的第一個封包﹔如果另一端(通常為 Server ) 接受這個請求的話﹐則會向請求端送回整個連線的第二個封包﹕其上除了 SYN 旗標之外﹐同時還將 ACK 旗標也設定起來﹐並同時在本機端建立資源﹐以待連線之需﹔然後﹐請求端獲得服務端第一個回應封包之後﹐必須再回應對方一個確認封包﹐此時封包只帶 ACK 旗標(事實上﹐後繼連線中的所有封包都必須帶有 ACK 旗標)﹔只有當服務端收到請求端的確認( ACK )封包(也就是整個連線的第三個封包)之後﹐兩端的連線才能正式建立。這就是所謂的 TCP 連線的‘三段式交握( Three-Way Handshake )’的原理。

Tips﹕如果 Three-Way Handshake 過程中的這三個封包﹐任意一個不能順利送抵目的地的話﹐那整個連線就沒辦法建立。所以﹐如果您想在防火牆上擋掉所有來自外部的連線建立﹐只要單純的將第一個封包(也就是僅帶 SYN 旗標的封包)攔下來就可以了﹗不過﹐這樣做會導致一些複雜的連線﹐例如 FTP ﹐不能正常服務。那就需要更加仔細的設定防火牆規則了。

另外﹐許多惡意的攻擊者﹐也會利用 Three-Way Handshake 的工作原理來達到搗亂和破壞的目的。因為服務端只要收到 SYN 封包就會在系統上建立相關的資源﹐然後等待對方的 ACK 封包進行連線確認﹔如果確認遲遲不達﹐那服務端將等待一段時間才取消這個連線所佔用的資源。如果攻擊者一下子送進大量的 SYN 封包﹐然後又故意將 ACK 確認掛起來﹐這樣會急劇的耗費服務端的資源﹐如果資源耗盡的話﹐將不能再提供任何新的連線服務、甚至讓機器掛掉﹗這就是聞名的‘ SYN Flood ’攻擊法了。

說實在﹐要防範 SYN Flood 攻擊是很難的﹐只要您對外提供連線服務就有機會遭到攻擊﹐主動權完全在攻擊者手上。各家防火牆作者無不挖空心思以圖降低這類攻擊的損傷﹐可謂‘八仙過海、各顯神通’﹐不一而足。不過﹐大部份方法不外乎兩種﹕降低連線等待時間、或是限制 SYN 封包數量。當然﹐有些聰明的防火牆程式能夠‘動態’的偵測此類攻擊的特性﹐然後自動的啟動防護機制。我們這裡所介紹的 iptables 也有這個功能﹐如果您有興趣的話﹐不妨參考 Pakcet Filtering HOWTO 文件第七章之“ 過濾規格 ”中關於 limit 的敘述。

理解 Three-Way Handshake 非常重要﹐這在日後整個防火牆管理生涯中都佔據著舉足輕重的地位。

連線狀態

當我們了解 SYN 封包的作用之後﹐最後﹐讓我們了解一些 TCP 連線狀態吧。

我們已經知道一般的服務程式會使用 Well-known Port 等待 client 端的連線﹐通常﹐這些 Socket 會隨著服務程式的載入而開啟。那麼﹐當一個服務程式打算向外部提供服務的時候﹐其連線狀態必須處於 LISTEN 狀態。當您執行 netstat 程式的時候﹐就很容易找出機器目前有哪些服務對外提供﹐只要找出那些 LISTEN 狀態的連線就知道了。

當一個 client 端需要建立連線的時候﹐會送出一個 SYN 封包﹐如果它還沒完成 Three-Way Handshake 的時候﹐那麼該連線則會處於 SYN_SENT 狀態﹔相對的﹐當 server 端收到這個 SYN 封包的話﹐那連線則處於 SYN_RECV 狀態﹔假如 Three-Way Handshake 能順利完成﹐換句話說﹐連線已經建立﹐那這時候則處於 ESTABLISHED 狀態。

然而﹐當連線要結束的時候﹐其狀態則複雜得多﹐大致有如下這些狀態﹕FIN_WAIT1FIN_WAIT2TIME_WAITCLOSEDCLOSE_WAITLAST_ACKCLOSING 。我這裡不詳細說明這些狀態了﹐有興趣請參考相關的 TCP/IP 書籍吧。至於完全找不到分類的﹐則歸為 UNKNOWN 狀態。

Tips﹕然而﹐ iptables 對於連線狀態的描述略有不同﹕

NEW
一個建立新連線的封包。

ESTABLISHED
一個屬於成功建立連線之封包。

RELATED
一個與現有連線相關﹐但卻並不限於其連線的封包﹐諸如 ICMP 錯誤﹐或是建立 FTP 數據連線的封包( FTP 模組已插入)。

INVALID
一個因某些原因不能被鑒別的封包﹕這包括記憶體不足和不能回應任何已知連線的 ICMP 錯誤。通常﹐這樣的封包都會被丟棄掉。

您最好能夠區分以上提到的連線狀態﹐因為在防火牆設定中也常會用到。

好了﹐關於防火牆設定所需要的基本原理﹐我暫時介紹到這裡。當然﹐還有許多其它相關概念還沒來得及跟各位介紹的﹐那就請閣下自行研究和補充了。

使用 iptables

我們這裡所介紹的防火牆工具就是 iptables ﹐所以﹐在我們真正動手實作之前﹐最好還是讓我們了解一下這個工具的使用規則吧。

不管使用哪一種防火牆﹐基本上都是設定防火牆規則( RULES ) 來規範封包的處理而已。我們在 iptables 上也不例外﹐只是﹐iptables 會將不同的規則集合起來﹐放進不同的鏈( CHAINS ) 中以備查用。大家已經從前面的圖五中看到了 iptables 的三個內建鏈 ( Built-in Chains ) 了﹐它們分別是﹕INPUTOUTPUT、與 FORWARD 。它們在 iptables 跑起來之後就會建立(一開始裡面是空的、不帶任何規則)﹐而且它們是不能刪除的。至於它們三者的分別﹐我已經在前面說過了﹐如果您忘記了的話﹐最好捲回去溫習一下(圖五)。除了這些內建的鏈外﹐您可以在 iptables 任意建立自定鏈( User-defined Chains ) ﹐除了可以避免重複輸入規則外﹐還能增加整鏈的處理速度 (哦﹐這要您非常了解 iptables 的規則運作才能體會啦~)。

Tips﹕如果您從 ipchains 轉過來使用 iptables 的話﹐那您這裡還必須要認識一個新的概念﹕ tables 。我們知道 chains 裡面包含的就是 rules 和 policies ﹐但 tables 又是什麼呢﹖哦~~ 它包含的是 chains ﹗沒錯﹗這就是為什麼 iptables 之所以被稱為 tables 的原因啦~~ ^_^

iptables 是使用 -t 參數來指定 table 的。在預設的情況下﹐iptables 的命令都是作用於 filter 這個 table 上﹐如果您要修改其他 table 的話﹐那就必須用 -t 來指定。我們前面介紹的 NAT 設定規則﹐都是在 nat 這個 tables 中進行的(細心的讀者或許一早就在問那個 -t nat 是什麼了吧﹖)。除了 filter 與 nat 外﹐您還可能會使用到 mangle table 來進行其它的封包改寫功能﹐不過我們這裡不談這個進階題目了。

基本上﹐當一個封包進入防火牆之後﹐iptables 就將之丟進不同的鏈中、逐行檢查規則、一旦找到符合設定的規則﹐那麼就會根據規則所定義的動作來處理這個封包﹕

ACCEPT
接受這個封包﹐也就是可以通過規則檢驗而放行、順利通過這個鏈。

DROP
丟棄這個封包﹐也就不能通過規則檢驗而被擋掉。

REJECT
與 DROP 一樣﹐但會向來源地送出 ICMP 封包﹐告之對方‘ port unreachable ’的錯誤信息。

REDIRECT
將封包重導至 本機端 的其它 port 。

QUEUE
將封包重導至 本機端 的佇列 ( queue ) 程式。

LOG
將該封包連線透過核心程式記錄下來。

MARK
為封包進行標記﹐供後面其它規則使用﹐或是供其它封包處理程式使用。

SNAT / DNAT / MASQUERADE
這些都是 NAT 的處理﹐視要求而修改為特定的 Source Socket 或 Destination Socket 、或動態的根據路由判斷後的界面而修改 Source Socket 。

[ User-Defined Chain ]
將封包轉至自定的鏈中繼續檢驗﹐該鏈事先必須用 -N 建立。如果在自定鏈中找不到符合的規則﹐則會自動返回原鏈的下一行規則繼續檢驗﹕

RETURN
直接跳離目前的鏈﹕如果是自定鏈﹐則返回原鏈的下一個行規則繼續檢驗﹔如果是內建鏈﹐則參考原則( policy﹐下述 ) 來處理封包。

以上所列的處理動作﹐在 iptables 稱為目標 ( TARGET )﹐在規則中以 -J ( jump ) 選項進入。事實上﹐還有許多其它種類的目標﹐有興趣請自行 man iptables 慢慢參考吧。

假如封包經過一個鏈而找不到符合的規則﹐那麼﹐其最終命運則以該鏈的原則( POLICY ) 為依據(通常為 DROP 或 ACCEPT )﹐在 iptables 中以 -P 來定義(預設全為 ACCEPT )。請您記住一個很重要的特性 --- 規則檢驗的 First-Match 原則﹕一旦找到符合的規則將不再往下檢查鏈中的其它規則(除非被指向其它鏈)。如何合理的安排規則的順序就變得非常重要了﹗

接下來﹐讓我們學習一些關於鏈的管理技巧﹕

  • 建立一個新的(自定)鏈 ( -N )。
  • 刪除一個空的(自定)鏈 ( -X )。
  • 改變一個內建鏈的原則 ( -P )。
  • 列出一個鏈中的規則 ( -L )。
  • 清除一個(內建)鏈中的所有規則 ( -F )。
  • 歸零( zero ) 一個鏈中所有規則的封包字節( byte ) 記數器 ( -Z )。
  • 在一個鏈的最後面新增( append ) 一條規則 ( -A )。
  • 在鏈內某個位置插入( insert ) 一條新規則( -I )。
  • 在鏈內某個位置替換( replace ) 一條規則 ( -R )。
  • 在鏈內某個位置刪除( delete ) 一條規則 ( -D )。
  • 刪除(delete) 鏈內第一條符合的規則 (-D)。

當我們設定一條 iptables 規則的時候﹐一般而言﹐可以透過下面數個元素的組合來決定封包的目標(target)﹕

  • 封包於防火牆中的流向 ( INPUT、OUTPUT、FORWARD )
  • 相關界面 ( -i 或 -o )
  • 所屬協定 ( -p )
  • 連線類型 ( -m state )
  • 封包類型 ( --syn )
  • 來源地 ( -s )
  • 來源埠口 ( --sport )
  • 目的地 ( -d )
  • 目的地埠口 ( --dport )

Tips﹕事實上﹐iptables 的規則依據非常多樣﹐以上僅是少部份而已。例如﹐您可以透過 tcp-flag 、 icmp-type 、 mac 、 limit 、owner 、等元素進行更透徹的限制。更詳細的說明﹐請參考 man page 或 HOWTO 文件。

下面﹐讓我們以一些例子來說明好了﹕

iptables -A FORWARD -i ppp0 -p TCP -s xxx.xxx.xxx.xxx -d yyy.yyy.yyy.yyy -j DROP

這行規則是說﹕所有從 ppp0 傳入並且不是送給本機的 TCP 封包(因為是 FORWARD 鏈)﹐如果是從 xxx.xxx.xxx.xxx 傳給 yyy.yyy.yyy.yyy 的話﹐一律給予丟棄( -j DROP)。因為我們是用 -A ( Append ) 來增加這行規則﹐它會加在 FORWARD (注意大小寫﹗) 規則的最後一行。請注意﹐在路由過程中的封包位址是不會改變的﹐除非您經過了NAT 處理過(請參考後面的敘述)。在規則的位址﹐可以是一段網路位址( Net_ID/netmask )、也可以是一個 IP 位址(預設使用 32-bit mask )、甚至可以是一個主機名稱(可查詢到 IP 就行)。

假如您想做出更嚴格的限制﹐還可以增加一些元素進去﹕

iptables -A FORWARD -i ppp0 -p TCP -s xxx.xxx.xxx.xxx --sport 1024:65535 \
    -d yyy.yyy.yyy.yyy --dport www -j DROP

這樣的話﹐要是封包從 xxx.xxx.xxx.xxx 這個客戶端(使用 1024 以上的 port )﹐傳給 yyy.yyy.yyy.yyy 這台網頁伺服器的 port 80 (也就是 www)﹐一律給擋下來。您會發現﹕如果您不指定額外特定元素﹐iptables 內定會全部接受它們。如果這時候您再這樣輸入一行命令﹕

iptables -I FORWARD 1 -s xxx.xxx.xxx.xxx -j ACCEPT

因為這裡只有一個來源( -s )有定義﹐那就不必管其他條件是怎麼樣的了﹕不管用什麼 port、不管到哪裡、不管用什麼協定、不管是什麼連線狀態、不管 ....... 、只要來自 xxx.xxx.xxx 就一律 ACCEPT 。但這行規則會導致原本會被擋掉的封包﹐這時候就會先行被放行了。因為 " -I FORWARD 1" 是要將這行規則加在 FORWARD 鏈的第一行之上(如果您不指定行數﹐那預設就是第 1 行)。iptabless 在檢查規則的時候﹐會先從第一行規則往後一直比對下去﹐只要條件符合﹐就不再往下檢查其它規則了。如果所有規則都不符合﹐然後會檢查原則( Policy )。所以﹐設定 iptables 的順序要格外小心哦~~~ ﹗

Tips﹕關於規則順序情形﹐在 VPN 與 transparent proxy 的設計上很重要。以後者為例﹕假設您參考文章開頭的介紹﹐用如下命令設計 transparent proxy ﹕

iptables -t nat -A PREROUTING -i eth0 -p tcp -s 192.168.100.0/255.255.255.0 \
    --dport 80 -j REDIRECT --to-ports 3128

如果這時候﹐您在 NAT 機器本機端就有提供 www 服務的話﹐那會導致內部主機連不上。這樣的話﹐您只要在上面句子之前插入一行就行了﹕

iptables -t nat -I PREROUTING -i eth0 -p tcp -s 192.168.100.0/255.255.255.0 \
    -d 192.168.100.23 --dport 80 -j ACCEPT

註﹕請將 192.168.100.23 修改為您的實際位址。並確定關於 lo 界面的 INPUT 與 OUTPUT 沒有被擋掉。

通常來說﹐使用 iptables 的時候﹐我們最需要小心設計的就是設定從外部界面進入本機的 INPUT 規則、以及從外面送進內部網路的 FORWARD 規則﹔OUTPUT 規則及由內至外的 FORWARD 規則﹐相對來說可以鬆一點。但為了養成良好習慣﹐我們也應該嚴格的設定好每一個 OUTPUT 與 FORWARD 規則。如果路由上不能讓封包順利完成傳遞﹐例如使用私有 IP 的內部網路需要連接 Internet﹐那麼我們就要使用 NAT 來改寫封包了。

在 iptables 中﹐與過往的 ipchains 比較起來﹐它在 NAT 的設定上明顯多樣化及複雜多了。剛從 ipchains 轉換過來或許一下子不容易理解的﹐所以﹐我們有必要再花點時間學習一些 iptables 的 NAT 技巧。

先讓我們看看封包在 NAT 中的處理過程吧﹕


圖八

圖中的三個灰色圓角方塊﹐事實上就是 NAT 的三個內鍵鏈。其中的 OUTPUT 是 local process 產生的封包才會經過那裡﹐而 PREROUTING 和 POSTROUTING 事實上也不難理解﹕在英文語法上﹐PRE 就是在什麼什麼之前、POST 就是在什麼什麼之後。那麼﹐PREROUTING 就是在 ROUTING 之前﹐通常是封包從一個界面進入之後﹐尚未交給路由判斷之前的處理﹐您可以在這裡進行 DNAT 的 socket 替換﹔而 POSTROUTING 則是在路由判斷之後﹐通常是當封包離開界面之前的處理﹐您可以在這裡進行 SNAT 的設定。

Tips﹕您會發現﹐這與前面(圖五)的封包過濾流程不太一樣﹐而許多朋友也經常搞不懂這兩者的分別﹐並試圖將兩個圖合併在一起。事實上﹐我認為那是不必的﹐因為我們可以簡單的將 Packet Filtering 與 NAT 看成兩個不同的系統(table)﹐只是用同一工具來設定而已。當然了﹐要說完全將兩者割裂﹐看來也是說不過去的。這似乎有點矛盾﹐只是﹐我個人覺得在追蹤封包的處理過程的時候﹐分開兩個系統來檢查是比較容易一些。只要我們能夠精確了解封包於每一個鏈中的狀態、及其來龍去脈就好了。

那麼﹐我們如何用 iptables 來進行 SNAT 處理呢﹖看看下面的例子吧﹕

## 將來源位址換成 1.2.3.4 ﹕
# iptables -t nat -A POSTROUTING -o eth0 -j SNAT --to 1.2.3.4

## 將來源位址換成 1.2.3.4、 或 1.2.3.5、 或 1.2.3.6 ﹕
# iptables -t nat -A POSTROUTING -o eth0 -j SNAT --to 1.2.3.4-1.2.3.6

## 將來源位址換成 1.2.3.4 ﹐而 port 則換成 1 到 1023 之間﹕
# iptables -t nat -A POSTROUTING -p tcp -o eth0 -j SNAT --to 1.2.3.4:1-1023

## 將所有從 ppp0 界面送出之來源位址換成 ppp0 的位址﹕
# iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERADE

我們在前面已經知道 SNAT 就是替換 Source Socket 而已。前面三個命令常用於靜態(或指定範圍)的 NAT 轉換、而最後一行的 MASQUERADE 則是動態的根據 ppp0 的位址進行轉換(因為 ppp0 可能每次撥接所獲得的位址都不一樣)。不管哪種 SNAT ﹐通常都是修改來自於內部網路的封包﹐讓它能順利到達外面的網路去。這除了可以解決路由的問題外﹐還可以節省 IP 位址﹐而且在安全上面也是有幫助的。因為內部網路使用的私有 IP 在 Internet 上面是不能路由的﹐所以﹐就算您的 FW 被攻破﹐除非入侵者能佔據您的 NAT 主機且以它做據點﹐否則﹐他們還是不能連接到內部網路的主機。

然而﹐如果我有些服務﹐例如 WWW、MAIL 等﹐想架在內部網路呢﹖按慣例﹐通常會將那些伺服器架在 DMZ 裡面。如果 DMZ 是可以直接路由到 Internet 上去的話﹐我們在 FORWARD 那裡設 ACCEPT 規則就可以了。但是﹐如果我們的 DMZ 是用另外一個私有 IP 網路來架呢﹖那我們就需要使用 DNAT 技術﹐將那些傳到 NAT 外面界面的某些 socket (可以為任意 port ﹐但通常是那些固定的服務埠口) 的封包﹐轉到 DMZ 裡面的機器去了。例如﹕

## 將目的地位址換成 5.6.7.8 ﹕
# iptables -t nat -A PREROUTING -i eth1 -j DNAT --to 5.6.7.8

## 將目的地位址換成 5.6.7.8、或 5.6.7.9、或 5.6.7.10 ﹕
# iptables -t nat -A PREROUTING -i eth1 -j DNAT --to 5.6.7.8-5.6.7.10

## 將送給目的地位址之 Web 封包送至 5.6.7.8 及 port 8080 那邊﹕
# iptables -t nat -A PREROUTING -p tcp --dport 80 -i eth1 \
        -j DNAT --to 5.6.7.8:8080

## 將送給 1.2.3.4 的本機重導致 loopback 界面﹕
# iptables -t nat -A OUTPUT -d 1.2.3.4 -j DNAT --to 127.0.0.1

## 將從 eth1 界面進入的要送給 port-80 web 封包重導給 squid (transparent) proxy ﹕
# iptables -t nat -A PREROUTING -i eth1 -p tcp --dport 80 \
        -j REDIRECT --to-port 3128

關於 FTP

事實上﹐僅知道如何操作 iptables 工具﹐還不足以設定好您的防火牆及 NAT ﹐您還要對各種 TCP/IP 服務都要有相當程度的了才行﹕您必須知道一個連線的建立過程是怎樣的﹐每一個封包動作是怎樣從一端送到另一端的。您除了要知道每一個封包的來源和目的位址﹐還要知道它們的來源埠口和目的埠口。而且﹐我們不要忘記了﹕所有連線都是雙向的﹐您除了要照顧從客戶端到伺服器端的請求﹐也要照顧從伺服器到客戶端的回應。下面﹐讓我們看一看火牆的設定如何影響 FTP 的運作的( FTP 是一個非常經典的 NAT/Firewall 設定範例)。

首先﹐我們要知道 FTP 的連線模式有兩種﹕主動模式( active )和被動模式( passive )。要了解這兩個模式的不同﹐得要了解 FTP 的連線是怎樣建立的﹕

在正常模式下﹕

  1. FTP client 開啟一個隨機選擇的高於 1024 的 port 呼叫 FTP server 的 port 21請求連線。當順利完成 Three-Way Handshake 之後﹐連線就成功建立﹐但這僅是命令通道的建立。
  2. 當兩端需要傳送資料的時候﹐client 透過命令通道用一個 port 命令告訴 server ﹐客戶端可以用另一個高於 1024 的 port 做數據通道﹐並準備好 socket 資源。
  3. 然後 server 用 port 20 和剛才 client 所告知的 socket 建立數據連線。請注意﹕連線方向這是從 server 到 client 的﹐TCP 封包會有一個 SYN 旗標。
  4. 然後 client 會返回一個帶 ACK 旗標的確認封包﹐並完成另一次的 Three-Way Handshake 手續。這時候﹐數據通道才能成功建立。
  5. 開始數據傳送。

在 passive 模式下﹕

  1. FTP client 開啟一個隨機選擇的高於 1024 的 port 呼叫 FTP server 的 port 21請求連線﹐並完成命令通道的建立。
  2. 當兩端需要傳送資料的時候﹐client 透過命令通道送一個 PASV 命令給 server﹐要求進入 passive 傳輸模式。
  3. 然後 server 像上述的正常模式之第 2 步驟那樣﹐挑一個高於 1024 的 port ﹐並用命令通道告訴 client 關於 server 端用以做數據通道的 socket。
  4. 然後 client 用另一個高於 1024 的 port 呼叫剛才 server 告知的 scoekt 來建立數據通道。此時封包帶 SYN 標籤。
  5. server 確認後回應一個 ACK 封包。並完成所有交握手續、成功建立數據通道。
  6. 開始數據傳送。

我們都知道﹕火牆的保護對象是內部網路的 client 主機。防火牆為了擋掉一些來自外面的危險動作﹐通常會限制那些來自外面的主動連線﹐也就是帶 SYN 標籤的封包(請參考 Three-Way Handshake 的過程)。在這樣的情況下﹐iptables 或許有這樣的一個設定﹕

iptables -I INPUT -i ppp0 -p TCP --syn -j DROP
iptables -I FORWARD -i ppp0 -p TCP ! --syn -j ACCEPT

在第二行中的那個 " ! " 就是 NOT 的意思﹐" ! --syn " 就是 NOT SYN 之意﹐也就是只允許非主動連線的 TCP 封包從外部界面進入﹐這和第一行的意思一樣﹕不接受來自外面主動建立的連線。

Tips﹕前面我們已經說過我們可以將傳到 NAT 外部界面的連線轉到 DMZ 裡面去。假如我們有這樣的要求﹐卻繼續沿用前面那行帶 " ! -y " 的規則的話﹐外面的客戶主機就無從建立連線來連接我們的服務主機了。但我們又不想取消它﹐以免危害到其它非 DMZ 的內部網路。

還記得 iptables 的行為習慣嗎﹕它會自上而下的對比規則﹐找到符合的就不再往下找了。根據這樣的特性﹐我們可以將 NAT 外部界面進入的某些連線﹐在那行 syn 過濾規則之前 ACCEPT 進來。不過﹐就要非常小心別漏了必要的限制元素﹐有可能的話﹐盡您想象把規則設得嚴密又嚴密吧。

假設我們這時候有一個在防火牆之後的 FTP client 要試圖連接外面的 FTP server 。在正常模式下﹐(第 3 步)來自 server 的數據通道之建立請求就會被擋下來﹐這會導致數據通道無從建立。雖然您可以 login 進遠端的 FTP 伺服器﹐但如果輸入 ls 後﹐卻看不到結果的。FTP 的 login 與 ls 是透過命令通道進行的﹐因為命令通道的建立並沒問題﹐所以 server 可以收到命令。但 ls 的結果確需要透過數據通道送回來﹐然而數據通道卻不能建立建立。所以 server 在傳送失敗後﹐再用命令通道告訴您﹕ “Can't build data connection: Connection refused.”

明白了﹖是否解釋了困繞您心目中以久的疑團呢﹖ ^_^

好了﹐如果用 Passive 模式又會如何呢﹖讓我們回去看看正常模式的第 3 步和 Passive 模式的第 4 步﹐然後檢查它們進出防火牆的方向﹐以及防火牆如何處理 SYN 封包就知道了。我不想直接把答案告訴您。自己要懂得思考﹐才會有進步﹗不是嗎﹖

Tips﹕不過﹐在 iptables 中有一個 RELATED 的 TCP 連線狀態標籤﹐可以讓 ftp-data 連線順利建立﹕

iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT

但是﹐我必須再指出﹕上面談的是基於一個 routing 健全的情形下。如果用 NAT 的話﹐封包的位址將會有所改變。無論用正常模式或 Passive 模式﹐server 都看不到 client 的位址﹐server只看到 NAT 的位址而已。故此﹐就算您取消了入站封包的 SYN 限制﹐在正常模式下來自 server 的數據通道連線﹐封包只會送到 NAT 主機的外面界面那裡。但問題是 NAT 並沒有該連線的 NAT 記錄﹐也就不會將修改封包 socket 將之送回裡面的客戶主機。除非﹐您能夠幫 client 端做 ftp-data 的 DNAT 。然而﹐因為 client 所使用的 ftp-data socket 是隨機挑選的一個大於 1024 的 port ﹐您事先沒辦法在 NAT 上知道 ftp client 用哪個 socket 的。然而﹐更重要的問題在於﹕client 在命令通道中所使用的 port 命令﹐其 IP 是原有的內部位址、而 NAT 根本不會將之修改過來﹗所以 server 在建立 ftp-data 連線的時候﹐根本就不可能路由到正確的位址﹗

那是否有辦法解決呢﹖當然有的啦~~ 使用 passive 模式就可以了﹗為什麼﹖不知道您的腦筋是否能轉得過來呢﹖ 那是因為﹕在 passive 模式下面﹐所有連線的建立方向都是從內部的 client 送往外面的 server 的﹔因為這個連線需要做 SNAT﹐那麼﹐NAT 的 table 中有這個 socket 轉換的記錄了﹔然後從 server 所送回的回應封包﹐理所當然也能順利完成 socket 的還原工作。

明白了﹖或許﹐您又會問﹕那如果在 NAT 情況下﹐不使用 passive 模式呢﹖是否還有辦法解決 ftp 數據通道的問題﹖

搭配核心模組

呵~~ 就算不用 passive 也可以啦﹗只要能讓 NAT 抓到 ftp client 在命令通道中送出的 port 命令﹐並修改原本在封包 body 中的 socket ﹐那就可以‘聰明的’在外部界面上建立 DNAT socket﹐以等待 server 那邊的數據通道連線請求了。這工作﹐通常是由核心模組來完成的﹐在 2.4.x 核心上﹐您只需執行如下命令就可以了﹕

modprobe ip_conntrack_ftp
modprobe ip_nat_ftp

Tips﹕在 2.2.x 核心中所使用的模組名稱為 ip_masq_ftp ﹐不過﹐它只能跟 ipchains 配合使用。就算您在 2.4.x 核心中能夠載入 ipchains 模組﹐但因為 2.4.x 核心的 ip_nat_ftp 並不能與 ipchains 合作﹐所以也就沒辦法了。假如您有興趣知道 ip_masq_ftp 這個模組會做些什麼﹖請參考網友 常鍋麵 兄的精解﹕

常鍋麵  wrote in message news:3g4d6d$1q1@bbs.cynix.com.tw...

> ip_masq_ftp.o 的工作,除了幫 masq 建立一個 port 20 的紀錄
> 他最重要的工作還是跟 port 21 相關
>    a.監視 client -> server destination port 21 的封包 (tcp of course)
>      並修改其中帶有 PORT command 的封包...
>            [IP header|TCP header|PORT xxx,xxx,xxx,xxx,xx,xxCRLF]
>    b.修改此封包 IP header 中 total length 欄位
>      紀錄 data length 的變動以維持 port 21 的連線不要斷掉..
>      修改每一個 server -> client source port 21 封包的 ack number(TCP header)
>      修改每一個 client -> server destination port 21 封包的 seq number
> 畢竟 masq 紀錄只有 IP header 的 IP add & TCP header 的 port number
> 其他都要靠 ip_masq_ftp.o
> 如果 ftp server port 不是 21 就完蛋了,因為攔不到 PORT command
> 就更別提 data connection 了...(dir, get 都會送 PORT command)
> 不過寫得彈性一點(可設定)應該也不會困難才對...

文章雖然是描述 ip_masq_ftp 模組的﹐但其中的原理和 ip_nat_ftp 是一樣的。

事實上﹐FTP 服務的設定﹐在防火牆與 NAT 設計中非常具有代表性。如果要測試自己對 NAT 的設定是否及格﹐用 FTP 服務來考自己就對了。如果您有興趣﹐不妨做做下面這個習題哦﹕

NAT 的 FTP 設定命題

補充﹕在過往的測試中﹐我曾經作過如此猜測﹕

如果 server 和 client 兩端都各自躲在 NAT 背後﹐無論用 ipchains 和
iptalbes 都無法用 passive 模式來建立 data channel。

然而﹐在最近的測試中﹐我發現 netfilter 所帶來的 ip_conntrack_ftp 以及 ip_nat_ftp 模組 已經能夠成功的進行 server 端與 client 端的連線轉換。

換而言之﹐我之前所提到的過濾﹕“client 與 server 雙方都在 NAT 之後是否能連線﹖”現在已經不是問題了﹗實在非常感謝 netfilter 的工作者們。

p.s. 我所測試的核心為 2.4.16 以及 iptables-1.2.1a-1 。

如果您在參考答案之前能夠搞定 FTP 服務的話﹐那基本上您就及格了。否則﹐我勸您還是別急著碰 NAT 的設定。

除了 FTP 之外﹐還有許多其它服務程式﹐也有類似的‘多通道連線’的特性﹐例如 netmeeting 、realaudio、網路游戲、等等。因為這些程式大都沒有所謂的 passive 模式﹐所以﹐只能靠相關的模組幫忙了。如果您有相關問題﹐不妨到下面網站找找﹕

http://www.tsmservices.com/masq/

這個網站有許多奇奇怪怪的 IP 偽裝技巧與模組﹐相信總會有您感興趣的東西。

關於 netmeeting

下面﹐和大家說一說如何在 iptables 使用 netmeeting (H.323) 這個東西。因為﹐ h.323 和 ftp 連線一樣﹕也是由多個通道來建立連線的﹐而且大都使用 UDP 協定。假如不解決來自外部的通道連線問題的話﹐那麼﹐當您使用語音和視訊的時候﹐對方能聽到、看到您的﹐但您卻聽不到、看不到對方﹗

在 kernel 2.2.x 的年代中﹐已經有人寫出來 h323.o 的模組來解決這個問題﹐但到了 2.4.x 核心之後﹐一直到 2.4.16 這個版本才有較為完整的 patch 可以使用。所以﹐請您首先下載 2.4.16 的核心 source 回來﹐並解開在 /usr/src 目錄中(步驟可以參考 system 系列之“ 編譯核心 ”)。同時﹐再到 http://roeder.goe.net/~koepi/ 下載 newnat5-and-helpers-2.4.16.patch.gz 檔案(請留意您下載的檔案必須是 gzip 格式﹗因為我抓回來之後發現它是已經解壓之後的檔案。關於 newnat 的原始網站位於﹕ http://www.kfki.hu/~kadlec/sw/netfilter/ )。要不然﹐您可以從 本站 下載 tar+gzip 的版本(下面例子所採用)﹐同樣置於 /usr/src/ 目錄下面﹕

  1. 開始進行 patch 工作(假設您是從本站下載的 patch)﹕

    cd /usr/src/linux # 請確定為 2.4.16 版本
    tar zxvf ../newnat5-and-helpers-2.4.16.patch.tgz
    cat newnat5-and-helpers-2.4.16.patch | linux patch -p1 -E

    Tips﹕
    # 不管您的 patch 從哪裡獲得﹐不妨先用 cat 看一看其內容﹐
    # 如果您確定您的版本是 gzip 壓縮檔案(亂碼)﹐那最後兩行可改為﹕
    gzip -cd ../newnat5-and-helpers-2.4.16.patch.gz | patch -p1 -E
    # 如果您抓下的檔是已經解壓的話(明碼)﹐那就改為﹕
    cat ../newnat5-and-helpers-2.4.16.patch.gz | patch -p1 -E
    (請確定您的當前目錄是在 /usr/src/linux/ 那裡。)

  2. 如果您還有其它 patch (例如 ipvsadm)﹐也請一一安裝。

  3. 然後執行核心編譯﹐確定如下的選項﹕

    Networking options  --->
    	[*] Network packet filtering (replaces ipchains)
    	[*] TCP/IP networking
    	  IP: Netfilter Configuration  --->
    		<M> Connection tracking (required for masq/NAT)
    		<M>  FTP protocol support 
    		<M>  talk protocol support 
    		<M>  H.323 (netmeeting) support
    		<M>  IRC protocol support
    		......
    

    當然了﹐其它選項也請行選擇。事實上﹐除了一些新選項外﹐大部份都和“ 編譯核心 ”文章所列一樣就行了。

  4. 完成您的核心、模組、LILO 的安裝﹐重新開機﹐確定新核心無誤。

  5. 然後依順序載入如下模組﹕

    modprobe ip_conntrack_h323
    modprobe ip_nat_h323

這樣就可以讓您的 netmeeting 連到外面去了~~﹗

ICMP 的問題

在 NAT 設計中﹐除了要特別解決來自外面建立的連線問題外﹐還有一個 ICMP 的 Host Gateway Redirect 的問題。典型的設計是﹕您將 NAT 的一個外部 socket 經過 DNAT 重導進內部網路﹕

## 將外部的位址 1.2.3.4:80 重導至 192.168.1.1 上面去﹕
# iptables -t nat -A PREROUTING -d 1.2.3.4 \
        -p tcp --dport 80 -j DNAT --to 192.168.1.1

然後再用內部網路的其它機器( 192.168.1.x) 嘗試連接 NAT 這個外部 socket 。下面是我曾經做過的一個實驗﹐茲整理出來供大家參考﹕

> 1﹐當客戶端送出關於 HTTP (port 80) 的連線請求至 NAT 外部界面之後﹐也就是發出
>    一個帶 SYN 的 TCP 封包﹐來源 port 為 1238﹐Sequence 為
>    43980464﹐Acknowledgement 為 0。
> 2﹐緊接著客戶端就收到一個來自 NAT 內部界面的 ICMP 封包﹐類型為一個 Host
>    Gateway 的 Redirect 封包﹐目的 IP 指向內部 server 的位址。
> 3﹐然後客戶端改向內部 server 發出 SYN 的 TCP 封包﹐來源 port 依然為
>    1238﹐Sequence 也是 43980464﹐ ACK 為 0。
> 4﹐接著客戶端收到來自內部 server 的 ACK/SYN 封包﹐目的 port 為 1238﹐SEQ為
>    1579961125﹐ACK 為 43980465。
> 5﹐然而奇怪的事情來了﹕客戶端卻向內部 server 送出一個 Reset 封包﹐來源 port
>    為 1238﹐SEQ 為 43980465﹐ACK 和 SEQ 一樣也是 43980465。
> 6﹐然後隔 6 秒後重複上述動作﹐之後再隔 12 秒重複﹐然後客戶端出現不能連線錯誤
>    信息。
> 假如將 URL 改為直接向內部 server 查詢﹐或是在 NAT 上啟動另一界面於不同子網做
> 為 DMZ﹐再將 server 搬遷過去﹐其連線就不會再出現 ICMP 的 redirect﹐而且 TCP
> 的 SYN -- ACK/SYN -- ACK 這三段 handshake 都能順利完成。

要了解這種現像的話﹐我們不妨追蹤一下封包的傳遞過程﹕

  1. 內部主機將連線請求送至 NAT 主機﹔
  2. 然後 NAT 從 PREROUTING 規則中進行 DNAT ﹐將 Destination Socket 修改為內部 server 的位址﹔
  3. 修改後的封包經過路由判斷發現目的地為同一網路的另一主機﹐於是還是經由內部界面送給內部 server ﹔
  4. 但因為發現這個封包的 source 與 destination 都在同一個 subnet ﹐認為後面的傳遞無需再經過本機。於是同時向 source 端送出一個 ICMP 封包﹐告訴它一個 Host Gateway Redirect 的信息﹐因為它們之間的傳遞有更好的路徑 --- 就是直接傳送。
  5. 內部 server 處理完封包之後﹐因為 source socket 並沒修改﹐所以其回應就無需再經過 NAT ﹐而是直接送回 client 端。
  6. 但問題來了﹕這個回應封包的 source socket 卻是內部 server 的﹐而不是 NAT 的外部 socket ﹗但 client 所期待的回應封包﹐其 source socket 必須是它當初請求的 destination socket --- 也就是 NAT 的外部 socket 。就算 client 端已經獲得 ICMP 的 redirect 通知﹐但事實上﹐socket 並沒獲得修正﹐而且 client 與 server 之間的連線依據﹕Sequence Number 與 Acknowledgement Number 也會發生衝突。因此﹐client 認為這個連線是非法的﹐於是就送出 RESET 封包﹐終止這個連線﹗
  7. 然而﹐client 端原本所其期待的回應卻依然未獲得。根據 TCP 協定的規範﹐client 在得不到回應的情況下﹐會相隔一段時間再重送請求﹐直至多次嘗試均告失敗為止。
  8. 然而﹐根據前面所設定的情況﹐這是一個無解的狀態。於是連線永遠無法建立。

要解決這個問題﹐在之前的 ipchains 年代中只能用 DNS 的欺騙手段來達成﹕也就是分開對外和對內的 DNS ﹐然後讓內部網路使用內部的 DNS ﹐而這台 DNS 會將 IP 解釋為內部位址(也就是讓連線繞開 NAT 主機)。

不過﹐如果您使用 iptables 的話﹐可以參考 NAT HOWTO第十章 的做法﹐同時修改內部封包的 Source Socket 就可以了﹕

# # 將內部封包的來源修改為 NAT 的內部界面(192.168.1.250)﹕
# iptables -t nat -A POSTROUTING -d 192.168.1.1 -s 192.168.1.0/24 \
        -p tcp --dport 80 -j SNAT --to 192.168.1.250

這樣的話﹐內部 server 所回應的封包﹐將永遠不會嘗試直接將封包回給 client﹐而是回給 NAT 主機﹔既然 NAT 主機已經有這個連線的 NAT 記錄﹐於是也能順利的還原連線所期待的 socket 資訊。

關於 ICMP 的另一問題是﹕現在﹐我們的網路越來越不安全了﹐就算不使用 TCP/UDP 這些正常連線﹐許多無聊的騷擾者還可能透過 ICMP 連線進行干擾或搗亂。許多網路管理員在設定防火的時候﹐或許很單純的將 ICMP 連線攔下來﹐比方說不想讓別人來 ping 或 traceroute (請參考第一章“ 網路設定 ”)。

然而﹐事實上我們是不能單純的不分青紅皂白的將 ICMP 攔下來的。首先﹐如果我們需要對外 ping 的話﹐要將 echo-reply 的封包接收進來、要完成 tracertoute 的話﹐要將 time_exceeded 的封包接收進來。然而﹐更重要的﹐許多底層網路設備之間的通訊﹐都必須依靠 ICMP 的溝通來進行協調(事實上這也是 ICMP 最大的應用之一)。如果我們一概將 ICMP 攔掉﹐或許會在不經意之間導致某些連線出問題﹐這通常出現在那些需要 fragmentation 的封包上。

因此﹐一般而言﹐如下的 ICMP 封包我們需要確保它們能順利的進出防火牆﹕

  • destination-unreachable
  • parameter-problem
  • source-quench
  • time-exceeded
  • echo-reply

怎樣﹖您是否覺得我在文章一開始時說的話是正確的呢﹖我說﹕“要設定一個稱職的火牆並不是一件容易的事情﹐它要求您有非常豐富的 TCP/IP 基礎、和嚴密的邏輯頭腦、還得加上無以複加的細密測試。”假如您對我剛纔所解釋的東西不能理解的話﹐那我還是建議您先補習 TCP/IP 基礎之後再回來設定吧。關於更多的 iptables 技巧﹐請參考 Rusty 的原文 HOWTO 文件﹕

Linux 2.4 Packet Filtering HOWTO
Linux 2.4 NAT HOWTO

還有﹐小州兄的文章 關於 iptables 和 NAT 的一些技巧 也非常有助您迅速的掌握 iptables 的基本技巧。

設定封包過濾

當您具備了相當的 TCP/IP 基礎知識、以及對 iptables 的使用有一定程度的熟識之後﹐接下來應該更好保護自己的網路了。您可以自己寫一個類似下面的 script ﹐然後在開機的時候執行它。雖然還是挺簡單的﹐但也應該難倒一般的駭客了吧﹕

#!/bin/bash
#
# Script name: ipt_masq
# A simple script for masquerading, used in Linux (kernel 2.4.x).
#
# Copyleft 2002 by netman (netman@study-area.org).
#
# Redistribution of this file is permitted under the terms of 
# the GNU General Public License (GPL).
#
# Date: 2002/07/03
# Version: 1.3

PATH=/sbin:/usr/sbin:/bin:/usr/bin
RC_SQUID=/etc/rc.d/init.d/squid
EXT_IF=ppp0
INT_IF=eth0
ALLOWED_ICMP="0 3 3/4 4 11 12 14 16 18"

#
# ------------- ensure iptables ----------
which iptables &>/dev/null || {
	echo 
	echo "$(basename $0): iptables program is not found."
	echo "	Please install the program first."
	echo 
	exit 1
}
# ------------- disable ipchains ----------
lsmod | grep ipchains &>/dev/null && {
	echo "Disabling ipchains..."
	rmmod ipchains &>/dev/null
}

# ------------- modules -----------
echo "Loading modules..."
modprobe ip_tables &>/dev/null || {
    echo -n "$(basename $0): loading ip_tables module failure."
    echo " Please Fix it!"
    exit 3
}
for file in /lib/modules/`uname -r`/kernel/net/ipv4/netfilter/ip_conntrack_*.o
do
	module=$(basename $file)
	modprobe ${module%.*} &>/dev/null
done
for file in /lib/modules/`uname -r`/kernel/net/ipv4/netfilter/ip_nat_*.o
do
	module=$(basename $file)
	modprobe ${module%.*} &>/dev/null
done

# ------------- ipforwarding -----------
echo "Turning on IP forwarding..."
echo "1" > /proc/sys/net/ipv4/ip_forward

# ------------- anti spoofing -----------
echo "Turning on anti-spoofing..."
for file in /proc/sys/net/ipv4/conf/*/rp_filter; do
	echo "1" > $file
done

# ------------- flushing ----------
echo "Cleaning up..."
iptables -F -t filter
iptables -X -t filter
iptables -Z -t filter
iptables -F -t nat
iptables -X -t nat
iptables -Z -t nat

# ------------- policies -------------
echo "Setting up policies to ACCEPT..."
iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -t nat -P PREROUTING ACCEPT
iptables -t nat -P POSTROUTING ACCEPT
iptables -t nat -P OUTPUT ACCEPT

# ------------- ICMP -------------
echo "Creating icmpfilter chain..."
iptables -N icmpfilter
for TYPE in $ALLOWED_ICMP; do
    iptables -A icmpfilter -i $EXT_IF -p icmp \
        --icmp-type $TYPE -j ACCEPT
done

# ------------- block -------------
echo "Creating block chain..."
iptables -N block
iptables -A block -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A block -m state --state NEW,INVALID -i $EXT_IF -j DROP
iptables -A block -m state --state NEW -i ! $EXT_IF -j ACCEPT
iptables -A block -j DROP

# ------------- filter -------------
echo "Filtering packets..."
iptables -A INPUT -j icmpfilter
iptables -A INPUT -j block
iptables -A FORWARD -j icmpfilter
iptables -A FORWARD -j block

# ------------- masq -------------
echo "Masquerading internel network..."
iptables -t nat -A POSTROUTING -o $EXT_IF -j MASQUERADE

# ------------- tproxy -------------
$RC_SQUID status | grep pid &>/dev/null && {
    echo "Enabling transparent proxy..."
    INT_IP=$(ifconfig | grep "$INT_IF " -A 1 \
        | awk '/inet/ {print $2}' | sed -e s/addr\://)
    if [ -z "$INT_IP" ]; then
        echo
        echo "$(basename $0): there is no IP found on $INT_IF."
        echo "  Please make sure $INT_IF is setup properly."
        echo
        exit 3
    fi
    iptables -t nat -A PREROUTING -d $INT_IP -i $INT_IF \
        -p tcp -m tcp --dport 80 -j ACCEPT
    iptables -t nat -A PREROUTING -i $INT_IF -p tcp -m tcp \
        --dport 80 -j REDIRECT --to-ports 3128
}
exit 0
## EOS

(上面的 script 我將之取名為 ipt_masq ﹐您可以 點這裡 下載使用。)

不過﹐您必須確定對外的界面必須是 ppp0 、對內地界面必須是 eth0﹐否則請自行修改。而且﹐上面這隻 script 是沒有限制從裡面到外面有哪些服務不能連接﹐如果您要限制的話﹐請自己增加 DROP 規則。

Tips﹕在我們設計防火牆的時候﹐通常有採用兩種原則之一﹕

  1. 拒絕特定的﹐然後開放所有﹔
  2. 接受特定的﹐然後拒絕所有。

在我們前面的 script 中﹐雖然將 policy 設定為 ACCEPT ﹐但事實上﹐我們並非採用第一原則。因為﹐您仔細觀測 block 鏈的設定﹐在最後面的一行規則﹐就是將所有連線 DROP 掉。所以﹐在這行拒絕所有的規則之前﹐我們必須先行將需要接受的連線 ACCEPT 進來。

您或許發現上面的 script 並沒有對外開放任何服務﹐因為從外部界面進入的 NEW 連線都給 DROP 掉了。如果您需要在機器上提供相關服務﹐那您可以逐一加上﹐最後﹐您的 script 或許會長這個樣子﹕

#!/bin/bash
#
# Script name: ipt_server
# A simple script for firewall, used in Linux (kernel 2.4.x),
# with certain services provided to outside world.
#
# Copyleft 2002 by netman (netman@study-area.org).
#
# Redistribution of this file is permitted under the terms of 
# the GNU General Public License (GPL).
#
# Date: 2002/07/03
# Version: 1.4

PATH=/sbin:/usr/sbin:/bin:/usr/bin
RC_SQUID=/etc/rc.d/init.d/squid
EXT_IF=ppp0
INT_IF=eth0
TRUSTED_TCP_PORT="20 21 22 25 53 80 110 113 143 220 443 465 993 995"
TRUSTED_UDP_PORT="53"
ALLOWED_ICMP="0 3 3/4 4 11 12 14 16 18"

#
# ------------- ensure iptables ----------
which iptables &>/dev/null || {
	echo 
	echo "$(basename $0): iptables program is not found."
	echo "	Please install the program first."
	echo 
	exit 1
}

# ------------- disable ipchains ----------
lsmod | grep ipchains &>/dev/null && {
	echo "Disabling ipchains..."
	rmmod ipchains
}

# ------------- modules -----------
echo "Loading modules..."
modprobe ip_tables &>/dev/null || {
    echo -n "$(basename $0): loading ip_tables module failure."
    echo " Please Fix it!"
    exit 3
}
for file in /lib/modules/`uname -r`/kernel/net/ipv4/netfilter/ip_conntrack_*.o
do
    module=$(basename $file)
    modprobe ${module%.*} &>/dev/null
done
for file in /lib/modules/`uname -r`/kernel/net/ipv4/netfilter/ip_nat_*.o
do
    module=$(basename $file)
    modprobe ${module%.*} &>/dev/null
done

# ------------- ipforwarding -----------
echo "Turning on IP forwarding..."
echo "1" > /proc/sys/net/ipv4/ip_forward

# ------------- anti spoofing -----------
echo "Turning on anti-spoofing..."
for file in /proc/sys/net/ipv4/conf/*/rp_filter; do
	echo "1" > $file
done

# ------------- flushing ----------
echo "Cleaning up..."
iptables -F -t filter
iptables -X -t filter
iptables -Z -t filter
iptables -F -t nat
iptables -X -t nat
iptables -Z -t nat

# ------------- policies -------------
echo "Setting up policies to ACCEPT..."
iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -t nat -P PREROUTING ACCEPT
iptables -t nat -P POSTROUTING ACCEPT
iptables -t nat -P OUTPUT ACCEPT

# ------------- ICMP -------------
echo "Creating icmpfilter chain..."
iptables -N icmpfilter
for TYPE in $ALLOWED_ICMP; do
    iptables -A icmpfilter -i $EXT_IF -p icmp \
        --icmp-type $TYPE -j ACCEPT
done

# ------------- services ------------
echo "Creating services chain...."
iptables -N services
for PORT in $TRUSTED_TCP_PORT; do
    iptables -A services -i $EXT_IF -p tcp --dport $PORT -j ACCEPT
done
for PORT in $TRUSTED_UDP_PORT; do
    iptables -A services -i $EXT_IF -p udp --dport $PORT -j ACCEPT
done

# ------------- block -------------
echo "Creating block chain..."
iptables -N block
iptables -A block -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A block -m state --state NEW -i ! $EXT_IF -j ACCEPT
iptables -A block -j DROP

# ------------- filter -------------
echo "Filtering packets..."
iptables -A INPUT -j icmpfilter
iptables -A INPUT -j services
iptables -A INPUT -j block
iptables -A FORWARD -j icmpfilter
iptables -A FORWARD -j block

# ------------- masq -------------
echo "Masquerading internel network..."
iptables -t nat -A POSTROUTING -o $EXT_IF -j MASQUERADE

# ------------- tproxy -------------
$RC_SQUID status | grep pid &>/dev/null && {
	echo "Enabling transparent proxy...."
	INT_IP=$(ifconfig | grep $INT_IF -A 1 \
		| awk '/inet/ {print $2}' | sed -e s/addr\://)
	if [ -z "$INT_IP" ]; then
		echo 
		echo "$(basename $0): there is no IP found on $INT_IF."
		echo "	Please make sure $INT_IF is setup properly."
		echo 
		exit 3
	fi
	iptables -t nat -A PREROUTING -d $INT_IP -i $INT_IF \
		-p tcp -m tcp --dport 80 -j ACCEPT
	iptables -t nat -A PREROUTING -i $INT_IF -p tcp -m tcp \
		--dport 80 -j REDIRECT --to-ports 3128
}
exit 0
## EOS

(上面的 script 我將之取名為 ipt_server ﹐您可以 點這裡 下載使用﹐您只需找到您想要提供的服務之 port number ﹐然後修改 script 前面的 TRUSTED/UNTRUSTED_PORT 變數就可以了。)

保存設定

剛纔介紹的規則﹐您可以在任何時候執行它﹐這樣就能夠將防火牆設定起來﹐並能為內部網路提供 IP 偽裝服務了。但是﹐如果您重新開機的話﹐那麼這些規則會全部還原為 ACCEPT ﹐也就是您必須再次執行這個個 script 才能重新將設定要回來。

那麼﹐您或許想知道如何在開機的時候如何自動的將防火牆設定起來吧﹖

事實上﹐要做到這個要求是有很多方法。聰明的您或許會首先打 /etc/rc.d/rc.local 的主意吧﹖ ^_^ 沒錯﹐您甚至可以將整個 script 的內容(第一行除外)加進 rc.local 的最後面去。不過﹐更聰明的您應該會將 script 存放在一個目錄下面﹐加上 +x 權限﹐然後在 rc.local 中將該 script 的路徑加上去就好了。

那是否有更好的方法呢﹖有的﹗iptables 套件本身就有提供一個非常好用的工具﹕iptables-saveiptables-restore 。您可以在任何時候執行 iptables-save 然後用文字檔將結果存起來﹕

iptables-save > ipt_settings

然後﹐您可以在任何時候﹐再用 iptables-restore 將結果要回來﹕

iptables -t filter -F
iptables -t filter -X
iptables -t nat -F
iptables -t nat -X
iptables-restore < ipt_settings

注意﹕執行 iptables-restores 的時候﹐如果您不將現有的規則清掉的話﹐現有規則將會保留起來並排在前面。

換句話說﹐您只要用 iptables-save 將規則保存起來﹐然後開機的時候在 rc.local 中用 iptables-restore 將結果要回來就行了。然而﹐更讓您驚喜的是﹕在 RH7.1 中﹐還有更好的方法哦~~ 啊﹖真的嗎﹖沒錯~~ ^_^

您還記得前面我教您執行 ntsysv 將 ipchains 取消並將 iptables 選擇起來嗎﹖您想知道這動作是做什麼用的嗎﹖啊~~ 這又回到 Linux 的系統管理知識中了(剛纔談的 rc.local 就是系統管理技巧之一)﹕如果您對系統的開機流程熟識(請參考 system 系列之“ 開機與關機 ”)﹐您應該知道所有開機的 daemon 都放在 /etc/rc.d/init.d 這個目錄下面﹐然後按 Run Level 的要求﹐再 link 到相應的 rcX.d 目錄去。

您可以仔細檢查 /etc/rc.d/init.d/iptables 這隻開機 script ﹐您會發現它會去找 /etc/sysconfig/iptables 這個設定檔﹐如果找到的話﹐就從這個檔中抓出所有規則﹐然後用 iptables-restore 設定回來。

那麼﹐您現在有靈感了嗎﹖也就是說﹐只要您用 iptables-save 將結果存到 /etc/sysconfig/iptables 中的話﹐那麼﹐開機時﹐iptables 這個 daemon 就會幫您自動將防火牆規則設定好了﹗夠方便吧﹖ ^_^

Tips:不過,與 iptables 相關的 ip_nat_*.o 模組,並沒辦法透過 /etc/sysconfig/iptables 載入,這還是要動到 rc.local 來輔助完成。

架設 NAT

前面所介紹的﹐基本上能滿足一般的家庭使用者了。但如果您手頭上有更多機器﹐甚至有自己的 DMZ 網路﹐那您還可以用 iptables 幫您設定 NAT 哦。使用 NAT 有什麼好處呢﹖

首先﹐如果您當初架設的網路使用了私有位址﹐而現在又想連上 internet ﹐雖然您可以對您的網路重新分配 IP 位址﹐但這樣做是非常費心和隱藏的問題多多。這時候﹐ NAT 就可以幫到您了﹕您只需用一台 Linux 主機安裝兩片網路卡﹐一張使用合法 IP 位址來擔當對外的連線﹐另外一張使用私有 IP 負責和內部網路溝通。當然﹐這台 Linux 主機可以同為上面所介紹的、負責 IP masquerading 的主機。

然後﹐您為那張對外連線的網卡建立多個 IP alias ﹐當收到傳給那些 IP alias 的請求的時候(或是不用 ip alias 也行﹐只要 socket 分配好就行)﹐就可以把這些請求通過 NAT 轉送給內部真正負責網路服務的伺服器﹐而那些伺服器根本無需更改 IP 位址﹐依舊使用原來的私有 IP 就可以(但機器名稱可能需要修改為外部能查詢到的、並且查詢到的 IP 要指向 NAT 的外部界面)。

使用 NAT 還有一個好處是﹐可以分流網路負擔 (load balancing) ﹐也就是說﹐您可以將同一個 socket 的請求分別導向好幾台執行相同服務的伺服器(還記得 NIS 和 NFS 的妙用嗎﹖)。這樣對於處理大量數據的服務是非常好用的(不過相關的設定細節我還沒有機會來實作啦~~)。

好了﹐說了這麼多﹐您是否已經非常蠢蠢欲動了呢﹖您可以寫一個 script 來啟動 NAT 的功能。在下面的例子中﹐我的 Linux 主機是唯一連接外界的橋樑﹐它有三個網路界面﹕一個連接 Internet ( 事實上是撥接 ADSL 的 ppp0 界面)﹔另一個界面連接 DMZ 網路﹐我將提供 internet 服務的主機放在一個所謂的 DMZ 網路﹔而最後一個界面則連接私有網路﹐我將私有網路簡單的整個給 masquerade 掉﹐裡面完全不提供任何 internet 服務﹐但私有網路可以直接連接 DMZ 網路上面的服務主機。如圖所示﹕


圖九

這裡使用的網路環境或許和真實的環境有所不同。有一個地方一定要注意﹕您必須確定 routing 是正確的。在設定 DMZ 最容易犯的錯誤是使用和外部網路一樣的 Net_ID﹐這樣的話﹐無論內部網路還是外部網路﹐都無法和 DMZ 溝通的。當然﹐這也不是不能解決的﹐方法之一就是用 Proxy ARP 來解決。

Tips﹕Proxy ARP 的做法很簡單﹐您只要找到 NAT 左右兩邊網路的機器的 IP 位址﹐以及連接兩邊的網路卡 MAC 位址。然後在左邊網路卡上設定右邊網路的靜態 ARP ﹔以及在右邊的網路卡上設定左邊網路的靜態 ARP 位址﹐同時用靜態路由來指定界面就好﹕

arp -s 1.2.3.5 00:02:B3:4B:69:49 pub
route add -host 1.2.3.5 dev eth1

這樣就行了。上面的例子中﹕1.2.3.5 是右邊網路的機器之 IP 位址﹐而 00:02:B3:4B:69:49 則是左邊網路卡(eth0)的 MAC 位址。

不過﹐如果可以的話﹐我建議您另外一個私有網路來架設 DMZ ﹐然後用 NAT 技術讓它和外面溝通(也就是下面這個 script 的功能)。除了可以節省 IP 之外﹐也較為安全。假如您的 DMZ 使用另一個真實 IP 且可以直接和外面路由的話﹐那麼﹐下面的 script 將不適用﹐您必須小心的修改關於 DMZ 的規則﹐不要在 POSTROUTING 上做 MASQUERADE、也不要在 PREROUTING 上做任何的 DNAT 動作﹐只需在 FORWARD 上面設定 ACCEPT 就好了。請一定要留意﹗

#!/bin/bash
#
# Script name: ipt_nat
# A simple script for firewall, used in Linux (kernel 2.4.x),
# with certain services provided to outside world,
# via redirecting to a single DMZ machine.
#
# Copyleft 2002 by netman (netman@study-area.org).
#
# Redistribution of this file is permitted under the terms of
# the GNU General Public License (GPL).
#
# Date: 2002/10/20
# Version: 1.4

PATH=/sbin:/usr/sbin:/bin:/usr/bin
RC_SQUID=/etc/rc.d/init.d/squid

# ------------- giving info ----------
EXT_IF=ppp0
INT_IF=eth1
DMZ_IF=eth2
DMZ_SERVER="10.0.2.2"

TRUSTED_TCP_PORT="21 22 25 53 80 110 113 143 220 443 465 993 995"
TRUSTED_UDP_PORT="53"
ALLOWED_ICMP="0 3 3/4 4 11 12 14 16 18"

# ------------- ensure iptables ----------
which iptables &>/dev/null || {
        echo
        echo "$(basename $0): iptables program is not found."
        echo "  Please install the program first."
        echo
        exit 1
}

# ------------- FUNCTION: grap nic info ----------
netinfo () {
        unset IF
        unset IP
        unset NET
        unset MASK
        for IF in $1; do
                IP=$(ifconfig | grep "$IF " -A 1 \
                        | awk '/inet/ {print $2}' | sed -e s/addr\://)
                if [ -z "$IP" ]; then
                        echo
                        echo "$(basename $0): there is no IP found on $IF."
                        echo "  Please make sure $IF is setup properly."
                        echo
                        exit 2
                fi
                MASK=$(ifconfig | grep "$IF " -A 1 \
                        | awk '/inet/ {print $4}' | sed -e s/Mask\://)
        getnet () {
            for i in $(echo $IP | sed -e 's/\./\ /g'); do
                let N="$i & $1"
                NET="$NET.$N"
                #shift
            done
        }
        getnet $(echo $MASK | sed -e 's/\./\ /g')
        done
}

# ------------- have network info ----------
netinfo $EXT_IF
    EXT_IP=$IP
    EXT_NET=${NET#.}/$MASK
netinfo $INT_IF
    INT_IP=$IP
    INT_NET=${NET#.}/$MASK
netinfo $DMZ_IF
    DMZ_IP=$IP
    DMZ_NET=${NET#.}/$MASK

# ------------- disable ipchains ----------
lsmod | grep ipchains &>/dev/null && {
        echo "Disabling ipchains..."
        rmmod ipchains
}

# ------------- modules -----------
echo "Loading modules..."
modprobe ip_tables &>/dev/null || {
        echo -n "$(basename $0): loading ip_tables module failure."
        echo " Please Fix it!"
        exit 3
}
for file in /lib/modules/`uname -r`/kernel/net/ipv4/netfilter/ip_conntrack_*.o
do
        module=$(basename $file)
        modprobe ${module%.*} &>/dev/null
done
for file in /lib/modules/`uname -r`/kernel/net/ipv4/netfilter/ip_nat_*.o
do
        module=$(basename $file)
        modprobe ${module%.*} &>/dev/null
done

# ------------- ipforwarding -----------
echo "Turning on IP forwarding..."
echo "1" > /proc/sys/net/ipv4/ip_forward

# ------------- anti spoofing -----------
echo "Turning on anti-spoofing..."
for file in /proc/sys/net/ipv4/conf/*/rp_filter; do
        echo "1" > $file
done

# ------------- flushing ----------
echo "Cleaning up..."
iptables -F -t filter
iptables -X -t filter
iptables -Z -t filter
iptables -F -t nat
iptables -X -t nat
iptables -Z -t nat

# ------------- policies -------------
echo "Setting up policies to ACCEPT..."
iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -t nat -P PREROUTING ACCEPT
iptables -t nat -P POSTROUTING ACCEPT
iptables -t nat -P OUTPUT ACCEPT

# ------------- ICMP -------------
echo "Creating icmpfilter chain..."
iptables -N icmpfilter
for TYPE in $ALLOWED_ICMP; do
        iptables -A icmpfilter -i $EXT_IF -p icmp \
                --icmp-type $TYPE -j ACCEPT
done

# ------------- DMZ ------------
echo "Setting up rules for dmz...."
for PORT in $TRUSTED_TCP_PORT; do
        iptables -t nat -A PREROUTING -i $EXT_IF -p tcp -d $EXT_IP \
                --dport $PORT -j DNAT --to-destination $DMZ_SERVER
done
for PORT in $TRUSTED_UDP_PORT; do
        iptables -t nat -A PREROUTING -i $EXT_IF -p udp -d $EXT_IP \
                --dport $PORT -j DNAT --to-destination $DMZ_SERVER
done

# ------------- services ------------
echo "Creating services chain...."
iptables -N services
for PORT in $TRUSTED_TCP_PORT; do
        iptables -A services -i $EXT_IF -p tcp --dport $PORT -j ACCEPT
done
for PORT in $TRUSTED_UDP_PORT; do
        iptables -A services -i $EXT_IF -p udp --dport $PORT -j ACCEPT
done

# ------------- block -------------
echo "Creating block chain..."
iptables -N block
iptables -A block -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A block -m state --state NEW -i lo -j ACCEPT
iptables -A block -m state --state NEW -i $INT_IF -j ACCEPT
iptables -A block -m state --state NEW -i $DMZ_IF -d $DMZ_IP -j DROP
iptables -A block -m state --state NEW -i $DMZ_IF -d $EXT_IP -j DROP
iptables -A block -m state --state NEW -i $DMZ_IF -d ! $INT_NET -j ACCEPT
iptables -A block -j DROP

# ------------- filter -------------
echo "Filtering packets..."
iptables -A INPUT -j icmpfilter
iptables -A INPUT -j block
iptables -A FORWARD -j icmpfilter
iptables -A FORWARD -j services
iptables -A FORWARD -j block

# ------------- masq -------------
echo "Masquerading outgoing traffic ...."
iptables -t nat -A POSTROUTING -o $EXT_IF -j MASQUERADE

# ------------- tproxy -------------
$RC_SQUID status | grep pid &>/dev/null && {
        echo "Enabling transparent proxy...."
        INT_IP=$(ifconfig | grep $INT_IF -A 1 \
                | awk '/inet/ {print $2}' | sed -e s/addr\://)
        if [ -z "$INT_IP" ]; then
                echo
                echo "$(basename $0): there is no IP found on $INT_IF."
                echo "  Please make sure $INT_IF is setup properly."
                echo
                exit 3
        fi
        iptables -t nat -A PREROUTING -d $INT_IP -i $INT_IF \
                -p tcp -m tcp --dport 80 -j ACCEPT
        iptables -t nat -A PREROUTING -i $INT_IF -p tcp -m tcp \
                --dport 80 -j REDIRECT --to-ports 3128
}

# ------------- status -------------
echo "-----------------------------------------------"
echo "Network summary:"
echo -n "The function of IP_FORWARD is "
if [ "$(cat /proc/sys/net/ipv4/ip_forward)" = "0" ]; then
        echo -n "OFF"
else
        echo -n "ON"
fi
echo "."
echo "Internal:"
echo "  interface: $INT_IF"
echo "  ip: $INT_IP"
echo "  network: $INT_NET"
echo "External:"
echo "  interface: $EXT_IF"
echo "  ip: $EXT_IP"
echo "  network: $EXT_NET"
echo "DMZ net:"
echo "  interface: $DMZ_IF"
echo "  ip: $DMZ_IP"
echo "  network: $DMZ_NET"
echo
exit 0
## EOS

(上面的 script 我將之取名為 ipt_nat ﹐您可以 點這裡 下載使用﹐您只需找到您想要提供的服務之 port number ﹐然後修改 script 前面的 TRUSTED/UNTRUSTED_PORT 、以及 DMZ_SERVER 等變數就可以了。)

然而﹐不管您如何使用前面介紹的 script ﹐最新、最好用的則是我的另外一隻稱為 ipt_config 的 script (您可以 點這裡 下載)。我建議您用這隻 script 來設定您的 NAT 伺服器。這隻 script 讓您有更大的設計空間﹐而且使用上也非常方便﹐不過就需要您比較了解連線 socket 的概念﹐建議執行 ipt_config help 看看它的限制和使用辦法。不過﹐如果您要修改它以符合您實際的需求﹐我建議您先從前面三隻 script 著手﹐這樣您會比較知道要修改哪裡﹐和如何修改。

附註﹕ 我必須強調的是以上提供的範例僅僅是例子。如果您在沒有首先花時間了解您自身的需要、您的環境、以及您想要之服務的含義與複雜性﹐請勿盲目地運用這些例子。您必須認知到那些例子並非很完善﹐它們仍然需要修正。

安全考量

安全問題是一個非常龐大且牽涉甚廣的題目﹐很難用三言兩語就解釋得清楚。期中的一個因素是每一個網路環境和要求都是不一樣的。我們在設定防火牆的時候﹐進行網路規劃和規則設定順序是很重要的。

例如設定 DMZ﹐不管您是用真實 IP 還是私有 IP﹐將那些對外伺服器和內部伺服器分開是個明智之舉。往往﹐在 DMZ 裡面的機器﹐雖然也可以受到防火牆的保護﹐但畢竟是對外開放的﹐總有被攻擊的危險。所以﹐我們常會以‘假設 DMZ 主機攻破後怎麼辦﹖’的考量去設定防火牆的預設失效模式。換而言之﹐DMZ 裡面的主機是可以隨時被‘犧牲’掉的﹐但內部網路呢﹖可不能這樣玩了﹐當然﹐任何一位安全專家也無法保證內部網路不被攻破﹐但起碼﹐通往內部網路的防火牆應該是最嚴格的。

如果有條件﹐多架一個防火牆在 DMZ 和內部網路之間﹐而該主機是內部網路的唯一出路。也就是說﹐將 DMZ 架於外部防火牆與內部防火牆之間﹔而內部防火牆則分別連接 DMZ 和內部網路﹐它必須再經一道外部防火牆出去。如果再於 DMZ 裡面建置隱蔽性的檢查裝置、充份的記錄設定、以及有效的預警機制。那麼當第一個防火牆失效之後﹐起碼還有第二道防火牆來保護內部網路﹐從而提供更多預警時間在入侵者攻破第二道防火牆之前做出適當反應。當然﹐兩個防火牆的設定﹐將比設定一個防火牆要複雜很多﹐而且測試也費時許多﹐因為往往在判定問題(通常是過嚴)出在哪個防火牆是頗難確定的。如果覺得還不夠﹐那就再使用代理服務器﹐而只允許代理服務器的封包進出這個閘道。

無論如何﹐防火牆自身的安全設定是至關重要的﹐其中一個守則必須遵守﹕一切從嚴﹗在我們架設防火牆的時候﹐應該採取“預設拒絕狀態”﹐也就是﹕將 ACCEPT 的必要控制在最低限度﹐然後 DROP 掉所有的。

作為連接外部網路的主機(包括防火牆本身)﹐盡可能安裝最少極限的服務﹐諸如 telnet 、r-serials、x protocol 等危險性高的服務﹐甚至連 FTP 和 HTTP 等普通服務也一概移除。這裡說的移除﹐並非指單純的不啟動服務﹐而是根本就不在主機上面安裝該等服務(或從核心中移除)。同時﹐在那些提供 internet 服務的主機上面﹐除了它所提供的服務外﹐也不要安裝其它服務﹐而且最好還是只跑單一服務。 最高原則是﹕不要的服務一概不提供。

防火牆的設計是非常複雜和多樣性的﹐很難提供一個唯一方案而適合所有的網路狀況。任何微小的疏忽﹐都可能導致您所有的努力付諸東流。 如果以我個人的認識來看﹕了解網路環境、評估服務要求、設定防火牆、和測試﹐這四項工作的時間比例﹐將是 30%﹐30%﹐20%﹐20%。如果您不了解一個網路的環境﹐很難設計出合理的防火牆﹐尤其在 NAT 環境之後﹐情形將更是複雜。

測試

在測試過程中如果失敗﹐多數是因為規則設定得過於嚴格所至﹐您可以先從最寬鬆的設定開始、然後逐級逐級的向嚴謹方向調整。要檢查您目前的設定狀態﹐輸入 iptables -L 來檢查目前的規則狀態。它預設上只顯示 filter 這個 table 裡面的 chains 規則﹐如果您要顯示 nat 的 chains 規則﹐則必須加上 -t nat 這個參數﹐例如﹕

iptables -t nat -L PREROUTING -n

這樣的話﹐就會將 nat 這個 table 中的 PREROUTING 鏈中的規則顯示出來。如果您在 -L 後面沒指定是哪個 chains 的話﹐那麼﹐關於這個 table 裡面的所有 chains 都會顯示出來。最後面的 -n 參數是要用數字方式來顯示﹐這樣可以很快的看到所有規則﹐否則可能會因為名稱查詢的關係而遭到延遲﹐您自己看著辦吧。

Tips﹕我個人的經驗是﹕從 -L 所列視出來的規則並不容易看懂﹐我寧願執行 iptables-save 然後從它的 STDOUT 來讀取每一行規則。這方法對您來說或許不適用﹐但我卻用得很開心~~ ^_^

假如您要將一些過嚴的限制暫時解除掉﹐可以用 -D 命令來刪除一行規則﹕

iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 0:1024 -j DENY

假如 PRETOUTING 這個 chain 中有相同的多行規則的話﹐那第一行符合這個設定的規則就會被刪除。如果您知道這行規則是第 17 行的話﹐那您可以直接刪除它的行號﹕

iptables -t nat -D PREROUTING 17

這裡的 -D 命令是逐行刪除規則的﹐假如您想一次過清空一條 chains 中的所有規則的話﹐那可以改用 -F 參數﹕

iptables -t nat -F PREROUTING

如果您執行 -F 命令的時候﹐指定了 table 而不指定 chain 名稱的話﹐那麼該 table 下面的所有 chains 都會被清空﹕

iptables -t nat -F

假如您連 table 都沒指定﹐那預設上就是清空 filter 這個 table 裡面的所有 chains 了。

Tips﹕我們用 -F 命令只作用於內建 chains 而已﹐如果是用戶自定的 chains ﹐則改用 -X 命令。

所以﹐如果我們需要重新設定 iptables 的話﹐您可以這樣執行﹕
iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -t nat -P PREROUTING ACCEPT
iptables -t nat -P POSTROUTING ACCEPT
iptables -t nat -P OUTPUT ACCEPT
iptables -F
iptables -X
iptables -Z
iptables -t nat -F
iptables -t nat-X
iptables -t nat-Z

這樣﹐所有設定都會打回原形﹐然後您就可以施然地逐漸增加規則了。

Tips﹕注意哦﹕如果您是從遠端連線上來的話﹐請先設定 Police 然後再執行 -F﹐否則可能馬上就給擋在外面了。在增加規則的時候﹐也同樣要注意這個問題。我建議您先為自己留一個後門﹐然後等測試通過之後﹐才將後門拿掉。

當然﹐您也可以用另的方法來排除某些規則﹐比方說您懷疑某些 DROP 規則可能會有問題﹐那您可以用 "-I" 命令將那些規則插 (Insert) 到 chain 的前面去﹐以確定是否那些規則影響了連線。再不然﹐用 -P 來改變原則 (Policy) 設定也可以將問題限定(或排除)於某一大範圍之內。如果當您將某一條 chain 的 policy 設定為 ACCEPT﹐然後將其全部規則都清除之後﹐能夠讓您將本來連不上線的連線接通的話﹐那您就可以肯定問題與這條 chain 有關了。

最後一個要留意的地方是 routing 的設定﹐您必須確定其他機器有通往上網主機的 gateway ﹐也要確定 Linux 主機能 route 回到各機器。有時候﹐您或許無意中在 Linux 主機上面設定了自己的 Default Gateway ﹐您要確定它在撥接之後的 Default Gateway 要指向 ISP 那端。任意兩個連接到 NAT 不同界面的網路﹐避免使用相同的 network ID (除非您確定能用 Proxy ARP 來解決)﹐這在網路經過切割之後﹐那就要更加小心了。總而言之﹕您的防火牆必須建在您的網路必經之路上﹔如果是做 NAT 的話﹐那麼必須確定關於改連線的、雙向的、所有的封包都必須經過同一 NAT 主機。

有時候﹐當您檢查所有規則和路由都找不出問題所在﹐借用 tcpdump 之類的封包擷取軟體﹐抓封包來看也非常有幫助。比方說關於一個 DNAT 的設定問題﹕

1) 從 client 送出的封包﹐是否能正確 resolve 為 IP ﹖
2) 該 IP 是否正確的被路由到 NAT 外部界面﹖
3) 然後這個封包到達 NAT 外部界面的時候﹐是否被接收進來﹖
4) 然後是否能正確的轉換 destination socket?
5) 轉換後是否能順利 forwarded (例如 routing 判斷)?
6) 是否能順利從內部界面送出 ﹖
7) 被修改後的封包是否能到達內部 server ?
8) 內部 server 是否能接受這個封包﹖
9) 內部 server 是否能正確讀到封包的 source socket ﹖
10) 內部 server 在處理完之後﹐是否能順利將 source socket 換成 destination socket ?
11) 然後這個封包是否能順利送到 NAT 的內部界面﹖
12) NAT 的內部界面是否能接受這個封包﹖
13) NAT 是否能順利復原遭替換的 socket ﹖並變成 source socket ?
14) 封包能否正確的進行路由判斷而從外部界面送出﹖
15) 封包是否能順利送回 client 端﹖
16) 封包的 source socket 是否與 client 所 expect 的一致﹖
17) 封包的其它監測資訊(如 checksum、seq/ack number)是否能被接受﹖
18) 封包的內容所攜帶的資訊是否正確﹖(例如您本來以為會得到 A 結果卻是 B 之類的)
......

如果您能從每一個步驟追蹤封包的話﹐那應該很容易知道問題所在。比方說﹐您能在 NAT 上發現封包已經抵達外部界面﹐但卻不見它從內部界面送出。那您可以合理的懷疑 PREROUTING、FORWARD、和 POSTROUTING 的設定﹐其一是有問題的。

至於其他的﹐靠您自己的經驗囉~~~ 關鍵還是多實作、多變通、多思考啦。當然了﹐我也非常歡迎您 來信 和我討論相關的技術問題﹐只是﹐除了您本身要具備足夠的基礎知識外(請參考文章的開頭部份)﹐我還有一個條件﹕您必須信任我、同時使用真實的資料以及詳細的描述環境。否則﹐我可能會拒絕和您進行深入的討論﹐因為過往我花了太多冤枉時間在不實資料的討論上了、甚至還險些得罪素未謀面的朋友。希望您能體諒﹐謝謝﹗

 

 


www.study-area.org © 2002 Netman 網中人
Last Updated: October 20, 2002