カッコや註だらけにならないように、これから頻出することになる基本的なネットワーク用語についてまず定義を明確にしておかなくてはならない。
なお、TCP/IPのレイヤーの呼び方は、なるべくTCP/IP参照モデルの4層モデルのものを使うことにする。なるべく、基本的には。
普通一般にファイヤーウォールと言えば、ほとんどはルータベースである。しかし、今日のカーネルと iptables では、レイヤー1 でまでパケットフィルタリングができるようになった。これを利用すると、ネットワークを複数のサブネットに分割することなく、単一のIPセグメントに属するコンピュータ群を物理的に幾つかの島に分けて、その間にLinuxファイヤーウォールマシンを1個のハブのように差し込んでファイヤーウォールを掛けることができる。これを、ポートベースのファイヤーウォールとか、透過型ファイヤーウォール、Bridged Firewall あるいは Bridgewall などと呼ぶ。透過型なら、ファイヤーウォール導入にあたって各コンピュータのIPアドレス体系を変えたりデフォルトゲートウェイを再設定して廻ったりといった、面倒で危険な一大パーティを催さなくて済むのだ。
Transparent FW と示したのがこのLinuxファイヤーウォールマシンだ。Host1 と Host2 はKVMホストマシン。手近な材料で済ませるため KVM仮想環境上で検証を行なった都合で、このような図になっている。透過型FWマシンには少なくとも2個のネットワークインターフェースが必要だ。一方のインターフェースは信頼できないマシンたちを繋いだスイッチ (Physical Backyard Switch-2 の系統) に、もう一方は信頼できるつまり保護したいマシンたちを繋いだスイッチ (Physical Backyard Switch-1 系) に接続する。Backyard Switch-2 は、複数の仮想環境ホストがある場合に他のホスト上の仮想マシンにも透過型FWが使えるよう橋渡しをするだけのためにあり、決して信頼系のスイッチやその他の上流スイッチに繋いではいけない。フレームは、例えば上図の2本の矢印線のような経路でネットワークを往き来する。
透過型FWは、基本的には、パケットの出入りするインターフェース (eth0 or eth1 ) に基づいてフィルタリングを行う。もちろん、追加条件として、慣れ親しんだソースIPアドレスや宛先IPアドレス、ポートナンバー等々も使える。
ファイヤーウォールマシンにはまず論理ブリッジインターフェース br0 を作成する。そして、左図で Pyhs. Interface として示した物理インターフェース (当実験では仮想マシンなので仮想NICだが) をブリッジのスレーブインターフェースとして組み込む。すると、点線で囲んだものがソフトウェアスイッチングハブと化す。eth0 と eth1 は言うなればハブの口だ。実際、ブリッジ br0 は、各物理インターフェースの向こう側にあるMACアドレスを学習・維持し、まんまラーニングブリッジとして振る舞う。おまけに、Linux の bridge は、ありがたくもスパニングツリーによるループ防止まで標準装備している。物理インターフェースはIPアドレスを持たず、明示的に Promiscuous(無差別)モードを有効にするまでもなく(※コラム)、全てのフレームを受け取り、出るべきインターフェースまたは透過型ファイヤーウォールマシン自体へ引き渡す。
ただ、マシンがまったく IPアドレスを持っていないと、物理コンソールや仮想マシンコンソールで直接ターミナルを開くしか管理方法がなく、ファイヤーウォールルールのメンテナンスにも困るので、IPアドレスを持たせたい。管理用IPアドレスは、br0 に振る。決して物理インターフェースにIPを振ってはいけない。あるいは、ブリッジポートとして使うNICとは別に管理用のNIC(例えば eth2) を付与するという手もある (その分ルールが複雑になるのであまりお勧めしない)。
感覚的にさえ分かる通り、bridgeに組み込んだ物理インターフェースは自分のMACアドレス宛てに限らず NICに触れたフレームはとにかく選り好みなく取り込んでくれないと、ブリッジとして用をなさない。通常、インターフェースをプロミスキャスモードにするには `ifconfig eth0 promisc' のようにコマンドする(ifcfg-IF ファイルでの PROMISC=yes パラメータは最近 initscripts セットから廃止された模様)。しかし、bridgeに物理インターフェースを組み込む `brctl addif br0 eth0' は eth0 を暗黙的にプロミスキャスモードにする。ここで早合点して `ifconfig eth0' とやると、PROMISC フラグの表示は見あたらず、ほら無差別モードになってないじゃないか、ということになる。実は、brctl に伴って有効にされた PROMISC フラグは ifconfig に表示されないそうである。見たければ /sys/class/net/IF/flags を読む。
# grep . /sys/class/net/*/flags
/sys/class/net/br0/flags:0x1003
/sys/class/net/eth0/flags:0x1103
/sys/class/net/eth1/flags:0x1103
/sys/class/net/lo/flags:0x9
ビットの意味のリストは /usr/src/kernels/$(uname -r)/include/linux/if.h ヘッダファイルの /* Standard interface flags (netdevice->flags). */ という節に書かれている。全部載せると場所を喰うので上記出力に一致する分だけ抜粋すると、
#define IFF_UP 0x1 /* interface is up */ #define IFF_BROADCAST 0x2 /* broadcast address valid */ #define IFF_LOOPBACK 0x8 /* is a loopback net */ #define IFF_PROMISC 0x100 /* receive all packets */ #define IFF_MULTICAST 0x1000 /* Supports multicast */
なので、eth0 (0x1103) は UP BROADCAST PROMISC MULTICAST のフラグが立っていることが晴れて明らかになる。こう書いてしまってから何だが、kernels/ 配下の一式は kernel-devel パッケージで提供されるもので、当ページの通りにインストールしたのならインストールされていないので存在しない、悪しからず。
もうひとつ肝心なのが、CentOS/RedHat系ディストリビューションのデフォルト状態では、ブリッジを通っているフレームは iptables から見えないという点だ。本来 iptables が操作対象とするのはTCP/IP参照モデルのレイヤー2と3 (及びレイヤー4の一部)。ネットワークインターフェース層の住人であるフレームは、カーネルの備える bridge-nf (bridge-netfilter/br-nf) コードの助けを借りて初めて iptables で扱えるようになる。その使用/不使用は、sysctl ツリーにある iptables, ip6tables, arptables 毎の net.bridge.bridge-nf-call-* キーでオン/オフできる。有効にすると、bridge-nf がフレームからパケットを取り出して、カーネルの Netfilterスタックつまり iptables から観測・操作できるテーブルやチェーンの中を通らせる。bridge-nf と Netfilter/iptables との関係は、Ralf Spenneberg が Linux-Kongress に寄稿した Bridgewalling - Using Netfilter in Bridge Mode を読むとよく分かる。
ちなみに、レイヤー1 に特化したフレームマニピュレーションプログラムに ebtables (Ethernet Bridge Tables) がある。ARPヘッダやVLANタグに基づいた操作、MACアドレスNATなど、普通には考えられない強力な機能があるようだ。
bridge-nf は、正確に言うと、カーネルにもともと備わっているbridge コードを拡張する、カーネルパッチの呼び名。しかし、いちいち「bridge-nfによって拡張されたbridgeコードが」などと書いていたら書く方も読む方もやりきれないので、単に「bridge-nf コード」のように記す。少なくともCentOS/RHEL 6 にパッケージされているカーネルでは、初めからこのパッチが当たっていて、bridgeはカーネルモジュールとしてコンパイルされている。
当サイトではこれまでファイヤーウォール専用マシンを作るケースには触れてこなかったので、当節では、OSのインストールから順を追って述べていくことにする。ただし、話題がとっちらかっては分かりにくくなると思うので、たまたま筆者の場合そうであった仮想インフラ系の準備については、特段の必要がない限り触れない。検証は CentOS 6.7 で行なった。
今回は、X-Windowもなし、ssh でリモートメンテナンスができれば充分というスパルタンな構成にした。べつに、ご自分の流儀で入れてもらって構わない。
筆者は CentOS-6.X-x86_64-minimal.iso でなく通常の *-x86_64-bin-DVD1.iso を使い、インストールタイプで Minimal を選択した。仮想マシンということもあり、メモリ量を後々いくらに変更しても一応過不足のないよう、スワップは 6GB に。とりあえず、普通に eth0 でインターネットにつながる設定でインストールを進める。インストール結果は、パッケージ数たかだか250、ディスク使用量はわずか 1.2GB となった。あのうっとうしい NetworkManager も入らず、ああ何と幸せなことだ。
# yum install bridge-utils ntp vim-enhanced nano man mutt
hostsファイルに自ホストレコードを書き加える。
10.0.2.10 fw1.hoge.cxm fw1
resolv.conf にdomainを書き加える。
domain hoge.cxm
その代わり、sysconfig/network のHOSTNAME をホスト部だけにする。
HOSTNAME=fw1
sysconfig/network に下記を書き加える。
NOZEROCONF=yes
sysconfig/network-scripts/ifcfg-br0
DEVICE=br0 TYPE=Bridge ONBOOT=yes NM_CONTROLLED=no BOOTPROTO=none IPADDR=10.0.2.10 NETMASK=255.255.255.0 NAME=br0 #STP=on #DELAY=15 #AGEING=300
"Bridge" はこの通り頭大文字で書かないと読み取ってもらえないので注意。NM_CONTROLLED は NetworkManager がインストールされていない場合不要だが書いても害はない。最後の3つは Linux bridge の調整パラメータで、書き方とデフォルト値を示す意味で載せているだけ。AGEING を 0 にするとMACアドレスのラーニングをせず常に全ブリッジポートへフレームを送出するようになる。
sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0 TYPE=Ethernet HWADDR=xx:xx:xx:xx:xx:xx ONBOOT=yes NM_CONTROLLED=no BOOTPROTO=none BRIDGE=br0 NAME=eth0
sysconfig/network-scripts/ifcfg-eth1
DEVICE=eth1 TYPE=Ethernet HWADDR=xx:xx:xx:xx:xx:xx ONBOOT=yes NM_CONTROLLED=no BOOTPROTO=none BRIDGE=br0 NAME=eth1
設定ファイルが一通りできあがったので、機能していることを見る。スタートした際、STPの安全マージンのため $DELAY 秒間は物理インターフェースがブロックされる。また、両端に存在するマシンの数や他のネットワーク機器構成によっては、ブリッジが機能し始めるまで2分ほどかかる場合がある。
# service network restart # ifconfig -a # brctl show # brctl showstp # brctl showmacs
今 iptables は停まっているので、全体図の例えばマシンAからB、BからAと、まるで島分割もブリッジファイヤーウォールも存在しないかのように通信ができるはずだ。疑い深い人はFWマシン上で `service network stop' してみるのもいい。
いよいよ本題である。が、未だ少し埋まっていない外堀があるのでまずはそこから。
/etc/sysctl.conf に下記を追加。
# Enable bridged traffic filtering net.bridge.bridge-nf-call-arptables = 0 net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 0 net.netfilter.nf_conntrack_max = 655360
コメントの下の2行目が、前述した iptables に対して bridge-nf を有効化する設定。最後の行は、iptables のコネクション追跡管理レコードの上限数を既定値 (FWマシンの搭載メモリ2GBの時) の10倍にしている。トラッキングレコードひとつあたり最大350バイトのメモリを占めるということを頭に置いて、搭載メモリ量と相談して値を決めよう。このようにほぼ純粋なブリッジファイヤーウォールの場合、NATやルーティングによってパケットをインターフェース間でフォワードするわけではないので net.ipv4.ip_forward は 0 のままにしておくのが妥当だ。
sysconfig/iptables-config にある下記のコメント記号を外して有効化する。
IPTABLES_SYSCTL_LOAD_LIST=".nf_conntrack .bridge-nf"
これによって、iptables を SysVinitスクリプトで start する度に、前記 sysctl.conf を引数のリストで fgrep してマッチした行が "sysctl -w" される。sysctl.conf に書かれている全ては本来システムブート時に反映されるものなのだが...。ブート時にbridgeカーネルモジュールのロードされるタイミングによってはこれらのsysctl値投入時にエラーが出る場合があるという この問題(libVirtの記事) への対処策なのではなかろうか。何にせよ、これらの値を再チューニングした時には iptables をリスタートすれば反映されるのだから、これはこれでありがたい。
デフォルト状態では iptables からのログも messages へ行ってしまう。ここはファイヤーウォール専用マシンらしく、ログファイルを専用にしよう。/etc/rsyslog.conf に定義を追加する。基本整備項目に挙げたように、rsyslog は Ver.5 から rsyslog7 にアップグレードしてあるものとする。
rsyslog.conf.diff
--- rsyslog.conf.org 2016-01-29 23:35:42.000000000 +0900 +++ rsyslog.conf 2016-01-29 18:18:36.000000000 +0900 @@ -33,6 +33,10 @@ #### RULES #### +# Firewall log +if $programname == 'kernel' and $msg startswith 'iptables: ' then /var/log/firewall.log +& stop + # Log all kernel messages to the console. # Logging much else clutters up the screen. #kern.* /dev/console
これを書いただけでは役に立たない。本丸である iptables ルールファイルで、LOG ターゲットの --log-prefix 引数と協調させて初めてこのログフィルタが効果を発揮する。
このログファイルをローテーションするため logrotrate の設定もすべき。logrotate.d/syslog にファイルパスを追加すればいい。ピンと来ない人はこちらを参照。
やっとここに辿り着いた。本題であるのとは裏腹に、百聞に一見は如かずで最も説明の要らない部分だろう。bridge-nf がフレームから取り出して Netfilter に差し出すパケットがブリッジのどの物理インターフェースを入口/出口にしたものかを特定する拡張マッチが physdev であることは言わずもがなである。filter テーブルの FORWARD チェーンが重要な役割を演ずるこのポリシーでは、INPUTチェーンはFWマシン宛てのパケットだけが、FORWARDチェーンはFWマシンを素通りするパケットだけが通る、といった iptables の基本を頭に置いておくとすんなり読めるはずだ。気絶しそうになったら、Iptablesチュートリアル の Chapter 6. テーブルとチェーンの道のり を手元に広げることをお勧めする。
sysconfig/iptables
# Firewall configuration # # My IP(br0) = 10.0.2.10 # Bridge = br0 # Trusted physdev on bridge = eth0 # Untrust physdev on bridge = eth1 *filter :INPUT DROP [0:0] :FORWARD DROP [0:0] :OUTPUT DROP [0:0] # User defined chains :badphys_in - -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT -A INPUT -i lo -j ACCEPT -A OUTPUT -o lo -j ACCEPT -A INPUT -s 10.0.2.10 -j DROP -A INPUT -m physdev ! --physdev-in eth1 -d 10.0.2.10 -j ACCEPT -A OUTPUT -s 10.0.2.10 -j ACCEPT -A badphys_in -m state --state INVALID -j DROP -A badphys_in -p icmp --icmp-type 8 -j ACCEPT -A badphys_in -p icmp --icmp-type 11 -j ACCEPT -A badphys_in -p udp --sport 68 --dport 67 -j ACCEPT -A badphys_in -p udp --dport 53 -j ACCEPT -A badphys_in -p udp --dport 123 -j ACCEPT -A badphys_in -m hashlimit --hashlimit 3/sec --hashlimit-mode srcip,dstport --hashlimit-name ipt_badphys_in --hashlimit-burst 15 -j LOG --log-level info --log-prefix "iptables: FORWARD packs died " -A badphys_in -p all -j DROP -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT -A FORWARD -m physdev --physdev-in eth0 -j ACCEPT -A FORWARD -m physdev --physdev-in eth1 -j badphys_in -A INPUT -m hashlimit --hashlimit 3/sec --hashlimit-mode srcip,dstport --hashlimit-name ipt_input_hosts --hashlimit-burst 15 -j LOG --log-level info --log-prefix "iptables: INPUT packs died " -A OUTPUT -m hashlimit --hashlimit 3/sec --hashlimit-mode srcip,dstport --hashlimit-name ipt_output_hosts --hashlimit-burst 15 -j LOG --log-level info --log-prefix "iptables: OUTPUT packs died " -A FORWARD -m hashlimit --hashlimit 3/sec --hashlimit-mode srcip,dstport --hashlimit-name ipt_forward_hosts --hashlimit-burst 15 -j LOG --log-level info --log-prefix "iptables: FORWARD packs died " COMMIT
そしてもちろん、
# service iptables start # chkconfig iptables on