English

トップ レポート 講演資料(PDF) 紹介記事 ホワイトリスト ブラックリスト 監視ツール 導入事例 Q&A ブログ リンク 更新履歴 連絡先

拒絶ログソーティングスクリプト

2014/10/27バージョンアップ(旧バージョンはこちら

 S25Rスパム対策方式によって正当なメールサーバが誤って拒絶されているのを発見するのに有用なシェルスクリプトを紹介します。メールサーバがウェブサーバを兼ねているなら、このスクリプトをcgi-binディレクトリ配下のディレクトリにパスワード付きで置くことにより、ウェブブラウザで拒絶記録を容易に監視できます。コマンドとして実行することもできます。

機能
 このスクリプトは、応答コード「4XX」(「後で再試行せよ」の意味;「XX」は数字の組)で拒絶されたアクセスの記録をPostfixのメールログから抽出し(S25Rによるもの以外も抽出されます)、再試行アクセスが連続して並ぶようにソーティングして表示します。すなわち、クライアントIPアドレス、送信者アドレス、および受信者アドレスとも同じであるアクセスは、連続した行で表示されます。それらのいずれかでも異なるアクセスは、空白行で分断して表示されます。
 拒絶の理由コードが次の文字で表示されます。
 また、最後に次のデータを表示します。
活用法
 正当なメールサーバは、応答コード「4XX」による拒絶に対して、必ず適度な時間間隔を置きながら送信を再試行します。その拒絶の記録は、このスクリプトによって連続して表示されます。したがって、ホワイトリストに登録すべき正当なメールサーバからのアクセスを見つけるのに助けになります(理由コードが「C」か「B」でない場合は、S25Rのホワイトリストにホストを登録しても受信できませんので、ご注意ください)。
 連続して表示されたアクセスが次のすべての条件を満たすならば、クライアントはホワイトリストに登録すべき正当なメールサーバである可能性があります。  一方、連続して表示されたアクセスが次のいずれかの条件に該当するならば、クライアントはおそらく、あるいは間違いなく不正です。  クライアントが正当か不正か、いずれとも判断しにくい場合は、ひとまずホワイトリストに登録して、もし受信者からスパムの苦情が来たら登録を取り消すのがよいでしょう。

必要な設定
 HTTPデーモンの権限でメールログファイルが読めるようにアクセス権を設定してください。多くのシステムでは、以下のコマンドで設定できます。
chgrp nobody /var/log/maillog*
chmod g+r /var/log/maillog*
 HTTPデーモンのユーザー・グループ名が「daemon」である場合は、上記の「nobody」を「daemon」に置き換えてください。

改造
(どんな改造もその公表もご自由にどうぞ。)

シェルスクリプトコード

(プレーンテキスト表示版;長い行は折り返し)
#!/bin/sh
echo "Content-Type: text/plain"
echo
echo "Mail rejection log (4XX) - sorted;" \
    "Reason code: C=Client, B=DNSBL, S=Sender, R=Recipient, H=HELO, O=Other"
echo
#
# (1) Input mail log.
#
cat /var/log/maillog.1 /var/log/maillog | \
#
# (2) Extract records indicating "4XX".
#
egrep 'reject: RCPT from [^]]+\]: 4[0-9][0-9] ' | \
#
# (3) Extract essential items.
#
gawk '
{
  client=substr($0, match($0, /from [^]]+\]/)+5, RLENGTH-5)
  sub(/\[/, " [", client)
  sender=substr($0, match($0, /from=<[^>]*>/), RLENGTH)
  rcpt=substr($0, match($0, /to=<[^>]*>/), RLENGTH)
  helo=substr($0, match($0, /helo=<[^>]*>/), RLENGTH)
  if (match($0, /Client host rejected/))
    reason="C"
  else if (match($0, /Client host .+ blocked/))
    reason="B"
  else if (match($0, /Sender address rejected/))
    reason="S"
  else if (match($0, /Recipient address rejected/))
    reason="R"
  else if (match($0, /Helo command rejected/))
    reason="H"
  else
    reason="O"
  printf "%s %2d %s %s %s %s %s %s\n", \
      $1, $2, $3, reason, client, sender, rcpt, helo
}
' | \
#
# (4) Convert month names into month numbers.
#
gawk '
BEGIN {
  month_num["Jan"]=1
  month_num["Feb"]=2
  month_num["Mar"]=3
  month_num["Apr"]=4
  month_num["May"]=5
  month_num["Jun"]=6
  month_num["Jul"]=7
  month_num["Aug"]=8
  month_num["Sep"]=9
  month_num["Oct"]=10
  month_num["Nov"]=11
  month_num["Dec"]=12
  max_month_num=0
}
{
  $1=month_num[$1]
  if ($1>max_month_num)
    max_month_num=$1
  else if ($1<max_month_num)
    $1+=12
  printf "%3d %2d %s %s %s %s %s %s %s\n", $1, $2, $3, $4, $5, $6, $7, $8, $9
}
' | \
#
# (5) Sort according to IP address, sender address and recipient address.
#
sort -k 6,8 | \
#
# (6) Insert a blank line between records with a different triplet.
#
gawk '
BEGIN {
  prev_triplet=""
}
{
  if (prev_triplet!="") {
    if (prev_triplet!=$6 $7 $8)
      print ""
  }
  print
  prev_triplet=$6 $7 $8
}
' | \
#
# (7) Convert retry records in a sequence into one line.
#
gawk '
BEGIN {
  RS=""
}
{
  gsub(/\n/, "\036")
  print
}
' | \
#
# (8) Sort according to date and time.
#
sort -k 1,3 | \
#
# (9) Reconvert retry records in a sequence into multiple lines.
#
gawk '
{
  gsub(/\036/, "\n")
  print
  print ""
}
' | \
#
# (10) Reconvert month numbers into month names.
#
gawk '
BEGIN {
  month_name[1]="Jan"
  month_name[2]="Feb"
  month_name[3]="Mar"
  month_name[4]="Apr"
  month_name[5]="May"
  month_name[6]="Jun"
  month_name[7]="Jul"
  month_name[8]="Aug"
  month_name[9]="Sep"
  month_name[10]="Oct"
  month_name[11]="Nov"
  month_name[12]="Dec"
}
{
  if ($0!="") {
    $1=month_name[($1-1)%12+1]
    printf "%s %2d %s %s %s %s %s %s %s\n", $1, $2, $3, $4, $5, $6, $7, $8, $9
  }
  else
    print ""
}
' | \
#
# (11) Output sorted records with counting.
#
gawk '
BEGIN {
  Suppress_single_access_records=0
  RS=""
  acc_count=0
  host_and_rcpt=""
  msg_count=0
  seq_count=0
}
{
  retry_count=gsub(/\n/, "\n")
  acc_count+=1+retry_count
  if (index(host_and_rcpt, $6 $8)==0) {
    ++msg_count
    host_and_rcpt=$6 $8 host_and_rcpt
  }
  if (retry_count>0)
    ++seq_count
  if (!(retry_count==0 && Suppress_single_access_records)) {
    print
    print ""
  }
}
END {
  print "access count =", acc_count, \
      ", estimated message count =", msg_count, \
      ", retry sequence count =", seq_count
}
'

(HTML <PRE>タグ表示版;長い行は横スクロール)
#!/bin/sh
echo "Content-Type: text/html"
echo
echo "<html><body><pre>"
echo "Mail rejection log (4XX) - sorted;" \
    "Reason code: C=Client, B=DNSBL, S=Sender, R=Recipient, H=HELO, O=Other"
echo
#
# (1) Input mail log.
#
cat /var/log/maillog.1 /var/log/maillog | \
#
# (2) Extract records indicating "4XX".
#
egrep 'reject: RCPT from [^]]+\]: 4[0-9][0-9] ' | \
#
# (3) Extract essential items.
#
gawk '
{
  client=substr($0, match($0, /from [^]]+\]/)+5, RLENGTH-5)
  sub(/\[/, " [", client)
  sender=substr($0, match($0, /from=<[^>]*>/), RLENGTH)
  rcpt=substr($0, match($0, /to=<[^>]*>/), RLENGTH)
  helo=substr($0, match($0, /helo=<[^>]*>/), RLENGTH)
  if (match($0, /Client host rejected/))
    reason="C"
  else if (match($0, /Client host .+ blocked/))
    reason="B"
  else if (match($0, /Sender address rejected/))
    reason="S"
  else if (match($0, /Recipient address rejected/))
    reason="R"
  else if (match($0, /Helo command rejected/))
    reason="H"
  else
    reason="O"
  printf "%s %2d %s %s %s %s %s %s\n", \
      $1, $2, $3, reason, client, sender, rcpt, helo
}
' | \
#
# (4) Convert month names into month numbers.
#
gawk '
BEGIN {
  month_num["Jan"]=1
  month_num["Feb"]=2
  month_num["Mar"]=3
  month_num["Apr"]=4
  month_num["May"]=5
  month_num["Jun"]=6
  month_num["Jul"]=7
  month_num["Aug"]=8
  month_num["Sep"]=9
  month_num["Oct"]=10
  month_num["Nov"]=11
  month_num["Dec"]=12
  max_month_num=0
}
{
  $1=month_num[$1]
  if ($1>max_month_num)
    max_month_num=$1
  else if ($1<max_month_num)
    $1+=12
  printf "%3d %2d %s %s %s %s %s %s %s\n", $1, $2, $3, $4, $5, $6, $7, $8, $9
}
' | \
#
# (5) Sort according to IP address, sender address and recipient address.
#
sort -k 6,8 | \
#
# (6) Insert a blank line between records with a different triplet.
#
gawk '
BEGIN {
  prev_triplet=""
}
{
  if (prev_triplet!="") {
    if (prev_triplet!=$6 $7 $8)
      print ""
  }
  print
  prev_triplet=$6 $7 $8
}
' | \
#
# (7) Convert retry records in a sequence into one line.
#
gawk '
BEGIN {
  RS=""
}
{
  gsub(/\n/, "\036")
  print
}
' | \
#
# (8) Sort according to date and time.
#
sort -k 1,3 | \
#
# (9) Reconvert retry records in a sequence into multiple lines.
#
gawk '
{
  gsub(/\036/, "\n")
  print
  print ""
}
' | \
#
# (10) Reconvert month numbers into month names.
#
gawk '
BEGIN {
  month_name[1]="Jan"
  month_name[2]="Feb"
  month_name[3]="Mar"
  month_name[4]="Apr"
  month_name[5]="May"
  month_name[6]="Jun"
  month_name[7]="Jul"
  month_name[8]="Aug"
  month_name[9]="Sep"
  month_name[10]="Oct"
  month_name[11]="Nov"
  month_name[12]="Dec"
}
{
  if ($0!="") {
    $1=month_name[($1-1)%12+1]
    printf "%s %2d %s %s %s %s %s %s %s\n", $1, $2, $3, $4, $5, $6, $7, $8, $9
  }
  else
    print ""
}
' | \
#
# (11) Output sorted records with counting.
#
gawk '
BEGIN {
  Suppress_single_access_records=0
  RS=""
  acc_count=0
  host_and_rcpt=""
  msg_count=0
  seq_count=0
}
{
  retry_count=gsub(/\n/, "\n")
  acc_count+=1+retry_count
  if (index(host_and_rcpt, $6 $8)==0) {
    ++msg_count
    host_and_rcpt=$6 $8 host_and_rcpt
  }
  if (retry_count>0)
    ++seq_count
  if (!(retry_count==0 && Suppress_single_access_records)) {
    print
    print ""
  }
}
END {
  print "access count =", acc_count, \
      ", estimated message count =", msg_count, \
      ", retry sequence count =", seq_count
}
' | \
#
# (12) Convert "<" and ">" into entities.
#
gawk '
{
  gsub(/</, "\\&lt;")
  gsub(/>/, "\\&gt;")
  print
}
'
echo "</pre></body></html>"
トップ レポート 講演資料(PDF) 紹介記事 ホワイトリスト ブラックリスト 監視ツール 導入事例 Q&A ブログ リンク 更新履歴 連絡先