EMP (Excessive Multi-Post, 過度のマルチポスト) 排除のためのメモ Copyright(c)1998 by Hiroaki Sengoku sengoku@gcd.org はじめに NetNews に SPAM があふれています。幸いその多くは英語等で書かれて いるため、英語だったら読み飛ばす、という癖がついてしまった人も多 いのではないでしょうか。 読み飛ばすのが面倒になって、KILL ファイル等で、*.jp 以外から投稿 される記事を削除する、あるいは本文に日本語が含まれていなかったら 削除するスクリプトを書いている人もいるかも知れません。あるいは特 定ドメインからの記事を拒否するなど。 いずれの方法も少々乱暴で、SPAM 以外の記事、例えば日本語環境が使 えない人からの真面目な記事まで排除してしまいます。そこで SPAM だ けを確実に排除する方法として MD5 を使うことにしました。MD5 とは ハッシュ関数の一つで、NetNews 記事を 32 桁の数値に変換します。異 なる記事であれば異なる数値が得られることを利用して、同一記事 (つ まりマルチポスト) を検出しよう、というわけです。ほとんど全ての SPAM は同じ記事を多数の newsgroup へばらまいているだけですから、 この仕掛けにより排除することが可能です。 初心者が操作ミスでマルチポストしてしまった場合はどうするのか。こ こで述べるマルチポスト排除の仕掛けでは、一連のマルチポストのうち 最初の一記事だけは残しますので、ご安心を。 用意するもの INN 1.7.2 (他のバージョンでもおそらく可能) Perl 5.003 以上 RFC 1321 準備 (1) INN 付属の README.perl_hook を読んで、Perl filtering を有効 にする。具体的には、config.data の PERL_SUPPORT を DO にして、 PERL_INC と PERL_LIB に適切な値を設定する。 (2) 下記「ファイル」の項を参照して、innd.patch および filter_innd.pl を作る。必要なら filter_innd.pl の下記変数を 変更する。 $filter_innd_log 排除した記事のログ $filter_md5_log 排除した EMP のログ $filter_md5_limit 許容するマルチポストの個数 + 1 $filter_md5_spool 記事の MD5 を記憶するスプール $filter_cancel キャンセルすべき記事の ID の出力先 (3) $filter_md5_spool のディレクトリに、0/0 0/1 0/2 ... f/f の 256 個のサブディレクトリを news 権限で作る。例えば次のコマン ドを実行すれば良い。 % cd $filter_md5_spool % foreach a (0 1 2 3 4 5 6 7 8 9 a b c d e f) ? mkdir $a ? foreach b (0 1 2 3 4 5 6 7 8 9 a b c d e f) ? mkdir $a/$b ? end ? end (4) $filter_innd_log $filter_md5_log $filter_md5_spool $filter_cancel それぞれが、news 権限で書き込み可能か確認。 % ls -ld /var/log/news drwxrwxr-x 4 news news /var/log/news % ls -ld /var/news/spool/MD5 drwxrwxr-x 4 news news /var/news/spool/MD5 % ls -ld /var/news/spool/out.going drwxrwsr-x 2 news news /var/news/spool/out.going (5) RFC 1321 を参照して、md5c.c md5.h および global.h を作る。 インストール (1) 「準備」で作ったファイル md5c.c md5.h および global.h を、 INN のソースディレクトリの中の innd サブディレクトリへコピー。 cp md5c.c md5.h global.h /usr/local/src/inn-1.7.2/innd/ (2) 「準備」で作ったパッチ innd.patch をあてる。 cd /usr/local/src/inn-1.7.2 patch -p < innd.patch (3) INN のコンパイルおよびインストール。 (4) 「準備」で作ったフィルタ filter_innd.pl を _PATH_PERL_FILTER_INND にインストール。 cp filter_innd.pl $(_PATH_PERL_FILTER_INND) 起動 (1) INN 起動。 su news -c rc.news (2) $filter_md5_spool の下にファイルが作られるか確認。 例えば、記事本文の MD5 が 01234567890123456789012345678901 の場合、$filter_md5_spool/0/1/234567890123456789012345678901 というファイルが作られ、ファイルの内容は記事の Message-ID と なる。 (3) $filter_md5_spool の下のファイルに、$filter_md5_limit 個以上 の Message-ID が書き出されると、EMP と判断し記事の受け入れを 拒否するとともに、$filter_md5_log へログを出力。また、すでに 受け入れてしまった記事をキャンセルするために、$filter_cancel へ Message-ID を出力する。 後始末 (1) $filter_cancel の内容を定期的に見て、ctlinnd cancel を実行す る。例えば次の sh スクリプトを実行する。 #!/bin/sh if [ -s $filter_cancel ] then mv $filter_cancel $filter_cancel.work for id in `cat $filter_cancel.work` do ctlinnd cancel $id > /dev/null done fi (2) $filter_md5_spool の下のファイルを定期的に掃除する。例えば、 cron で次のコマンドを実行する。 find $filter_md5_spool -type f -mtime +10 | xargs rm -f ファイル innd.patch INN にあてるパッチ filter_innd.pl Perl filter innd.patch ----- ここから ----- ここから ----- ここから ----- ここから ----- toyokawa:/usr/local/src/inn-1.7.2 % diff -u innd/art.c.org innd/art.c --- innd/art.c.org Tue Dec 9 08:48:50 1997 +++ innd/art.c Fri Apr 10 10:42:03 1998 @@ -10,6 +10,13 @@ #include "dbz.h" #include "art.h" #include +#ifdef MD5 +#include "global.h" +#include "md5.h" +#define DIGEST_LEN 16 +BUFFER MD; +char MD5_str[SMBUF], digest_str[DIGEST_LEN*2+1]; +#endif typedef struct iovec IOVEC; @@ -131,11 +138,14 @@ #define _xref 17 { "Keywords", HTstd }, #define _keywords 18 + { "X-MD5", HTobs }, +#define _md5 19 { "Date-Received", HTobs }, { "Posted", HTobs }, { "Posting-Version", HTobs }, { "Received", HTobs }, { "Relay-Version", HTobs }, + { "NNTP-Posting-Host", HTstd }, }; ARTHEADER *ARTheadersENDOF = ENDOF(ARTheaders); @@ -533,7 +543,11 @@ register IOVEC *vp; register long size; register char *p; +#ifdef MD5 + IOVEC iov[7 /* +1 */]; +#else IOVEC iov[7]; +#endif IOVEC *end; char bytesbuff[SMBUF]; int i; @@ -591,6 +605,12 @@ vp->iov_len = Xref.Used; size += (vp++)->iov_len; +#ifdef MD5 +/* vp->iov_base = MD.Data; + vp->iov_len = MD.Used; + size += (vp++)->iov_len; */ +#endif + end = vp; vp->iov_base = NL; vp->iov_len = 1; @@ -882,6 +902,11 @@ register char *p; STRING error; int delta; +#ifdef MD5 + int s, t; + MD5_CTX context; + unsigned char digest[16]; +#endif /* Read through the headers one at a time. */ Data->Feedsite = "?"; @@ -938,11 +963,58 @@ } /* Scan the body, counting lines. */ +#ifdef MD5 + MD5Init(&context); + s = t = 0; +#endif for (i = 0; *in; ) { - if (*in == '\n') +#ifdef MD5 + if (t < SMBUF-1) buff[t++] = *in; +#endif + if (*in == '\n') { +#ifdef MD5 + switch(s) { + case 0: + if (t == 1) s++; + break; + case 1: + if (buff[0] == '-' && buff[1] == '-') s++; + else s = 0; + break; + case 2: + if (t == 1) s = 0; + break; + } + if (i < 100 && s < 2) { + buff[t] = '\0'; + MD5Update(&context, buff, t); + } + t = 0; +#endif i++; + } *out++ = *in++; } +#ifdef MD5 + MD5Final(digest, &context); + MD.Data = MD5_str; + MD.Size = SMBUF; + strcpy(MD.Data,"X-MD5: "); + MD.Used = strlen(MD.Data); + for (s=0; s < DIGEST_LEN; s++) { + (void)sprintf(&MD.Data[MD.Used],"%02x",digest[s]); + MD.Used += 2; + } + MD.Data[MD.Used++] = '\n'; + MD.Data[MD.Used] = '\0'; + MD.Left = MD.Size - MD.Used; + + /* Install in header table; STRLEN("X-MD5: ") == 7. */ + strncpy(digest_str,MD.Data+7,DIGEST_LEN*2); + HDR(_md5) = digest_str; + ARTheaders[_md5].Length = DIGEST_LEN*2; + ARTheaders[_md5].Found = 1; +#endif *out = '\0'; Article->Used = out - Article->Data; Data->LinesValue = i; toyokawa:/usr/local/src/inn-1.7.2 % diff -u innd/Makefile.org innd/Makefile --- innd/Makefile.org Fri Feb 20 19:48:01 1998 +++ innd/Makefile Thu Apr 9 16:19:25 1998 @@ -47,11 +47,13 @@ SOURCES = \ art.c cc.c chan.c his.c icd.c innd.c lc.c nc.c newsfeeds.c ng.c \ - proc.c rc.c site.c tcl.c perl.c + proc.c rc.c site.c tcl.c perl.c \ + md5c.c OBJECTS = \ art.o cc.o chan.o his.o icd.o innd.o lc.o nc.o newsfeeds.o ng.o \ - proc.o rc.o site.o tcl.o perl.o + proc.o rc.o site.o tcl.o perl.o \ + md5c.o ALL = innd inndstart @@ -112,6 +114,9 @@ echo $@ needs to be installed setuid root ;\ echo "" ; echo "" ;\ fi + +art.o: art.c + $(CC) $(CFLAGS) -DMD5 $< -c -o $@ ## Dependencies. Default list, below, is probably good enough. depend: Makefile $(SOURCES) ../include/dbz.h toyokawa:/usr/local/src/inn-1.7.2 % diff -u innd/global.h.org innd/global.h --- innd/global.h.org Fri Apr 25 23:04:41 1997 +++ innd/global.h Wed Apr 8 20:23:23 1998 @@ -11,7 +11,7 @@ #endif /* POINTER defines a generic pointer type */ -typedef unsigned char *POINTER; +/* typedef unsigned char *POINTER; */ /* UINT2 defines a two byte word */ typedef unsigned short int UINT2; ----- ここまで ----- ここまで ----- ここまで ----- ここまで ----- filter_innd.pl ----- ここから ----- ここから ----- ここから ----- ここから ----- $filter_innd_log = "/var/log/news/filter_innd.log"; $filter_md5_log = "/var/log/news/md5.log"; $filter_md5_limit = 4; $filter_md5_spool = "/var/news/spool/MD5"; $filter_cancel = "/var/news/spool/out.going/cancel"; %reject = ( 'From' => { 'anonymous' => 'From anonymous', 'nobody' => 'From nobody', }, 'From&Message-ID' => { '@\d+\.com$' => '.com', 'nowhere' => 'Junk domain', }, 'Newsgroups' => { 'binaries' => 'binaries group', 'erotica' => 'erotica group', 'fetish' => 'fetish group', }, 'Subject' => { 'make.*money.*fast' => 'MMF', 'cash.*cash.*cash' => 'Cash', 'sex.*pic' => 'Pict', 'business.*opportunity' => 'MMF', 'hardcore.*sex' => 'Sex', }, ); sub filter_art { my($rval) = ''; my($from) = $hdr{'From'}; my($mid) = $hdr{'Message-ID'}; my($md) = $hdr{'X-MD5'}; my(@i, $i, $j, $k, $l, $nfiles, $id, $mdfile, @ids); if ($hdr{'Newsgroups'} =~ /^local\./ || $hdr{'Control'}) { goto the_end; # no check } if ($from =~ /<([^>]+)>/) { $from = $1; } else { $from =~ s/\s*\([^\)]*\)\s*//g; $from =~ s/^\s*(\S+)\s*$/$1/; } $from =~ s/\s//g; $mid =~ s/^\s*<(\S+)>\s*$/$1/; $mdfile = $md; $mdfile =~ s/(.)(.)(.*)/$filter_md5_spool\/$1\/$2\/$3/; if (open(MDF,"+>> $mdfile")) { print MDF "<$mid>\n"; if (seek(MDF,0,0)) { $nfiles = 0; while() { ($id) = split; push(@ids,$id); $nfiles++; } close(MDF); if ($nfiles >= $filter_md5_limit && open(LOG,">>$filter_md5_log")) { if ($nfiles < ($filter_md5_limit - 1) * 2) { $id = $ids[$nfiles - $filter_md5_limit + 1]; if (open(OUT,">>$filter_cancel")) { print OUT "$id\n"; close(OUT); } print LOG &date_now." $md <$mid> $id\n"; } else { print LOG &date_now." $md <$mid>\n"; } close(LOG); } if ($nfiles >= $filter_md5_limit) { $rval = 'EMP'; goto the_end; } } else { close(MDF); } } if ($from !~ /^[-%\.\w\d\+]+\@[-\.\w\d]+\.[-\.\w\d]+$/) { $rval = 'Invalid From'; } elsif ($mid !~ /^[^\s\@<>]+\@[-\.\w\d]+\.[-\.\w\d]+$/) { $rval = 'Invalid Message-ID'; } elsif (",$hdr{'Newsgroups'}" =~ /,(fj|japan|kansai|kanto|okinawa|pin|tnn)\./i && ",$hdr{'Newsgroups'}" =~ /,(alt|comp|misc|news|rec|sci|soc|talk)\./i) { $rval = 'Crosspost JP & BIG8'; } else { while (($i, $j) = each %reject) { if ($i eq 'From') { @i = ($from); } elsif ($i eq 'From&Message-ID') { @i = ($from,$mid); } elsif ($i eq 'Message-ID') { @i = ($mid); } else { @i = ($hdr{$i}); } while ($_ = shift @i) { while (($k, $l) = each %{$j}) { if (/$k/i) { $rval = $l; goto the_end; } } } } @i = split(/,/, $hdr{'Newsgroups'}); if ($#i > 10) { $rval = 'ECP'; } } the_end: if ($rval) { $rval .= " rejected"; if ($filter_innd_log) { if (open(LOG,">>$filter_innd_log")) { print LOG "\n".&date_now." <$mid> $rval\n"; while (($i, $j) = each %hdr) { print LOG "$i: $j\n"; } close(LOG); } else { $filter_innd_log = ""; } } } undef %hdr; return $rval; } sub date_now { local($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); return sprintf("%s %2d %02d:%02d:%02d", ("Jan","Feb","Mar","Apr","May","Jun", "Jul","Aug","Sep","Oct","Nov","Dec")[$mon], $mday, $hour, $min, $sec); } sub filter_mode { } ----- ここまで ----- ここまで ----- ここまで ----- ここまで ----- #2493. 仙石 浩明 http://www.gcd.org/sengoku/ Hiroaki Sengoku