avicom의 신변잡기

Mail Filter 본문

LiNux / sTorAge

Mail Filter

avicom 2010. 12. 23. 11:51

Mail Filter

libmilter 설치

sendmail 컴파일

  • 소스 다운로드
rpm버전을 설치해도 되고, 소스 컴파일 설치해도 되지만, perl에서 milter 모듈을 컴파일하기 위해선 컴파일된 sendmail 소스가 필요하다.
wget ftp://ftp.sendmail.org/pub/sendmail/sendmail.8.14.4.tar.gz
  • 빌드
64bit linux에서 sendmail을 컴파일하기 위해선 gcc -fPIC 옵션을 붙여야한다. -fPIC(position independent code)옵션 없이 컴파일할 경우 메모리의 지정된 주소에서 함수 및 심볼을 찾기 때문에 해당 라이브러리를 링크해서 컴파일할 때 필요한 함수를 못찾는 에러가 발생할 수 있다. ~sendmail source/devtools/M4/header.m4에 보면 cc 설정을 정의한 부분이 있다. 여기에 -fPIC 옵션을 추가한다.
define(`confCC', `cc -fPIC')

여기서는 MTA로 postfix를 사용하고, sendmail의 libmilter 를 가져와서 사용할 것이기 때문에, sendmail 컴파일까지만 하고 설치할 필요는 없다. (libmilter.a라는 정적 라이브러리가 필요하다)
top level 디렉토리의 Makefile을 열어서 SUBDIRS 항목에 libmilter를 추가하고 Build한다.
# ./Build

obj.Linux.{kernel_version}.x86_64 디렉토리가 생성되고 그 밑에 libmilter 디렉토리에 milter관련 라이브러리가 컴파일되어있다.

perl Milter 모듈 컴파일

  • 소스 다운로드
wget http://search.cpan.org/CPAN/authors/id/C/CY/CYING/Sendmail-Milter-0.18.tar.gz

  • 컴파일 및 설치
perl Makefile.PL /root/src/sendmail-8.14.4 /root/src/sendmail-8.14.4/obj.Linux.2.6.18-92.el5xen.x86_64
make
make install
main.cf 수정


postfix는 가능하면 최근 버전을 사용하도록 한다. milter가 postfix에 완전하게 포팅되지 않아 postfix 버전별로 지원할 수 있는 기능이 다르다.

/etc/postfix/main.cf에 다음 항목을 추가한다.
milter_default_action = tempfail
smtpd_milters = unix:/var/run/milter.sock
milter_protocol = 2

이제 postfix과 milter.pl을 재시작하면 postfix로 들어오는 모든 메일은 /var/run/milter.sock을 통해 milter 모듈로 보내지고 필터링된 결과에 따라 반환값을 받게된다

Milter.pl 작성

Sendmail::Milter 모듈을 사용하여 milter 라이브러리를 제어한다.
milter는 몇가지 콜백함수를 정의해놓고 단계에 따라 필요할 때 호출하는 구조다. 콜백함수의 종류와 반환값은 아래 표를 참조. 여기서는 메일 헤더를 기준으로 필터링할 것이므로 header_callback 에만 동작을 지정해놓았다.
  • milter.pl
#!/usr/bin/perl

use strict ('vars');;
use warnings;

use Sendmail::Milter;
use Daemon::Simple;
use Socket;

my %my_callbacks = (
    'connect'   => \&connect_callback,
    'helo'      => \&helo_callback,
    'envfrom'   => \&envfrom_callback,
    'envrcpt'   => \&envrcpt_callback,
    'header'    => \&header_callback,
    'eoh'       => \&eoh_callback,
    'body'      => \&body_callback,
    'eom'       => \&eom_callback,
    'abort'     => \&abort_callback,
    'close'     => \&close_callback,
);

my $hash = {};

my $homedir = "/root/admin_tools";
my $pidfile = "/var/run/milter.pid";
my $command = $ARGV[0];

sub Log {

    my ($sec,$min,$hour,$mday,$mon,$year) = localtime;;
    $year = $year + 1900; $mon = $mon + 1;
    my $date = sprintf("%d-%02d-%02d %02d:%02d:%02d",$year, $mon, $mday, $hour, $min, $sec);
    my $msg = $_[0];
    my $LOG = "/var/log/milter.log";

    open FILE,'>>',$LOG;

    print FILE "$date :  $msg \n";
    close FILE;
}

Daemon::Simple::init($command,$homedir,$pidfile);

while(1) {
    BEGIN:
    {
        my $filter = "SpamMailFilter";
        my $unix_socket = '/var/run/milter.sock';

        print "Found connection info for '$filter': $unix_socket\n";

        if (-e $unix_socket) {
            print "Attempting to unlink UNIX socket '$unix_socket' ... ";

            if (unlink($unix_socket) == 0) {
                print "failed.\n";
                exit;
            }
            print "successful.\n";
        }

        if (!Sendmail::Milter::setconn('local:/var/run/milter.sock')) {
        #if (!Sendmail::Milter::auto_setconn($filter, './milter.cf')) {
            print "Failed to detect connection information.\n";
            exit;
        }

        if (!Sendmail::Milter::register($filter, \%my_callbacks, SMFI_CURR_ACTS)) {
            print "Failed to register callbacks for $filter.\n";
            exit;
        }

        print("Starting Sendmail::Milter $Sendmail::Milter::VERSION engine.\n");

        if (Sendmail::Milter::main()) {
            print "Successful exit from the Sendmail::Milter engine.\n";
        }
        else {
            print "Unsuccessful exit from the Sendmail::Milter engine.\n";
        }
    }
    sleep 5;
}

sub connect_callback {
    my ($ctx, $hostname, $sockaddr_in) = @_;

    return SMFIS_CONTINUE;
}

sub helo_callback {
    my ($ctx, $helohost) = @_;

    return SMFIS_CONTINUE;
}

sub envfrom_callback {
    my ($ctx, @args) = @_;

    return SMFIS_CONTINUE;
}

sub envrcpt_callback {
    my ($ctx, @args) = @_;

    return SMFIS_CONTINUE;
}

sub header_callback {
    my ($ctx, $headerf, $headerv) = @_;

    if ($headerf eq "From") {

        if ($headerv =~ /Returned mail/ || $headerv =~ /^Postmaster notify/ || $headerv =~ /^Mail Delivery/ ) { return SMFIS_CONTINUE; }

        my $from_address;

        if ($headerv =~ m/^[\W|\"| +]/) {
            $headerv =~ m/.*<(\S+\@\S+)>/;
            $from_address = $1;
        }
        else {
            $headerv =~ m/(\S+\@\S+)/;
            $from_address = $1;
        }

        my ($id, $addr) = split(/\@/, $from_address, 2);

        $hash->{$id}->{id} = $id;
        $hash->{$id}->{addr} = $addr;
        my $cur_time = $hash->{$id}->{cur_time} = time();

        my $blocked = $hash->{$id}->{blocked};
        my $blocked_time = $hash->{$id}->{blocked_time};

        if ( defined($blocked) && defined($blocked_time) && ($cur_time - $blocked_time) <= 30 ) {
            my $gap = $cur_time - $blocked_time;
            print "$headerv - time : $gap\n";
            return SMFIS_REJECT;
            #return SMFIS_DISCARD;
        }

        my $base_time = $hash->{$id}->{base_time};

        if (!defined($base_time)) {
            $hash->{$id}->{base_time} = time();
            $hash->{$id}->{count} = 1;
        }
        elsif ( time() -  $hash->{$id}->{base_time} <= 30) {
            $hash->{$id}->{count}++;
        }
        else {
            $hash->{$id}->{count} = 1;
        }

        #$cur_time = time();
        #$base_time = $hash->{$id}->{base_time};
        my $count = $hash->{$id}->{count};

        my $time_gap = $cur_time - $base_time;
        my $trans_rate;
        if ($count > 0 && $time_gap > 0) {
            $trans_rate = sprintf("%.2f",$count / $time_gap);
        }
        else {
            $trans_rate = 0;
        }


        my $sizeofhash += keys %$hash;

        if ($trans_rate > 30) {
            #printf ("reject : %30s    count %d     trans_rate = %.2f \n", $headerv, $count, $trans_rate);
            my $msg = sprintf("reject : %30s  total count in 30s = %2d  trans_rate = %.2f  addr : %30s hashsize : %5d", $from_address, $count, $trans_rate, $headerv, $sizeofhash);
            Log($msg);

            $hash->{$id}->{blocked} = 1;
            $hash->{$id}->{blockeb_time} = time();
            return SMFIS_REJECT;
            #return SMFIS_DISCARD;
        }
        elsif ( $time_gap > 30 ) {

            $hash->{$id}->{base_time} = time();
            $hash->{$id}->{cur_time} = time();
            $hash->{$id}->{count} = 0;
        }
        else {
            #print "addr : $id, basetime : $base_time, curtime : $cur_time, count : $count, trans_rate = $trans_rate \n";
            my $msg = sprintf("accept : %30s  total count in 30s = %2d  trans_rate = %.2f  addr : %30s hashsize : %5d", $from_address, $count, $trans_rate, $headerv, $sizeofhash) ;
            Log($msg);
        }

        if ($sizeofhash > 5000) {
            $hash = {};
        }

    }

    return SMFIS_CONTINUE;
}


sub eoh_callback {
    my ($ctx) = @_;

    return SMFIS_CONTINUE;
}

sub body_callback {
    my ($ctx, $body_chunk, $len) = @_;


#    print("▒▒▒▒ ▒▒▒▒: $len\n");
    #print "$body_chunk \n";

    return SMFIS_CONTINUE;
}

sub eom_callback {
    my ($ctx) = @_;

    $ctx->addheader("X-Simplexi-MailFilter", "1.0");


    return SMFIS_CONTINUE;
}

sub abort_callback {
    my ($ctx) = @_;

    return SMFIS_CONTINUE;
}

sub close_callback {
    my ($ctx) = @_;

    return SMFIS_CONTINUE;
}

  • start script 작성
cat milter.sh
#!/bin/sh

function start() {
        /root/admin_tools/milter.pl start
        chown postfix. /var/run/milter.sock
}

function stop() {
        /root/admin_tools/milter.pl stop
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    *)
        echo $"Usage: $0 {start|stop}"
        RETVAL=1
esac

  • 기동
milter.pl을 root로 띄울 경우 메일 프로세스가 milter.sock을 엑세스하지 못하므로 메일 데몬과 같은 owner로 띄워야한다.
./milter.sh start
/root/admin/script/postfix_policy/milter_hash.pl is Starting.

Filtering Test

  • milter.pl 설정을 30초 동안 초당 1 건을 초과하는 메일을 필터링하는 경우
2010-04-14 18:21:23 :  accept :           emerson@simplexi.com  total count in 30s =  1  trans_rate = 1.00  addr :           emerson@simplexi.com hashsize :     4
2010-04-14 18:21:23 :  accept :            dsdhks@simplexi.com  total count in 30s =  1  trans_rate = 0.00  addr :            dsdhks@simplexi.com hashsize :     4
2010-04-14 18:21:23 :  accept :               kim@simplexi.com  total count in 30s =  1  trans_rate = 0.00  addr :               kim@simplexi.com hashsize :     4
2010-04-14 18:21:23 :  accept :                ht@simplexi.com  total count in 30s =  1  trans_rate = 0.00  addr :                ht@simplexi.com hashsize :     4
2010-04-14 18:21:23 :  reject :           emerson@simplexi.com  total count in 30s =  2  trans_rate = 2.00  addr :           emerson@simplexi.com hashsize :     4
2010-04-14 18:21:24 :  reject :            dsdhks@simplexi.com  total count in 30s =  2  trans_rate = 2.00  addr :            dsdhks@simplexi.com hashsize :     4
2010-04-14 18:21:24 :  reject :               kim@simplexi.com  total count in 30s =  2  trans_rate = 2.00  addr :               kim@simplexi.com hashsize :     4
2010-04-14 18:21:24 :  reject :                ht@simplexi.com  total count in 30s =  2  trans_rate = 2.00  addr :                ht@simplexi.com hashsize :     4
2010-04-14 18:21:26 :  accept :            dsdhks@simplexi.com  total count in 30s =  3  trans_rate = 1.00  addr :            dsdhks@simplexi.com hashsize :     4
2010-04-14 18:21:26 :  accept :               kim@simplexi.com  total count in 30s =  3  trans_rate = 1.00  addr :               kim@simplexi.com hashsize :     4
2010-04-14 18:21:26 :  accept :                ht@simplexi.com  total count in 30s =  3  trans_rate = 1.00  addr :                ht@simplexi.com hashsize :     4
2010-04-14 18:21:26 :  accept :           emerson@simplexi.com  total count in 30s =  3  trans_rate = 0.75  addr :           emerson@simplexi.com hashsize :     4
2010-04-14 18:21:26 :  reject :            dsdhks@simplexi.com  total count in 30s =  4  trans_rate = 1.33  addr :            dsdhks@simplexi.com hashsize :     4
2010-04-14 18:21:26 :  reject :               kim@simplexi.com  total count in 30s =  4  trans_rate = 1.33  addr :               kim@simplexi.com hashsize :     4
2010-04-14 18:21:26 :  reject :                ht@simplexi.com  total count in 30s =  4  trans_rate = 1.33  addr :                ht@simplexi.com hashsize :     4
2010-04-14 18:21:26 :  accept :           emerson@simplexi.com  total count in 30s =  4  trans_rate = 1.00  addr :           emerson@simplexi.com hashsize :     4
2010-04-14 18:21:26 :  reject :            dsdhks@simplexi.com  total count in 30s =  5  trans_rate = 1.67  addr :            dsdhks@simplexi.com hashsize :     4

Milter API

  • Milter API callback function


 함수명  호출 시점


 connect_callback  클라이언트가 연결되었을 때 호출됨.


 helo_callback  HELO/EHLO 명령이 입력되었을 때 호출됨.


 envfrom_callback 
 MAIL FROM: 명령이 입력되었을 때 호출됨.


 envrcpt_callback  RCPT TO: 명령이 입력되었을 때 호출됨.


 header_callback  헤더가 입력될 때, 각 헤더에 대해 한번씩 호출됨.


 eoh_callback  End Of Header, 즉, 모든 헤더가 다 받아졌을 때 한번 호출됨.


 body_callback  메일 본문이 입력되고 나서 호출됨.


 eom_callback  End Of Message, 즉 모든 메시지가 다 받아졌을 때 호출됨.


 abort_callback  메일 트랜잭션이 중간에 취소 될때 호출됨.


 close_callback  클라이언트 연결이 종료될 때 호출됨.

             
  • Milter API Return Value
 콜백 함수의 반환값  의미
 SMFIS_CONTINUE  메일을 계속 정상적으로 처리하라.
 SMFIS_REJECT  메일을 더 이상 진행하지 말고 명시적으로 거부하라.
 SMFIS_DISCARD  현재 메일을 정상적으로 받아들이되, 아무말없이 그냥 버려라. 즉 송신측은 메일이 정상적으로 보내졌다고 생각하지만, 실제로 메일은 수신자에게 전달되지 않고 그냥 이 순간에 버려져버린다. 다른 밀터에게도 더 이상 메일이 전달되지 않는다.
 SMFIS_ACCEPT  추후의 다른 필터링 조건을 무시하고 무조건 받아들여라.
 SMFIS_TEMPFAIL  일시적 서비스 거부 메세지를 내보내라. 즉, 클라이언트에게 5xx 등의 정상적 메시지가 아닌 4xx 메시지를 내보낸다. 이것은 클라이언트에게 서버가 일시적으로 사용 불가능하므로, 추후 다시 접속할 것을 요구하는 것이다.