第22回 FreeBSDでPacketFilter(pf)を使う

2005/12/27作成


ずっとADSLだったのですが、Bフレッツの工事料金と2か月分の月額使用料がタダだというので、乗り換えることにしました。
せっかくなのでFreeBSDも入れ替えようと思っていたら、ちょうど先日6.0Releaseが出たのでさっそく人柱に。

しばらくさぼっていたら、FirewallについてはIPFilterより最近ではpfの方が主流ということで、いろいろ思い出しながら設定してみることにしました。
pfにはさまざまな機能が提供されており、またライセンスの問題もクリアしているということで、いいことずくめなのだそうです。
でも、高機能な分、設定がかなり複雑ですね。

注意!
ファイアウォールの設定について、言及する箇所がありますが、その設定を推奨するものではありませんし、当然ながらなんら保証もありません。
パケットフィルタリングのポリシーは自分の責任でやりましょう。


1.カーネル

まずはカーネル入れます。
FreeBSDのハンドブックが詳しいです。
カーネルに次のパラメータを追加して再構築します。
いつものことですが、カーネルの再構築の方法については触れません。ハンドブックのどっかに書いてあります。
device          pf
device          pflog
device          pfsync

options         ALTQ
options         ALTQ_CBQ        # Class Bases Queuing (CBQ)
options         ALTQ_RED        # Random Early Detection (RED)
options         ALTQ_RIO        # RED In/Out
options         ALTQ_HFSC       # Hierarchical Packet Scheduler (HFSC)
options         ALTQ_PRIQ       # Priority Queuing (PRIQ)
#options         ALTQ_NOPCC      # Required for SMP build
deviceのところは仮想デバイスなので、Pseudoのあたりに書いておくとカッコイイです。

ALTQは"Alternate queuing of network packets"のことだそうで、直訳すると「代替ネットワークパケットキュー機能」ってことでしょうか。
ネットワークカードによっては使えない場合もあるので、マニュアルを読んで確認しましょう。
そもそもALTQは使わないというのであれば書く必要ないかもしれません。

ALTQ_NOPCCはSMP(マルチプロセッサカーネル)じゃないと使えないと読めるのでコメントアウトしました。

2.rc.conf

これもハンドブックに書いてあります。
面倒がなくていいです。
pf_enable="YES"                 # Enable PF (load module if required)
pf_rules="/etc/pf.conf"         # rules definition file for pf
pf_flags=""                     # additional flags for pfctl startup
pflog_enable="YES"              # start pflogd(8)
pflog_logfile="/var/log/pflog"  # where pflogd should store the logfile
pflog_flags=""                  # additional flags for pflogd startup
IPFilterでも似たようなの設定しましたね。
pf_rulesのところは、"/etc/pf.rules"に変えるのが流行っているようなので、うちでもそうやってみました。
ipf.rulesと同じような機能です。

IPFilterではNATのルールは別に書いていたのですが、pfではNATも同じ設定ファイルに記述するようです。
だからよけいに設定が難しいんですよね。

3.pf.conf (pf.rules)

IPFilterのコンフィグと似ていますが、高機能な分、非常に複雑です。
"/etc/pf.conf"にサンプルがあるのでこれをベースにしてみたのですが、よけい混乱してきたので、マニュアル読みながら書き直すことにしました。

pfのコンフィグはいくつかのステートメントに別れています。
  1. Macros(マクロ):よく使う設定をあらかじめ定義しておく
  2. Tables(テーブル):マクロを拡張して、より柔軟性を増したもの
  3. Options(オプション):ステートフルのときのセッションタイムアウトやセッション保持の最大数などを設定します
  4. Traffic Normalization(通信の正規化):パケットのフラグメントの調整など。mssの調整も可能
  5. Queueing(キューイング):QoS(通信の優先付け)の設定をしたり、帯域幅の調整ができます
  6. Translation(変換):NATやポートリダイレクトの設定
  7. PacketFiltering(パケットフィルタ):パケットのフィルタリング
IPFilterではTranslationとPacketFilteringにおまけ程度に他の機能が付いていました。
私は正直なところQueueingの機能に驚いています。
でも、設定難しそうです。

3−1.Macros

よく使う値を設定しておきます。
外向けのインタフェースと内向けのインタフェースを設定しました。
外向けのインタフェースはmpdのPPPoEインタフェースを想定してます。
ext_if = "ng0" 
int_if = "fxp0"
使うときは$ext_ifとか$int_ifなどと指定します。
って、逆に長くなってますね。

こうすることによるメリットは、ネットワークカードを変更したときに、1カ所だけ変更すればよくなるところでしょうか。
viとかで一括で変換することもできるんですが、それは言わないお約束。

3−2.Tables

アドレスリストです。
constとpersistのフラグをセットすることが可能です。
constは定数を、persistはダイナミックにアドレステーブルを変更することができます。
IPアドレス(ネットワークアドレス)しか設定することはできないようです。
table <private> const { 10/8, 172.16/12, 192.168/16 }
table <badhost> persist
これで<private>にアドレステーブルが設定されます。使いたいときは<private>で指定します。
persistを設定した<badhost>テーブルはpfctlコマンドで管理します。
# pfctl -t badhost -Tadd 10.1.1.1     # テーブルへ追加
# pfctl -t badhost -Tdelete 10.1.1.1  # テーブルから削除
# pfctl -t badhost -Tshow             # テーブルの一覧表示
テーブルには外部ファイルを指定することも可能なようです。
table <spam> persist file "/etc/spamlist" "/etc/openrelays"
ファイルには1行ごとにIPアドレスを書いていきます。

3−3.Options

tcp/udpのセッション情報をもとに戻りのパケットの通過を許可させるステートフルな動作を設定した場合、セッションを保持するタイムアウトや保持数を設定することができます。
ステートフルとステートレスについては以前書いたドキュメントを読んでください。
ステートフルインスペクションとかコネクションプーリングとか言うようですが、私の嫌いな緑色のルータのエンジニアが好んで使う言葉なので、できれば使いたくないんですよ。(偏見だなぁ)
たぶんpfの設定ができるようになればルータのエンジニアよりも応用の効く知識が得られると思いますよ。

だいぶ余談でしたが。
# 分割されたパケットを再構成するときの分割パケットの待ち時間
set timeout { interval 10, frag 30 }
# tcp関連のタイムアウト設定
set timeout { tcp.first 120, tcp.opening 30, tcp.established 86400 }
# セッション終了後におけるステータス保持時間
set timeout { tcp.closing 900, tcp.finwait 45, tcp.closed 90 }
# udp関連のタイムアウト設定
set timeout { udp.first 60, udp.single 30, udp.multiple 60 }
# icmpのタイムアウト設定
set timeout { icmp.first 20, icmp.error 10 }
# その他のレイヤ4プロトコルのタイムアウト
set timeout { other.first 60, other.single 30, other.multiple 60 }
# adaptiveに関する設定
set timeout { adaptive.start 0, adaptive.end 0 }
# ステータスの保持制限数
set limit { states 10000, frags 5000 }
# pfctl -s infoで表示するログインタフェース
set loginterface ng0
# ブロック時の応答(捨てるかRSTを返すか)
set block-policy drop
# 以下、その他の設定
set optimization normal
set state-policy if-bound
set require-order yes
set fingerprints "/etc/pf.os"
よくわからない設定についても多分に含んでいます。
追々解決していくことにして、とりあえずこの設定で問題ないかと思います。

3−4.Traffic Normalization

トラフィックの正規化ということで、パケットをいじることができます。
主にパケットのフラグメントに関する設定です。
TCP/IPに詳しくないとなんのことだかさっぱりです。
no-df Don't Fragmentビットを無効にできます。
dfフラグが立っていると、MTUが小さい値しか受け付けられないルータでパケットが落ちてしまいます。
min-ttl TTL値の最小値を変更します。
小さすぎるTTLを設定した場合、経路が多いと到達できない場合があります。
tracerouteのパケットに適用すると問題が起きるかもしれません。
max-mss mssの最大値を設定します。
mpdではmssの調節をしないので、pfではここで1414と設定しましょう。
randam-id IP(InternetProtocol)のIdentificationフィールドをランダムな値に書き換えます。
もともとランダムな値が設定されるようです。
詳しくはRFCを読んだ方がよさそうです。
fragment reassemble フラグメントされたパケットを再構成します。
no-dfしてmax-mssをいじると経路上でパケットがフラグメントされる可能性があります。
フラグメントされたパケットはホスト上で再構成することも可能ですが、ホストのパフォーマンスが気
になる場合はpfに再構成してもらうとホストが楽になります。
フラグメントされたパケットはFragmentOffsetフィールドになんか書き込まれているので、これで
判断することができます。
fragment crop reassembleと同様に再構成するのですが、より簡易な機能です。
NATが動作する環境では使えないので、そのときはreassembleを使いましょう。
reassemble tcp tcpの再構成をします。TTLやタイムスタンプをいじることができるようですが、使い方はよくわかりません。
FreeBSDではmpdのmssの問題があるのでまずはng0のアウトプットに対してmax-mssをセットしておいて、入ってくるパケットにfregment reassembleを指定すると幸せかもしれません。
scrub in on $ext_if all fragment reassemble
scrub out on $ext_if all max-mss 1414

3−5.Queueing

帯域制御やQoSの設定ができます。
あらためて説明するまでもないかもしれませんが、QoSではプロトコルやアドレスごとに割り当てる帯域を変えることができます。
最近だと、IP電話が普及していますが、電話は遅延にシビアなので、IP電話を優先して帯域を多く使わせるとか。
反対にメールやftpとかテキストコンテンツについては優先順位下げるといったこともできます。

QueueingはALTQを組み込まないと動かないようです。

今回は使う予定がないので割愛しますが、非常に興味深いものなので、あとで挑戦してみたいです。

3−6.Translation

IPFilterではipnatとして別れていた機能ですが、pfではひとつにまとまっています。
文法もやや変更になっています。

ipnatではmap,bimap,rdrの3つの変換ルールでしたが、pfではnat,binat,rdrです。
mapがnatに変わったと思えばいいようです。

ここで設定する項目は、
  1. NAPT(IPマスカレード)の設定
  2. ポートフォワーディング
をやってみることにします。
まずはNAPT。
サンプルにはこう書いてあります。
ext_if = "ng0"
nat on $ext_if inet from ! ($ext_if) to any -> ($ext_if)
($ext_if)のところには本来IPアドレスが入るところなので、インタフェースを括弧で括るとIPアドレスが出てくるようです。
外向けインタフェースに付いているIPアドレス以外がソースになっているパケットはすべて変換というルールなのでしょうが少々横着なような気がしますね。
アドレス変換はルーティングされた後の処理なので、確かにこれでも動くかもしれませんけど。
万が一ルーティングテーブルが攻撃されたとしても対応できるように修正してみましょうか。
ext_if = "ng0"
table <private> const { 10/8, 172.16/12, 192.168/16 }
nat on $ext_if inet from 192.168.0.0/24 to ! <private> -> ($ext_if)
こんなところでどうでしょうか。
実際は$ext_ifのIPアドレスが固定なので実アドレス書いちゃいますけど。

リダイレクトのサンプルはこうです。
ext_if = "ng0"
rdr on $ext_if inet proto tcp from any to any port 80 -> 127.0.0.1 port 8080
httpをlocalhostの80番に転送ってやつですか。
これももう少し絞ってあげるとハッピーだと思います。
それと、localhostはあまり現実的じゃないので適当に置き換え。
ext_if = "ng0"
rdr on $ext_if inet proto tcp from any to ($ext_if) port 80 -> 192.168.0.64 port 80
ローカルにある192.168.0.64というWebサーバに全部転送する仕掛けです。

このほかに、binatの使えますが、今回は使いそうにないので割愛します。

3−7.PacketFiltering

ようやくやってきました。
パケットフィルタのルールはさまざまです。こだわりはじめたらキリがないので、思いついたところをザクザク設定していきます。
私はファイアウォールの専門家じゃないのでいろいろと問題もあるかもしれませんけど。
基本的にはブロックしたパケットはログに出しています。
#### Macros ####
# WANインタフェースを定義
ext_if = "ng0"
ext_addr = "XXX.XXX.XXX.XXX"
# LANインタフェースを定義
int_if = "fxp0"
int_addr = "192.168.0.1"

#### Tables ####
# ルータのローカルアドレスを定義
table <local> const {127.0.0.1, 192.168.0.1, XXX.XXX.XXX.XXX }

# プライベートアドレス群を定義
table <private> const { 10/8, 172.16/12, 192.168/16 }

# RFC3330からプライベートアドレスを除いた特殊用途アドレスを定義
# 一部のMulticastアドレスを使う予定がある場合は注意すること
# 14/8や24/8はケーブルテレビなど使われている可能性もあるので注意
table <special> const { 0/8, 14/8, 24/8, 39/8, 127/8, 128.0/16, 169.254/16, 192.0.0/24, \
192.0.2/24, 192.88.99/24, 198.18/15, 223.255.255/24, 224/4, 240/4 }

#### Filter ####
# まずはデフォルトで拒否
block in log all
block out log all


####
### WANから入ってくるパケットの処理 ###

# 外から入ってくるアドレス偽装パケットの処理
block in log quick on $ext_if from { <local> <private> <special> } to any 

# TCPの制御
## 外部からのhttp,DNS(tcp),SMTPを許可
pass in on $ext_if proto tcp from any to $ext_addr port {smtp domain http} flags S/SA keep state

## Windows95や98から直接のSMTPを拒否する
block in log quick on $ext_if proto tcp from any os {"Windows 95", "Windows 98"} to $ext_addr port smtp 

## IDENTにRSTを返す
block return-rst in quick on $ext_if proto tcp from any to any port 113

## それ以外のTCPは捨てる
block in log quick on $ext_if proto tcp all

# UDPの制御
## DNS(udp)を許可
pass in quick on $ext_if proto udp from any to $ext_addr port domain keep state

## それ以外は捨てる
block in log quick on $ext_if proto udp all

# ICMPの制御
## tracerouteを開ける
pass in quick on $ext_if inet proto icmp all icmp-type 3
pass in quick on $ext_if inet proto icmp all icmp-type 11

## 他のicmpは捨てる
block in log quick on $ext_if proto icmp all

####
### WANへ出ていくパケットの処理###

# 変なパケットは出さない
block out quick on $ext_if from any to { <local> <private> <special> }

# smbとnetbiosが外に漏れるのを防ぐ
block out quick on $ext_if proto {tcp udp} from any to any port 135
block out quick on $ext_if proto {tcp udp} from any to any port 137:139
block out quick on $ext_if proto {tcp udp} from any to any port 445

# SQL Slamerが気になる人はこちらも
block out quick on $ext_if proto {tcp udp} from any to any port 1433:1434

# 中からのセッション開始は許可
pass out quick on $ext_if proto tcp all modulate state
pass out quick on $ext_if proto udp all keep state

# ICMP関連
## 外に出ていくping
pass out quick on $ext_if inet proto icmp all icmp-type 8 code 0 keep state
## 他のicmpは外に出さない
block out log quick on $ext_if proto icmp all

####
### LAN内のパケット処理 ###
pass in quick on $int_if all
pass out quick on $int_if all

####
### loopback ###
pass in quick on lo0 all
pass out quick on lo0 all
てな感じで。
ステートフルにしたので、IPFilterに比べてだいぶスッキリしました。
ICMPの定義をするときは"inet"をつけるのを忘れないようにしましょう。
IPv4とv6ではICMPの実装が違うので、タイプを指定した場合は必須項目になっているようです。
以上で設定は終わりです。
上で設定したタイムアウトやnatのルールを追加してひとつのファイルにしておきましょう。
試しにまとめたものをここにおいておきます。
うちで実際に動いているものとは違うので、動かなくても文句は言わないでください。
間違いの指摘は大歓迎です。

4.ツール

うまく動かないときに役立つツールを紹介します。

4−1.pfctl

何を置いてもpfctlです。
フィルタルールを確認したり、natを確認したり、ステータスをチェックしたり。
今回の実験で比較的よく使ったものをご紹介します
(1)ファイルからルールを適用
# pfctl -f /etc/pf.rules
ルールを再適用するときなどに使えます。
再起動することなくダイナミックにルールが変更できるのでかなり使えます。
(2)フィルタルールの確認
# pfctl -s rules
ルールが適用されているかを確認します。
Macrosも展開されるので目視で設定に間違いがないことを確認する手法としては最適です。
後に出てきますが、ルール番号を確認できないのが玉に瑕です。
IPFilterではルール番号も確認できていました。
(3)NATルールの確認
# pfctl -s nat
NATルールが適用されているかを確認することができます。
(4)ステータスの確認
# pfctl -s state
ステートフルなので、保持しているセッションの状態を確認することができます。
(5)TABLESルールのチェック(再掲)
# pfctl -t private -Ts
ルールの確認ではテーブルまでは展開されないので、中身はこれでチェックします。

4−2.tcpdump

設定して動かなかったらまずはログをとりましょう。
pfのログはpcapというバイナリ形式で提供されます。
見るときはtcpdumpで見ます。
# tcpdump -n -e -ttt -r /var/log/pflog
インタフェースを直接監視して、流れるパケットをリアルタイムにチェックする方法もあります。
# tcpdump -n -e -ttt -i pflog0
こんな感じで表示されると思います。
2. 943721 rule 11/0(match): block in on ng0: xxx.xxx.xxx.xxx.1024 > yyy.yyy.yyy.yyy.445: S 1954608882:1954608882(0) win 65044 <mss 1414,nop,nop,sackOK>
[rule 11/0]というところが、ルールの番号なのですが、これを確認する方法がないのが困ったものです。
他のパラメータについては、pflogdのマニュアルとかtcpdumpのマニュアルを読みましょう。
思った以上にいろんなことができるはずです。
いろんなことが記録されているのですが、その分ログの量も増大してしまいます。
設定するまえにログ取得ポリシーを計画して、妥当性のある設定をしましょう。
ちなみに、pcapなので、etherealで読みとれるかなと思ったのですが、無理なようでした。
最新版のライブラリを入れれば動いたかもしれません。
それと、Rubyのモジュールでruby-pcapなどというものが存在するらしいので、これで遊べるかもしれません。

参考

RFC3330 Special-Use IPv4 Addresses
FreeBSDハンドブック(英語版) The OpenBSD Packet Filter (PF) and ALTQ
各種マニュアル [pf.conf(5),pfctl(8),pflogd(8),tcpdump(1),altq(4)] FreeBSD Hypertext Man Pages


もどる