月別アーカイブ: 2022年10月

メールファイルのOS上のタイムスタンプをメールヘッダ内のタイムスタンプに変更するサンプルプログラム

先日書いたiPhoneのメールアプリのタイムスタンプが狂う問題を修正を目的とした、Yuaiho作成のサンプルプログラム(Perlスクリプト)を配布します。サンプルを名乗ってますが、一応Yuaihoが実際に使っているスクリプトです。

配布用にコメントやメッセージ出力は割と手直しはしていますが、アルゴリズム的にはそのままです。


配布先

※免責事項は本記事の下部にあります。
ダウンロード from yuaiho.com
ダウンロード後、拡張子をplなどに変更して、実行権限を与えてください。

年月日時分秒からEpoch秒を求めているため、Timelocalモジュールが必要です。

メールファイルのファイルリスト一覧を標準入力で送り込むと、メールファイルを1つづつ開いて読み込み、Receivedヘッダ、もしくはDateヘッダからタイムスタンプを取得します。


使い方

使い方は下記の一番上の行のように、タイムスタンプが狂ったメールファイルが存在するメールボックスのcurディレクトリから呼ばれることを想定しています。

$ find ./ -type f | /tmp/dateperse.pl --debug --commit
$ ls -1 | /tmp/dateperse.pl --commit --noinfo
$ cat <filelist> | /tmp/dateperse.pl --debug --commit
$ cat <filelist(Abusolute file path)> | /tmp/dateperse.pl --absolute

引数に--commitをつけて実行すると、本当にファイルシステム上のタイムスタンプを書き換えます。--ommit引数がない場合は、タイムスタンプ書き換えは行わず、&#45;&#45;commitで実行する際のコマンドがテキストで表示されます。

引数に--debugをつけるとDEBUG:メッセージが出力され、より詳細な情報が出ます。ただ、私がデバッグ用に使用していたものそのままなので内容は不親切です。

引数に--noinfoを追加するとINFO:メッセージが出ません。--noinfoを追加してもエラー(ERROR:)が出るとエラーメッセージが出力されます。--commitと--noinfoを組み合わせると、エラー発生時以外は何も表示されなくなります。

標準入力から送られてくるファイルリストが絶対パスの場合は引数に--absoluteをつけてください。送られてくるファイルリストは絶対パスとして処理します。送られてくるパスをそのまま開きます。

引数に--absoluteが無い場合は標準入力から送られてくるファイルリストを相対パスとみなします。標準入力から送られて来たファイルリストの頭に、スクリプト実行時のシェルののカレントディレクトリを付け足してからファイルを開きます。

引数に-h --usage --helpを入れるとAboutメッセージと使い方が表示されます。

ファイル内に処理すべきタイムスタンプを含むヘッダが見当たらないファイルは--commitを入れて実行しても時刻変更の処理はされません。

下記は引数なしの出力例です。緑が--commit時に実行されるコマンドです。これがファイル数分出力されます。

$ find ./ -type f | /path_to_app/dateperse.pl
-----
INFO: Executing file "/path_to_maildir/cur/./1486390265.M692487P75574.mail.yuaiho.net,S=1962,W=2000:2,S"
INFO: Header read: 2003-08-02 12:58:24+0900 -> Final destinaiton time: 2003-08-02T12:58:24
/usr/bin/touch -d 2003-08-02T12:58:24 "/path_to_maildir/cur/./1486390265.M692487P75574.mail.yuaiho.net,S=1962,W=2000:2,S" >> /dev/null
-----
※ファイル名は一部架空のものです。

動作の前提となる条件

メールは下記を満たしていることを前提に作っています。下記を満たさないファイルを食わせると、不正確なタイムスタンプが生成されたり、プログラムが落ちたりします。微妙に文字色が違う部分の処理はかなり雑に書かれています。

  • タイムスタンプの年表記は西暦4桁表記であること。
  • タイムスタンプの月表記はJan/Feb/Marのように大文字開始の半角英字3文字であること。
  • タイムスタンプの時刻はhh:mm:ddの0フィル有り24時間制表記であること。
  • Date:はヘッダ内に必ず存在し、そこにはタイムスタンプが必ず書かれていること。
  • Received:ヘッダのセミコロンの後には必ずタイムスタンプが存在すること。
  • 時差表記は+hhmmまたは-hhmmで、hhは0〜19でmmは00から15分刻みであること。
  • タイムスタンプは0以上のEpoch秒の範囲で、かつ実在する有効な日付・時刻で、整数表記であること。
    (つまり2000年08月32日とか、25:05:00とか、13:75:00とか、12:00:00.000はダメです。)
  • ヘッダ内に空行が無いこと。
  • ヘッダと本文の区切りは必ず空行であること。

MUA(Mail User Agent/メーラ)を変更しても問題なく読めるメールであれば、基本的に上記を満たしていると思います。たぶん。


主な仕様

使用可能なヘッダ

採用するタイムスタンプは「Received:ヘッダのみ」「Dateヘッダのみ」「どちらか先に見つかった方」の3つを選べます。「$g_pref_mode_header」で設定します。デフォルトでは「どちらか先に見つかった方」です。

Received:ヘッダのみを参照する場合、参照範囲をヘッダ内に絞る設定があります。絞らないと、Received:ヘッダがヘッダ全体を探しても見つからない場合、本文や添付中に書かれたものを拾う可能性があります。これは「$g_pref_mode_parse_headeronly」で設定できます。デフォルトでは絞っています。

Received:ヘッダは1番上にあるもののみを自動的に拾います。そのファイル内にReceived:で始まる行が1つでもあったか(マッチしたか)を検査してるので、最初のReceived:ヘッダより行頭側にタイムスタンプがあっても反応しません。

タイムスタンプの拾い方はかなり手抜きをしてるので注意が必要です。例えば、Feb 31とか26:58:57とかも拾ってしまいます。

あり得ないタイムスタンプを検知して除外するような機能は実装していません。あり得ないタイムスタンプを拾ってしまうとプログラムがエラーで止まります。

ファイル内に利用可能なタイムスタンプが見つからない場合、そのファイルは何も処理されません。処理されない代わりに「ERROR: File “ファイル名” has no valid hedear.」と表示されて次のファイルを処理します。


時差補正

どちらのヘッダを利用していても、最終的なメールサーバの設置場所の時差を勘案したものとなるよう時差補正をかける処理を入れています。

例えばReceived:なりDate:なりのタイムスタンプに00:00:00+0000(UTC)と書かれているメールは、時刻やタイムゾーン設定が正確なら日本時間では09:00:00に受信してサーバのディスクに書き込まれたことになります。

タイムスタンプの時分秒をそのまま信じると00:00:00ですが、UTCの00:00:00であって日本時間の00:00:00なわけではないので、時差を考慮してサーバ設置場所の正しい時刻(ローカルタイム)に補正する必要があります。

このプログラムは読み込んだタイムスタンプを一旦UTCに戻し、そこからプログラム内で設定した時差を追加することで、サーバ設置場所のローカルクロックの示す時刻になるような処理を入れています。この修正用の時差は時差は「$g_pref_tz_sec」で秒単位で設定できます。

私のサーバはUTC+0900(日本)、にありますのでデフォルトは+0900に補正する設定になってます。

この補正値は任意の時間に設定出来ますが、タイムスタンプの日付や時刻によって補正値を変えるような処理には対応してません。つまりDST(夏時間)には非対応です。DST実施国にお住まいの方は適宜改造してくださいです。

+0900 (JST)のように後ろにタイムゾーンや標準時名とかが付いてる場合もありますが、各国の標準時はたまに変わるのでこのプログラムではカッコ内のタイムゾーン名や標準時名は無視しています。というか、たまに付与されてないし。

ちなみに日本標準時であるJSTは1951年から変わってませんが、例えばここ20年のUTC+9だけ見ても2015年に北朝鮮やロシア(イルクーツク)、モンゴルに異動にあり、UTCとの時差が変更になってます。

余談ですが、古いメールだとGMT+0900とかあったと思います。UTCとGMTは厳密な定義の上では異なる存在ですが、時差を示す場合においては同一のものと考えて差し支えありません。


免責事項

サンプルプログラムの利用・改造・転載は公序良俗に反しない限り自由です。事前・事後の連絡も不要ですが、必ず利用者の自己責任で行なってください。再配布の際はオリジナルのAuther名を残していただけると作者としては嬉しいです。

意図的に有害コードを含むようなことはしておりませんが、意図しない不具合を内包している可能性があります。

Yuaihoが実際に使ったプログラムですが、一般の使用に耐えうるような充分なテストがされてるわけではありません。例外処理などももほとんどされていないか、されていても激甘です。そのため実環境に適用される際はよく検証してからご自身の責任で適用してください。間違いや事故があっても責任は取れません

コードが汚い系の批判は、頂いてもたぶん直さないと思います。

サイドチャネル的に作ったので参考文献は特にありません。

iPhoneのメールアプリのタイムスタンプが狂う問題

iPhoneのメールで表示されるタイムスタンプはDateヘッダではなくエンベロープの到着日

理由は知りませんが、iPhoneのApple純正のメールアプリ(※)に表示されるタイムスタンプは、IMAP4を使ってる場合、メールヘッダのDate:ヘッダの中身では無く所謂エンベロープの到着日を表示しているようです。

※インストール時点で入っているApple公式の青いアイコンのメールアプリのことです。

エンベロープの到着日というのはIMAP4のINTERNALDATEに相当するタイムスタンプです。IMAPサーバにloginコマンドでログイン後、SELECTコマンドでメールボックスを選択して「fetch <メール番号> INTERNALDATE」みたいなコマンドを送りつけると返ってくるタイムスタンプです。

a FETCH 1 INTERNALDATE
* 1 FETCH (INTERNALDATE "18-Mar-2010 01:36:12 +0900")
a OK Fetch completed (0.013 + 0.000 + 0.012 secs).
a FETCH 1 SAVEDATE
* 1 FETCH (SAVEDATE "21-Oct-2022 05:38:12 +0900")
a OK Fetch completed (0.031 + 0.000 + 0.030 secs).

このエンベロープのタイムスタンプは私のメールサーバ(※)の場合、下記に示すようにファイルシステム上で、上記のメールに該当するメールファイルの属性値であるBirthかModified属性のいずれかのタイムスタンプがそのまま入っていました(※※)。ちなみにSAVEDATEコマンドの方はChange属性が入っているみたいです。

$ stat -x 1268843772.*****.mail.yuaiho.net:2,S
  File: "1268843772.*****.mail.yuaiho.net:2,S"
  Size: 654          FileType: Regular File
  Mode: (0600/-rw-------)         Uid: (yuaiho/515)  Gid: (yuaiho/515)
Device: 0,112   Inode: 19423***    Links: 1
Access: Fri Oct 21 05:07:10 2022
Modify: Thu Mar 18 01:36:12 2010
Change: Fri Oct 21 05:38:12 2022
 Birth: Thu Mar 18 01:36:12 2010
※ファイル名の一部(FQDN部分)やInode番号、UID/GIDは架空のものですが、それ以外はオリジナルのデータです。タイムスタンプ変更もこのファイルに関しては変更していません。

※FreeBSD13.1+Postfix3.7.3+Dovecot2.3.19.1。投稿日時点の安定版の最新Versionです。互換モードは無効化。
※※BirthかModified属性のいずれか
: BirthとModifiedのどっちを採用してるかは不明です。freebsd-ufs上ではModifiedもBirthも同じタイムスタンプになるため、どちらを見てるのかわかりません。

まぁ要するにMTA(Mail Transfer Agent)やMRA(Mail Retrieval Agent)の独自DBだったりMaildirだったりのどこかに、エンベロープのタイムスタンプが書かれているファイルが別途存在する、というわけでは無いと言うことです。

他のMTAやMRAの組み合わせまでは存じませんが、少なくともPostfix+Dovecotでは前述の通りです。

MRAはMUAからIMAPコマンドでINTERNALDATE相当の問い合わせを受けると、それに該当するメールファイルのファイルシステムのModified or BirthのタイムスタンプをOSに問い合わせて、その結果をエンベロープのタイムスタンプと(いうことに)して送って来てるだけと言うことです。

iPhoneの純正のメールアプリはこのエンベロープのタイムスタンプ、つまりメールファイルがサーバのファイルシステムに書き込まれた時点のタイムスタンプメール受信日として取り扱っています。


iPhoneのメールのタイムスタンプがおかしい理由

と、いうわけで、ファイルシステム上のメールファイルのModified属性のタイムスタンプが狂うと、iPhone上のメールアプリの受信日もその狂ったタイムスタンプになります。

他のメールソフトで見るとタイムスタンプがそれっぽいものがちゃんと表示されているのに、同じメールをiPhoneで見るとタイムスタンプがおかしい場合、たぶんこれが原因です。他のメールソフトはDate:ヘッダをParseして表示していたりするんですが、iPhoneのメールアプリはDate:ヘッダを使って無いっぽいです。

メールファイルのタイムスタンプが狂う起因ですが、例えばメールサーバのシェルなどでメールファイルを直接操作するときに、tar cfpやcp -pとかの属性保存オプションの使用を忘れて雑にcpやtarをしたりすると、コピー先でメールファイルのタイムスタンプがファイル操作実行時点のものに書き変わってしまいます。

エンベロープのタイムスタンプの実態はさっき説明したように、実態はメールファイルの置いてあるディスク(ファイルシステム)に記録されたModified属性です。

そのため、メールファイルのModifiedのタイムスタンプが変更されるような操作をすると、本当のエンベロープのタイムスタンプが虚空に消えると言うわけです。

そして、そんな正しいModifiedのデータが虚空に消え、”管理者が雑にファイルを操作した日”とかいう利用者からするとわけのわからんタイムスタンプが入ったメールファイルを含むメールボックスをiPhoneのメールアプリから見ると、その謎の日付が表示されることになります。


修正方法

さて、リストアする方法ですが、きちんとタイムスタンプが保存されているバックアップが無いなら基本的にはありません。

しかし、メールヘッダ内にあるタイムスタンプが信用できるか、(信用できるかどうかに関わらず)信用することにするなら、メールヘッダに入ってるタイムスタンプを元にしてModified属性の日付時刻に修正することは出来ます。

使用出来るタイムスタンプが入ったヘッダは2つあり、Received:ヘッダとDate:ヘッダです。

具体的には、Received:ヘッダのうち最も行頭に近いところにあるReceived:ヘッダの末尾に付いてるタイムスタンプと、Date:ヘッダに含まれるタイムスタンプのいずれかです。1つのReceived:ヘッダが複数行渡って書かれている場合、その1つのReceived:ヘッダ全体の末尾に書かれたタイムスタンプを使用します。

後述しますが、どちらのヘッダを使っても信頼性はケースバイケースです。特に昔の牧歌的な時代のタイムスタンプはReceived:ヘッダであっても正直怪しいです。

そしてReceived:ヘッダを使う方法は1つだけ制限があって、MUA(Mail User Agent/メーラ)が送信時に直接IMAPフォルダに書き込んだメール、つまりSentフォルダや送信済フォルダの中身には使用できません。これらのメールはSMTPサーバを経由しておらず、Received:ヘッダがヘッダに付与されていないので、Date:ヘッダしか使用できません。

Received:ヘッダ

もっとも行頭に近い部分にあるReceived:ヘッダは、メールリレーの終着点である自分のサーバが付けるので、送信者側から詐称が難しく、適切な時刻管理がされていればかなり正確なのが期待できます。この「適切な時刻管理がされていれば」と言うのがミソで、本当に正確かは終着点のサーバの管理次第です。

自営SMTPサーバを持ってて、そのSMTPがメールリレーの終点であれば1番上のReceived:ヘッダは自分のサーバが付与したヘッダになります。

エンベロープのタイムスタンプは終点のメールサーバにファイルが書き込まれた時刻です。MTA内部でメールスプールがふん詰まったりしない限り、通常はSMTPでメールを受信したら即座にMaildirのあるディスクに書き込まれますので、ヘッダ上の自分のサーバへの到着日をエンベロープの到着日とみなすのは一定の合理性があります。

まぁ、実際のところファイル属性値はミリ秒まで書かれているらしいんでが、SMTPのヘッダは1秒未満(=ミリ秒)の情報が無いのでミリ秒の情報はヘッダから復元出来ません。

そのため時刻管理が正しく行われているサーバのReceivedヘッダを使って修正したとしても実は若干不正確なんですが、それでも±1秒レベルぐらいの精度では合うので、秒以前に年月日すら合ってないようなわけわからんタイムスタンプに比べれば全然マシです。

仮に終点が自分のサーバでなくても、読み込んで時差補正さえかければまぁそれっぽい時間にはなります。

ただ時刻管理が杜撰なメールサーバでは、下手するとDate:ヘッダより不正確です。私の場合、過去のメール調べてみると2000年代前半のものはDate:ヘッダや先頭のReceived:ヘッダとその1つ前のReceived:ヘッダに乖離があるものが結構ありました。

まぁ2000年代前半(5号機時代)の私のサーバは時刻同期(NTP)の管理が杜撰そのものでした。当時はメインで使ってるマシンぐらいしかまとも時刻同期してませんでしたし、合わせ方もNTPではなくSNTPで気が向いた時にたまに合わせるぐらいでした(※)。

※今のモダンなOSは特に設定してなくても明確に止めない限り、w32tmなりntpdなりchronydなりsystem-timesyncdなりのNTPが動きます。

Real Time Clock Due to oddities of the Macintosh hardware interrupt priority scheme, NetBSD/mac68k keeps very poor time. Under a high interrupt load (e.g. SCSI or serial port activity), a machine can lose several minutes per hour. A consequence of this problem is that attempting to run ntpd is generally rather pointless.

https://cdn.netbsd.org/pub/NetBSD/NetBSD-9.3/mac68k/INSTALL.html#Known%20hardware%20issues%20with%20this%20release

ちなみに私の場合、杜撰な時刻同期管理に加えて、5号機で使ってたNetBSD/mac68kは上記のようにIO処理が重いと時計が狂う仕様らしいので正確な要素が全く見当たりませんね。実際、SNTP (ntpdate)で合わせると毎回1〜2時間ぐらいズレてて修正かかってました。

しっかりしたISPなら大丈夫だと思いますが、時刻管理が杜撰なサーバの古いメールだとReceivedヘッダの時刻は怪しいかもしれません。

たた、終点が自分のサーバなら、仮にそのサーバの時刻同期が杜撰で多少ズレていたとしても、1番上のReceived:ヘッダは「サーバに到着した時のサーバの時刻」が書かれてます。その時にサーバはその時刻だったのは間違いはないので、目を瞑れる程度のズレならスルーするしてしまうのがいいと思います。

前述したように、Received:ヘッダを採用する方式の弱点は、MUAから直接IMAPサーバに書き込まれたメール、つまり「Sentフォルダ」や「送信済メール」の中身には適用出来ません

これらのメールは基本的にReceivedヘッダが1つも無いことが多いのでReceived:ヘッダの時刻を適用する方法では修正出来ません。これらのメールに対してはDate:ヘッダを使う必要があります。

Date:ヘッダ

Date:ヘッダはReceived:ヘッダと異なり、メールファイル内で付いていないということはまず無いというメリットがあります。

ただ、Date:ヘッダは送信者の自己申告も容易に出来ます。つまり詐称しやすいヘッダです。よって、必ずしも信用できる値が入ってるわけではありません。これが最大のデメリットです。

ただ、これは私の主観にはなりますが、明らかにおかしいタイムスタンプが付いているケースはゼロではありませんが、実際のところはほとんど見かけません(※)。

また、Date:ヘッダを受信時刻として扱うMUAの実装もあるので、「管理者がファイル操作した時刻」というメールの中身と無関係なタイムスタンプよりは、Dateヘッダのタイムスタンプを使う方がまだマシとは言えます。

※たぶんですが、Date:ヘッダは明らかに詐称してるものがあるのでiPhoneは Date:ヘッダでなくエンベロープを見てるのだと思います。

基本はReceived:ヘッダから時刻を引っ張ってきて、なんらかの理由でReceived:ヘッダのタイムスタンプが明らかにおかしいか、SentフォルダなどでReceived:ヘッダがない場合はDateから復旧すれば良いと思われます。


修正の流れ

具体的な流れはメールファイルのヘッダから読み込んだReceived:なりDate:なりにあるタイムスタンプをParseして、整形して、時差を修正して、touch -dコマンドの引数として利用可能なYYYY-MM-DDThh:mm:ss形式(例:2000-05-15T21:15:09)にします。Yuaihoの場合はtouch -dの引数に入れる時刻はローカルタイム、つまり時差修整を含む日本時間を指定しています。

ちなみに、Date:ヘッダのタイムスタンプはYuaihoの持ってるメールだけでも下記のように割とバラエティがあってParseするが割と面倒でした。もし自分でコードを書くのであればReceived:ヘッダを使う方が楽だと思います。Receivedヘッダの方がバラエティが少ないです。

1. Fri, 28 Apr 2006 15:44:57 +0900 (JST) ※PostfixのReceivedヘッダ
2. Fri, 18 Jul 2003 17:58:36 +0900 ※よくわからんMTAのReceivedヘッダ
3. Thu, 18 Sep 2003 08:00:00 +0900 (JST) ※Dateヘッダ/Receivedヘッダと同じ
4. 18 Jan 2006 15:24:58 +0900 ※Dateヘッダ/曜日がないパターン
5. Wed, 5 Apr 2017 18:02:17 +0900 (JST) ※Dateヘッダ/日の0フィル無し(こっちが一般的)
6. Mon, 03 Mar 2008 12:01:34 +0900 (JST) ※Dateヘッダ/日の0フィル有り
7. Wed, 25 Jan 2006 01:52:30 +0900 ※Dateヘッダ/タイムゾーン名なし

上記は私の古いメールを漁って、タイムスタンプがどんな表記をしているのか確かめた実物です。上記の1と2はReceived:ヘッダのパターンで、3〜7はDate:ヘッダのパターンです。

具体的には1がPostfix、2が自宅サーバで受信していないメールから持ってきた詳細のわからないMTAです。3〜7はDate:ヘッダから目に付くものを持ってきたものです。

Received:ヘッダはJSTなどのTZ名の有無ぐらいしか差がありませんが、Date:ヘッダは曜日がなかったり、TZ名がなかったり、日にゼロフィルがあるなど意外とバリーションがあるのがわかります。

上記をうまくParseして、YYYY-MM-DDThh:mm:ss形式のタイムスタンプ文字列が出来たら、touchコマンドでメールファイルのModifiedを下記のように書き換えます。コマンドを実行するとメールファイルのファイルシステム上のタイムスタンプが変更されます。

[yuaiho@yuaiho.net cur]$ touch -d 2000-05-15T21:15:09 <メールファイル>

こうするとエンベロープのタイムスタンプ、つまりINTERNAL DATEをメールヘッダから読み込んだタイムスタンプに修正できます。

ファイルシステムのタイムスタンプを書き換え終わったら、シェルで書き換えたcurやnewフォルダの上位にあるのdovecotのキャッシュファイルを消します。具体的には dovecot-uidlist dovecot.index.log dovecot-keywords dovecot.index.cache の4ファイルです。curやnewなどのディレクトリを消さないようにしてください。削除後のDovecotの再起動は不要です。

-rw-------   1 yuaiho  yuaiho     15 Jan 14  2021 dovecot-keywords
-rw-------   1 yuaiho  yuaiho   2109 Sep 26 12:11 dovecot-uidlist
-rw-------   1 yuaiho  yuaiho  13332 Sep 29 11:56 dovecot.index.cache
-rw-------   1 yuaiho  yuaiho   3484 Sep 29 11:56 dovecot.index.log

ここまで終わったら、iPhone上でそのフォルダを読み直せばさっき修整した日付になる筈です。全ファイル読み直すので時間はかかります。

iPhoneのメールのメールボックスのリスト上では修整されたのに、メールを開いた際に出てくる日付が修整されてない場合はiPhoneの「設定」→「メール」→アカウントでそのメールボックスを含むアカウントを消してから、iPhoneを再起動し、アカウントをもう一度登録すると直ります。

アクセス制御リスト(ACL)の勘所

多くのNW機器やFW、OS、サーバソフトウェアにはアクセス制御(機構)が実装されています。

このアクセス制御機構は多くの場合、Allow list(ホワイトリスト)やDeny list(ブラックリスト)あるいはその両方を含むルールリスト、俗にいうACL(Access Control List)をもってて、アクセスして来たクライアントのIPやポートと、ACLに書かれているルールを比較して、OK、NGを判別しています。

1つのACLの実装しか扱ってないと中々気がつかないんですが、実はACLの評価方法は実装毎に異なります

実装毎の具体的な仕様は必要になったらその場で調べればいいんですが、少なくとも「実装ごとに評価方法は違う」と言うことだけは知っておかないと、慣れてない実装でACLを操作した時に嵌ります。

ポイント

新しいシステムのACLを操作するなら下記はの3つは確認した方が良いと思います。特にハマりやすいのが最初の2つです。新しい実装を触る場合は確認しておきましょう。

  • デフォルトの挙動(デフォルトポリシー)
  • マッチ後の挙動(ファーストマッチかラストマッチか)
  • ステートフル型かステートレス型か

デフォルトの挙動(デフォルトポリシー)

デフォルトの挙動とは「ACL内で明示したルールにマッチしなかったもの」に対する扱いです。例えば、下記のようなACLのところに10.0.5.15のIPが来た場合ですね。

192.168.0.0/24 → 不許可
172.16.0.0/12 → 許可

たまに勘違いしてる方がいるんですが、「ACLにルールを書いてない=暗黙のDeny(Block)」とは限りません。そう言う実装もありますが、全てではありません。

例えばLinuxで使われてるiptablesやnftables、FreeBSDのipfやpfのデフォルト設定は「暗黙のAllow (Permit)」です。ブロードバンドルータの簡易的なFirewall(FW)もデフォルトはAllowだったりします。Apache HTTPdなんかは自分で定義したOrderで、デフォルトのポリシーがAllowかDenyかが決まります。

まぁ、こんな感じでデフォルトAllowの実装は意外とあります。

デフォルトの動作についてちゃんと確認しとかないと、Allow List(ホワイトリスト)で作ったつもりのFirewallが、Default Allowの所為でガバガバになってて、全くアクセス制御されてなかった。とか言う事故に繋がります。

ちなみに、nftablesやiptablesに関してはデフォルトポリシーをDENYやREJECTに変更出来ます。BSDのpfやipfilterもたぶんできると思いますが知りません。


評価後の挙動(ファーストマッチかラストマッチか)

評価後の挙動とはマッチしたあとどうなるかと言うことですね。言い換えるとファーストマッチ型かラストマッチ型ということです。

下記の例で192.168.0.1のIPアドレスを持つクライアントがアクセスして来た時にどうなるかを考えます。

192.168.0.0/24 → 不許可
172.16.0.0/12 → 許可
192.168.0.1/32 → 許可
※DefaultはDenyとします。

ここで、「192.168.0.1は192.168.0.0/24に含まれるからブロックされる」と解釈するのは早計です。実装や書き方に依存しますので、もちろん正しいこともありますが常に正解とは限りません。

ルールにマッチするとACLの評価が終わって、その後ろに書かれてるリストは無視する実装だと上記の「192.168.0.1は192.168.0.0/24に含まれるからブロックされる」という解釈は正解です。

しかし、一度マッチしても評価が止まらず、最後までリストを舐めて、最後にマッチした条件(ここでは192.168.0.1/32の部分)で許可される実装もあります。中には行末に明示的に書いおいたDefault denyがマッチして結局ブロックされる場合もあります。

これも実装に依存します。特に主流らしい主流も無い気がしますし、BSD PFやipfilterのように、実装によってはルール毎(行毎)に当該行以降の評価をやめる/続けるを制御出来るものをあります。

例えばBSD PFは下記の上の行のようにquickを入れない場合はマッチしても評価が終わらず次の行が評価されます。下側のようにquickを入れるとルールマッチした時点でルールの評価が終了し、後ろのルール群は評価されません(無視されます)。

pass in log proto tcp from 172.16.0.1 any to any port 25
block in log quick proto tcp from 172.16.0.0/12 any to any port 25

※上記の場合172.16.0.1からSMTPへのアクセスはブロックされます。

許可した筈なのに許可されない、ブロックしたはずなのにブロックされてないというケースは、(その製品等にバクが無いなら)この評価の継続性やデフォルトの挙動をちゃんと把握してないか、認識を誤ってます。


ステートフル型かステートレス型か

ネットワーク通信はほぼ全てが双方向ですので、サーバであれば入ってくる方だけでは無く、出ていく通信も許可する必要があります。

例えばIPが192.168.5.15のメールサーバ(SMTPサーバ)で、Source 172.16.0.1:5555⇨Destination 192.168.5.15:25という通信が発生した際に、逆方向の通信、つまりサーバから見てSource 192.168.5.15:25⇨Destination 172.16.0.1:5555の通信を、Inbound通信のSourceやDestinaionを見て自動的に許可するのがステートフル型、そういういいい感じの許可をステートレス型のFWです。かなり雑な説明ですが。

どちらかを採用してるかは実装によりますし、ステートフル型でもあってもステートフルに許可する設定が入ってないと機能しないことがあります。例えばnftablesだと「ct state established related accept」みたいなワードを入れてあげないステートフルに動作しません。

ステートフル型はコネクションテーブルを管理する都合上、必要なメモリや処理量がステートレスより重くなりがちで、CPUやメモリをそれなりにリッチに必要とします。そのため、CPUやメモリがリーンな組込系のシステムでは、ステートレス型のアクセス制御機構を採用していることがあります。

ステートフル型はテーブルを持ってるので逆方向の通信は通信終了かタイムアウトまでは許可されます。逆に、ステートレス型は明示的に逆方向の通信を許可しないと通信が出来ません。

ステートレス型は所謂許可された通信の戻りの通信かどうかを見ていませんので。そのためステーレス側のACLにDefault Denyみたいなの強力なブロック性能を持つルールをステートフル型のノリで入れると、外に向けて提供していたサービスがブロックされて止まったり、目の前のコンソールが固まることがあるので注意が必要です。

ただ、サーバの場合、設計者や運用者によってはOutboundがガバガバの場合なケースがあります。こう言う場合はあまり気にすることはありませんが、Outbound側もより適切にフィルタリングしたい場合は注意が必要になります。

その他の勘所

そのほかにYuaihoが思うACLのハマりを紹介します。

IP重複の可否

これはnftablesなんかが該当するんですが、ACL内部でIP重複を認めない実装もあります。実装にもよりますが、host単位(/32や/128)だけはなく、レンジとして重複してるのもダメな実装もあり、例えば下記のようなルールはnftコマンドで同一セットに入れることはできません。

192.168.0.0/24
192.168.0.1/32

一方、nftablesの前世代にあたるiptablesはIP・IPレンジの重複を許容しています。

そのため雑に作ってて重複管理なんかしていないiptablesの中身を、何も考えずにnftablesに移植してくるとハマります。

どうなるかというと、リストの後ろ側で重複してるものが弾かれて、nftの再起動に失敗するなどしてハマります。というか、私はハマりました

ブロックポリシー・Denyポリシー

ブロックポリシー/DenyポリシーというのはルールなどによってDenyされた時、その後Denyされた通信をどう始末するかです。

よくあるのはDROP(静かにパケット破棄して無視する)するかREJECT(拒絶した旨の応答を返す)するかですね。

REJECTはIP FWだとTCP RST等を返したり、Webサーバ等のアプリケーションの場合は403(Forbidden)のような拒絶を示す応答を返したりすることです。

DROPとREJECTのどちらかが良いかは考え方次第で、結局は自分が何を良しとするかです。これについては他のサイトに説明を譲りますが、いずれにしても下記の3要素は考慮したほうが良いです。

1つめはルータやFWなどのOSの前段でもACLを用いてパケットフィルタリングをしてる場合、前段にあるルータやFWと、後段のOSのIP FWのDenyの挙動は合わせた方がいいです。

前段と後段でDenyの挙動を変えると、「特定のポート以外はREJECTで何か返ってくるのに、特定のポートは何も返ってこない」みたいに挙動になります。

これそのポートには何らかのサービスがあることを暗示することに繋がります。これは管理用のSSH等、隠蔽したいサービスやポートを開いている場合は適切ではありません。

2つめはIP FWでDROPを行うとクライアントがTCP SYN等を再送してくることがあります。FWでSYNの時点でログ出力する設定をしてると、DROPの場合この再送によってログの行数が無駄に増えます。大体2〜3回再送してくるので、これに伴ってDROPログも2〜3倍無駄にに増えます。

体感的な傾向では、攻撃観点ではIP FWの場合はREJECTにしてTCP RST送っておくと即座に諦めてくれることが多いです。ただ、REJECTされてもめげないIPはちょくちょくいます。まじでクソですね。またアプリケーションが行うREJECT場合、REJET(Webなら403 Forbidden等)を出しても諦めてくれるのは期待薄です。

ちなみに、YuaihoはIP FWではDROP派です。上記は一見REJECTを推してますが、Yuaihoが所有する機材はDROPしてることの方が圧倒的に多いです。

3つ目はアプリケーションの場合、DROPを返すと何も方法を与えないことができますが、REJECT応答を返すとそのサービスが稼働してること自体はわかってしまいます。ここでバナー隠蔽などの設定が漏れていると、OSやソフトウェアバージョンなどもわかってしまうことがあります。

最後に

ACLはこんな感じで実装ごとに動作挙動が異なります。

某C社の「暗黙のDeny」は結構有名ですが、「暗黙のDeny」は別にACLの標準仕様でもデファクトスタンダードでもありません。これまで見てきたように、本当に実装ごとに違いますし、なんなら暗黙のAllowも普通にあります。

初めて触れるアクセス制御機構を設定するときは、少なくとも「デフォルトの挙動」「マッチ後の挙動」ぐらいは確認しないとガバガバACLを作る可能性があります。事故った時にシャレにならないことになる方は特に注意しましょう・・・。