仙石浩明の日記

2007年8月16日

CPAN の IP::Country を C で書き直して MTA に組み込み、メールのヘッダに国コードを挿入するようにしてみた

自前のブラックリストを用いて迷惑メール (spam, UBE) を排除する方法について、 「迷惑メール送信者とのイタチごっこを終わらせるために (1)」で説明した。 メールの送信元 IP アドレスが DNS で逆引きできない場合に、 その IP アドレスがブラックリストに載っているか否かを調べ、 もし載っているならその IP アドレスを 「ダイアルアップ IP アドレス」に準じる扱いにする、 という方法である。

迷惑メールを送ってきた実績 (?) がある IP アドレスブロックであれば、 ためらうこと無くブラックリストに入れてしまえるのであるが、 初めてメールを送ってきた IP アドレスブロックを、 逆引きできないという理由だけでダイアルアップ IP アドレス扱いするのは、 少々乱暴だろう。 そこで、 接続元 IP アドレスが属する国のコードをメールヘッダに挿入する仕掛けを MTA (Message Transfer Agent, メールサーバ) に作り込んでみた。例えば、

Received: from unknown (HELO unknown.interbgc.com) (89.215.246.95)
  by senri.gcd.org with SMTP; 15 Aug 2007 20:46:15 +0000
X-Country: BG 89.215.246.95
Received-SPF: pass (senri.gcd.org: SPF record at thelobstershoppe.com designates 89.215.246.95 as permitted sender)
Message-ID: <34f301c7df7d$2e65ece5$5ff6d759@unknown.interbgc.com>

といった感じで、「X-Country: 」フィールドが挿入される。 「89.215.246.95」がこのメールを送ってきたマシンの IP アドレスであり、 その前の「BG」が、 この IP アドレスが属する国 (この例ではブルガリア) の ISO 3166 コード である。

ブルガリアに知り合いがおらず、 かつこのメールがメーリングリスト宛でなく個人アドレス宛であるならば、 MUA (Message User Agent, メーラー) の設定で、 このメールを迷惑メールとして排除することが可能だろう。 あるいは逆に、 「X-Country: JP」である場合は、 迷惑メール判定の結果にかかわらず排除しないという設定にして、 必要なメールを誤って排除するのを防止することもできるだろう (日本語の迷惑メールも、大半は海外の IP アドレスから送信されている)。

IP アドレスから国コード (ISO 3166 コード) を調べるサービスはいろいろあるが、 メールを受信するたびに外部のサイトへ通信するのはあまり感心しない。 ネットワークないし外部のサイトの状況の影響を受けてしまうし、 あるいは逆に大量のメールを一時に受信したときなど、 そのサイトに迷惑をかけてしまう恐れもある。 集中して問合わせを行なってしまった、などの理由で濫用と判断され、 サービスの提供が受けられなくなってしまう可能性もある。

したがって、IP アドレスから国コードを検索するためのデータベースを 自前で持つことが望ましい。 例えば CPAN には、 IP アドレスから国コードを検索するモジュール 「IP::Countryが 登録されている。 このモジュールをインストールすると、 「/usr/lib/perl5/site_perl/5.8.8/IP/Country/Fast」ディレクトリに、 「ip.gif」と「cc.gif」というファイルがインストールされる。

% ls -l /usr/lib/perl5/site_perl/5.8.8/IP/Country/Fast
total 256
-r--r--r-- 1 perl perl    681 Feb  2  2007 cc.gif
-r--r--r-- 1 perl perl 252766 Feb  2  2007 ip.gif

「ip.gif」が、IP アドレスから国番号を検索するためのデータベースであり、 「cc.gif」が、国番号から国コード (ISO 3166 コード) への変換テーブルである。

メールを受信するたびにメールサーバで perl スクリプトを実行するのは、 メールサーバの負荷などの観点からあまり望ましくない (私のサイトではメールサーバを chroot 環境で動かしていて、 その chroot 環境には perl をインストールしていない、 というセキュリティ上の理由もある) ので、 ほとんど perl スクリプトをそのまま C に置き換えただけなので、 説明は不要だろう。 inet_ntocc 関数に限ると、 C 版のほうが perl 版より簡潔に書けてしまっている点が興味深い。 コメントは、IP/Country/Fast.pm スクリプトのコメントをそのまま入れてある。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#ifndef DBDIR
#define DBDIR "/usr/lib/perl5/site_perl/5.8.8/IP/Country/Fast"
#endif
#define CC_MAX	256	/* # of countries */

int inet_ntocc(u_char *ip_db, u_long inet_n) {
/*
  FORMATTING OF EACH NODE IN $ip_db
  bit0 - true if this is a country code, false if this
         is a jump to the next node
  
  country codes:
    bit1 - true if the country code is stored in bits 2-7
           of this byte, false if the country code is
           stored in bits 0-7 of the next byte
    bits 2-7 or bits 0-7 of next byte contain country code
  
  jumps:
    bytes 0-3 jump distance (only first byte used if
           distance < 64)
*/
    u_long mask = (1 << 31);
    const u_long bit0 = 0x80;
    const u_long bit1 = 0x40;
    int pos = 4;
    u_char byte_zero = ip_db[pos];
    /* loop through bits of IP address */
    while (mask) {
	if (inet_n & mask) {
	    /* bit[$i] is set [binary one]
	       - jump to next node
	       (start of child[1] node) */
	    if (byte_zero & bit1) {
		pos = pos + 1 + (byte_zero ^ bit1);
	    } else {
		pos = pos + 3 + ((ip_db[pos] << 8 | ip_db[pos+1]) << 8
				 | ip_db[pos+2]);
	    }
	} else {
	    if (byte_zero & bit1) {
		pos = pos + 1;
	    } else {
		pos = pos + 3;
	    }
	}
	/*
	  all terminal nodes of the tree start with zeroth bit 
	  set to zero. the first bit can then be used to indicate
	  whether we're using the first or second byte to store the
	  country code */
	byte_zero = ip_db[pos];
	if (byte_zero & bit0) {
	    if (byte_zero & bit1) {
		/* unpopular country code - stored in second byte */
		return ip_db[pos+1];
	    } else {
		/* popular country code - stored in bits 2-7
		   (we already know that bit 1 is not set, so
		   just need to unset bit 1) */
		return byte_zero ^ bit0;
	    }
	}
	mask = (mask >> 1);
    }
    return -1;
}

u_char *getdb(char *file, int *fdp, int *lenp) {
    char path[PATH_MAX+1];
    int fd;
    struct stat st;
    int length;
    u_char *db;
    snprintf(path, PATH_MAX, "%s/%s", DBDIR, file);
    path[PATH_MAX] = '\0';
    fd = open(path, O_RDONLY);
    if (fd < 0) {
	fprintf(stderr, "Can't open: %s err=%d\n", path, errno);
	exit(1);
    }
    if (fdp) *fdp = fd;
    if (fstat(fd, &st) < 0) {
	fprintf(stderr, "Can't stat: %s fd=%d err=%d\n", path, fd, errno);
	exit(1);
    }
    length = st.st_size;
    if (lenp) *lenp = length;
    db = (u_char*)mmap((void*)0, length, PROT_READ, MAP_SHARED, fd, 0);
    if (db == MAP_FAILED) {
	fprintf(stderr, "Can't map: %s fd=%d len=%d err=%d\n",
		path, fd, length, errno);
	exit(1);
    }
    return db;
}

int main(int argc, char *argv[]) {
    int i;
    u_char *ip_db = getdb("ip.gif", NULL, NULL);
    const char *_cc = 
	"USDEGBNL--FREUBEITESCACHRUSEAUAT"
	"PLCZIEFIJPDKNOUAZANGILROGRCNPTHU"
	"INTRIQSGHKCYIRLTNZKRLUBGAEARBRSI"
	"IDCLSKTWSAMYTHYUMXLVCOPHLBPKKZGH"
	"EETZKWKERSBDHRDZEGVECMPEECLIPRGE"
	"ISMEAPBYMGMDAOCIMTSOPAZWVNBANEBH"
	"PSJOCDMZAZMAUZDOBJAMUGSLCGGNZMMU"
	"TJMCANMWJMGIMKCRBMLRBOUYGTBWLKGP"
	"ALGAMQKGTTNASVLYMNMRAFNPSNKHBBQA"
	"CUGLBNOMMOPYSYPGNISMMLSCDJSZLSBF"
	"CFGUNCVGHNFJPFTDLAYEFOBISDGQRWKY"
	"**BSSRADGMCVGDKNETERRETNTMTGYTMV"
	"VIHTKMSTGWAGVABZBTNRTOFKKIVUMPWS"
	"MMAWSBJEGFAIAQIOGYNFLCPWCKDMAXFM"
	"TVNUAS--------------------------"
	"--------------------------------";
#ifdef CHECKCC
    int cc_fd;
    int cc_len;
    int cc_num;
    u_char *cc_db = getdb("cc.gif", &cc_fd, &cc_len);
    char cc[CC_MAX * 2 + 1];
    cc_num = cc_len / 3;
    if (cc_num < 0 || CC_MAX <= cc_num) {
	fprintf(stderr, "Can't happen: irregular CC DB cc_num=%d\n", cc_num);
	exit(1);
    }
    for (i=0; i < CC_MAX; i++) {
	cc[i*2] = '-';
	cc[i*2+1] = '-';
    }
    cc[i*2] = '\0';
    for (i=0; i < cc_num; i++) {
	u_char c = cc_db[i*3];
	cc[c*2] = cc_db[i*3+1];
	cc[c*2+1] = cc_db[i*3+2];
    }
    munmap(cc_db, cc_len);
    close(cc_fd);
    if (strcmp(cc, _cc) != 0) {
	for (i=0; i < CC_MAX; i+=16) {
	    int j;
	    printf("\"");
	    for (j=0; j < 16; j++) {
		printf("%c%c", cc[(i+j)*2], cc[(i+j)*2+1]);
	    }
	    printf("\"\n");
	}
    }
#else
    const char *cc = _cc;
#endif
    for (i=1; i < argc; i++) {
	u_long in = ntohl(inet_addr(argv[i]));
	int c = inet_ntocc(ip_db, in);
	if (c < 0) {
	    printf("UNKNOWN %s\n", argv[i]);
	} else {
	    printf("%c%c %s\n", cc[c*2], cc[c*2+1], argv[i]);
	}
    }
    return 0;
}

国コードへの変換テーブル「cc.gif」は、 変更頻度もさほど高くないだろうと思われたので、 プログラム中に固定文字列として定義している。 コンパイル時に「-DCHECKCC」を指定することにより、 cc.gif と内蔵の変換テーブルが一致するかチェックできる。

あとは、このコードを MTA に組み込むだけ。 私のサイトでは qmail を 使っているので、 qmail-smtpd.c にこのコードを組み込んだ。

Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 07:42

3件のコメント »

  1. [pc][spam]SpamAssassinでボットネットか判別したり、接続元やURIの国コードを見るプラグイン

    SpamAssassinのプラグインでBotnetっていうプラグインがある。 Botnet Index of /~jrudd/spamassassin これは、接続元クライアントの逆引き情報とか、HELOの情報だとかから、Botnetとして使われてるPCからのものかを判別するというもの。 S25RとかHELOによるブラックリスト

    コメント by モーグルとカバとパウダーの日記 — 2007年8月30日 @ 17:06

  2. Cは門外漢ですが、次のような行を入れるとコンパイルできました。この機能はいろいろ使えそうですね。
    #define PATH_MAX 128

    コメント by ryo — 2009年1月8日 @ 00:22

  3. Linuxの64bit環境ではu_longは8バイトであるため動作しません。
    u_longをuint32_tにしてあげればOKです。

    コメント by す — 2010年12月15日 @ 17:44

この投稿へのコメントの RSS フィード。

コメントする