postfwd_v1

 

 


Install [Version 1]

 

準備

yum install -y epel-release

# Centos6: yum install -y perl-Unix-Syslog

# Centos7: yum install -y perl-Sys-Syslog

yum install -y perl-prefork perl-MooseX-Daemonize perl-IO-Multiplex \
perl-Time-HiRes perl-Net-CIDR-Lite perl-Net-DNS perl-Net-Server

Install

cd /usr/src

wget https://postfwd.org/postfwd-1.39.tar.gz

tar -zxf postfwd*.tar.gz

cd postfwd

cp bin/postfwd-script.sh /etc/init.d/postfwd   # 除了 start/stop 外其他都有 Bug

cp sbin/postfwd /usr/sbin

cp etc/postfwd.cf /etc/postfwd.cf

cp tools/postfwd-client.pl /usr/bin/postfwd-client

cp man/man8/postfwd.8 /usr/share/man/man8

Version & Help

# -m, --manual                shows program manual
# -h, --help                     shows usage
# -V, --version

postfwd -V                       # 在 Centos7 上 output

postfwd 1.39 (Net::DNS 0.72, Net::Server 2.007, Sys::Syslog 0.33, Time::HiRes 1.9725, 
              Storable 2.45, Perl 5.016003 on linux)

postfwd -m

...

 


Launch postfwd

 

Create a dedicated user/group for postfwd

# By default postfwd will try to use user 'nobody' and group 'nobody'.

useradd -r -d /var/run/postfwd -s /bin/false -c "postfwd daemon user" postfwd -m

passwd -l postfwd

vim /etc/init.d/postfwd

PATH=/bin:/usr/bin:/usr/sbin

PFWCMD=/usr/sbin/postfwd
PFWCFG=/etc/postfwd.cf
PFWPID=/var/run/postfwd/postfwd.pid

PFWUSER=postfwd
PFWGROUP=postfwd

...

# Run manual

# -d --daemon, -f --file <cf>

postfwd -d -f /etc/postfwd.cf -u postfwd -g postfwd

測試 Configure

# -C, --showconfig                    shows ruleset summary, -v for verbose

postfwd -f /etc/postfwd.cf -C

Rule   0: id->"DEFAULT"; action->"DUNNO"

Start

/etc/init.d/postfwd start

測試 Daemon

# By postfwd-client

postfwd-client 127.0.0.1:10040

parent process waiting for 1 pids 2609
CHILD-2609: OK: answer from 127.0.0.1:10040 -> 'dunno'
parent process finished after 0.71 seconds.
1 requests, 0 errors, 1 valid, 0 invalid, 0.20s total time, 4.92 requests per second

Auto Start

chmod +x /etc/rc.d/rc.local

/etc/rc.local

/etc/init.d/postfwd start &> /dev/null
...

Useful Opts

--reload                         # reloads configuration

--keep_rates                  # do not clear rate limit counters on reload

                                    # 第一次 startup 時已經要有此設定, 否則伙效 !!

reload

postfwd --reload  --pidfile /var/run/postfwd/postfwd.pid

log

Dec 12 12:37:39 myserver postfwd[1993]: catched HUP signal - 
reloading ruleset on next request

--save_rates <file>        # save and load rate limits on disk

-S, --summary <int>   

# 定期在 log file 寫統計資料, default=600

# This feature uses the alarm signal ( kill -ALRM `pidof postfwd`)

May  8 17:14:36 mail postfwd[6302]: [STATS] postfwd 1.39: up since 0 days, 00:00:59 hours
May  8 17:14:36 mail postfwd[6302]: [STATS] Requests: 4 overall, 4 last interval, 0.0% cache hits, 25.0% rate hits
May  8 17:14:36 mail postfwd[6302]: [STATS] Averages: 4.1 overall, 0.4 last interval, 0.4 top
May  8 17:14:36 mail postfwd[6302]: [STATS] Contents: 1 rules, 0 cached requests, 0 cached dns results, 1 rate limits
May  8 17:14:36 mail postfwd[6302]: [STATS]   4 matches for id:  RULE003_limit_mail_per_hour
May  8 17:14:36 mail postfwd[6302]: [STATS]   3 matches for id:  DEFAULT

當有 mail 進入時, 會有

... hits=RULE003_limit_mail_per_hour;DEFAULT, action=DUNNO

所以愈頂的 rule hit 中次數會愈多

-n, --nodns

            Disables all DNS based checks like RBL checks. Rules containing such elements will be ignored.

 


Postfix Configure

 

To place the postfwd check here, modify this as follows:

# note the leading whitespaces from the 2nd line!
smtpd_recipient_restrictions =
  check_policy_service inet:[127.0.0.1]:10040,  # 必須在 permit_* 之前, 因為 permit 左就 check 唔到
  permit_mynetworks,
  permit_sasl_authenticated,
  reject_unauth_destination,
  reject_rbl_client bl.spamcop.net

 * 'size' attribute 要在 smtpd_end_of_data_restrictions 設定

P.S.

1) 如果在 /etc/postfix/master.cf 有 smtpd_recipient_restrictions 設定, 那在 main.cf 的就會唔 work

submission inet n       -       n       -       -       smtpd
  -o smtpd_tls_security_level=may
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_recipient_restrictions=permit_mynetworks,check_policy_service,inet:[127.0.0.1]:10040,permit_sasl_authenticated,reject

2) 在此設定下, webmail 是透過 127.0.0.1 去發信, 所以 count 唔到數量

  permit_mynetworks,
  check_policy_service inet:[127.0.0.1]:10040,
  permit_sasl_authenticated,
  reject_unauth_destination,

 



Rule

 

Format

[ <item1>=<value>; <item2>=<value>; ... ] action=<result>

If a rule matches, there are two options:

* Rule returns postfix action (dunno, reject, ...)

* Rule returns postfwd action (jump(), note(), ...)

* Default 所有 rule 都會 proccess, 任何達標的都會生效 !!

postfwd --dumpcache | grep postmaster

%rate_cache -> %sasl_username=... -> %1_limit_user_outgoing_mail+10_86400 -> $count    -> '10'
%rate_cache -> %sasl_username=... -> %1_limit_user_outgoing_mail+10_86400 -> $maxcount -> '10'
%rate_cache -> %sasl_username=... -> %1_limit_user_outgoing_mail+10_86400 -> $rule     -> '1_limit_user_outgoing_mail'
%rate_cache -> %sasl_username=... -> %1_limit_user_outgoing_mail+10_86400 -> $time     -> '1576126120.76129'
%rate_cache -> %sasl_username=... -> %1_limit_user_outgoing_mail+10_86400 -> $ttl      -> '86400'
%rate_cache -> %sasl_username=... -> %1_limit_user_outgoing_mail+10_86400 -> $type     -> 'rate'
%rate_cache -> %sasl_username=... -> %1_limit_user_outgoing_mail+10_86400 -> $until    -> '1576212520.76129'
%rate_cache -> %sasl_username=... -> %limit_outgoing_mail+400_86400 -> $action   -> 
                  'REJECT limit of 400 mail per day. exceeded [$$sasl_username - $$ratecount hits]'
%rate_cache -> %sasl_username=... -> %limit_outgoing_mail+400_86400 -> $count    -> '10'
%rate_cache -> %sasl_username=... -> %limit_outgoing_mail+400_86400 -> $maxcount -> '400'
%rate_cache -> %sasl_username=... -> %limit_outgoing_mail+400_86400 -> $rule     -> 'limit_outgoing_mail'
%rate_cache -> %sasl_username=... -> %limit_outgoing_mail+400_86400 -> $time     -> '1576126120.76129'
%rate_cache -> %sasl_username=... -> %limit_outgoing_mail+400_86400 -> $ttl      -> '86400'
%rate_cache -> %sasl_username=... -> %limit_outgoing_mail+400_86400 -> $type     -> 'rate'
%rate_cache -> %sasl_username=... -> %limit_outgoing_mail+400_86400 -> $until    -> '1576212520.76129'

Order

# The order of the elements is not important.

i.e. This will deny all mail from 192.168.1.1 with envelope sender [email protected].

action=REJECT ; client_address=192.168.1.1 ; [email protected]      # SAME
client_address=192.168.1.1 ; [email protected] ; action=REJECT      # SAME

no rule has matched

If no rule has matched and the end of the ruleset is reached

=> postfwd will return dunno without logging anything unless in verbose mode.

Compared to the ruleset

ITEM == VALUE                # true if ITEM equals VALUE

ITEM != VALUE                 # false if ITEM equals VALUE

ITEM =~ VALUE                # true if ITEM ~= /^VALUE$/i

ITEM =  VALUE                  # 設定內置的 ITEM

Catch All Rule

You may simply place a last "catch-all" rule to change that behaviour:

... <your rules> ...
id=DEFAULT ;  action=dunno

will log any request that passes the ruleset without having hit a prior rule.

Sep 23 17:55:51 vm postfwd[2674]: [RULES] rule=3, id=DEFAULT, 
client=y.y.y[x.x.x.x], user=tim, sender=<[email protected]>, 
recipient=<[email protected]>, helo=<[192.168.88.150]>, proto=ESMTP,
state=RCPT, delay=0.00s, hits=R01;DEFAULT, action=dunno

說明

id

A unique rule id, which can be used for log analysis. It also serve as targets for the "jump" command.

 


IP & Sender

 

# Block a sender from a IP

id=R_001 ; action=REJECT ; client_address=192.168.1.1 ; [email protected]

# Since postfwd version 1.30 rules spanning span multiple lines

id=RULE001
  client_address=192.168.1.0/24
  [email protected]
  action=REJECT no access

 


ratecount (rate,rcpt,size)

 

 * service postfwd restart 後會重新計過

 * only available for rate(), size() and rcpt() actions

rate(<item>/<max>/<time>/<action>)

This command creates a counter for the given <item>, which will be increased any time a request containing it arrives.

if it exceeds <max> within <time> seconds it will return <action> to postfix.

# no more than 3 requests per 5 minutes from the same "unknown" client

id=RATE01 ;  client_name==unknown
    action=rate(client_address/3/300/450 4.7.1 sorry, max 3 requests per 5 minutes)

# Please note also that the order of rate limits in your ruleset is important, which means that this:
# works as expected

id=R001; action=rcpt(sender/500/3600/REJECT limit of 500 recipients per hour for sender $$sender exceeded)
id=R002; action=rcpt(sender/200/3600/WARN state YELLOW for sender $$sender)

# leads to different results than this: rule R002 never gets executed (錯誤次序)

id=R001; action=rcpt(sender/200/3600/WARN state YELLOW for sender $$sender)
id=R002; action=rcpt(sender/500/3600/REJECT limit of 500 recipients per hour for sender $$sender exceeded)

rcpt(<item>/<max>/<time>/<action>)

that the rate counter is increased by the request's recipient_count attribute.

to do this reliably you should call postfwd from smtpd_data_restrictions or smtpd_end_of_data_restrictions.

if you want to be sure, you could check it within the ruleset:

# recipient count limit 3 per hour per client

id=RCPT01 ;  protocol_state==END-OF-MESSAGE ;  client_address!=10.1.1.1
action=rcpt(client_address/3/3600/450 4.7.1 sorry, max 3 recipients per hour)

client_address=<a.b.c.d/nn>       # mask = CIDR[,CIDR,...]
[email protected]                     # mask = PCRE
[email protected]                  # mask = PCRE

size(<item>/<max>/<time>/<action>)

that the rate counter is increased by the request's size attribute.

to do this reliably you should call postfwd from smtpd_end_of_data_restrictions

id=R01; action=rate(sender/200/600/REJECT limit of 200 exceeded [$$ratecount hits])
id=R02; action=rate(sender/100/600/WARN limit of 100 exceeded [$$ratecount hits])

 


SASL

 

Limit user sent 3 mail per hour

rule

id=RULE003_limit_mail_per_hour
  sasl_username=~/^(\S+)$/
  action=rate(sasl_username/3/3600/REJECT limit of 3 mail per hour exceeded [$$sasl_username - $$ratecount hits])

id=DEFAULT; action=DUNNO

 * 必須加上 "sasl_username=~/^(\S+)$/", 否則正常收信都會 count 落去.

%rate_cache -> %sasl_username=                     -> @list      -> 'RULE001_limit_mail_per_hour+60_3600'
...
%rate_cache -> %[email protected] -> @list      -> 'RULE001_limit_mail_per_hour+60_3600'
...
%rate_cache -> %[email protected] -> @list      -> 'RULE001_limit_mail_per_hour+60_3600'
...

P.S.

  • \S : Non-space character.
  • + : One or more.

仍收到信時的 server log

May  8 17:02:17 mail postfwd[5887]: [RULES] rule=0, id=RULE003_limit_mail_per_hour, 
 client=mail.datahunter.org[127.0.0.1], 
 [email protected], 
 sender=<[email protected]>, 
 recipient=<[email protected]>, 
 helo=<mail.datahunter.org>, proto=ESMTP, 
 state=RCPT, rate=rate/4/103.49s, delay=0.00s, 
 hits=RULE003_mail_per_hour, 
 action=REJECT limit of 3 mail per hour exceeded [[email protected] - 4 hits]

reject 時的 sender log

smtplib.SMTPRecipientsRefused: {'[email protected]': (554, '5.7.1 <[email protected]>: 
 Recipient address rejected: limit of 3 mail per hour exceeded [[email protected] - 4 hits]')}

 * 當 RULE003 成立後, 要過 3600 - 103.49s 後才可以出信 (那 limit 會被 delete, 之後又可以出 limit 數量的信) 詳見

Limit Volume

# Do not allow more than 100 mail or 1GB per day for users alice and bob:

id=RULE003_mail_per_hour
  sasl_username=~/^(alice|bob)$/
  action=rate(sasl_username/100/86400/REJECT limit of 100 mail in a day exceeded [$$sasl_username - $$ratecount hits] )

id=RULE004_size_per_day
  sasl_username=~/^(alice|bob)$/
  action=size(sasl_username/1073741824/86400/REJECT only 20mb per day for $$recipient)

id=DEFAULT; action=DUNNO

 


Whitelists - Macros

 

Multiple use of long items or combinations of them may be abbreviated by macros.

&&TRUSTED_NETS {
        client_address=192.168.1.0/22
        client_address=172.16.128.32/27
}
&&TRUSTED_USERS {
        sasl_username==postmaster@mydomain
        sasl_username==admin@mydomain
}

# Whitelists
id=WL_001
    &&TRUSTED_NETS
    action=dunno
id=WL_002
    &&TRUSTED_USERS
    action=dunno

 


Status

 

postfwd --dumpstats

[STATS]  postfwd 1.35: up since 0 days, 00:02:17 hours
[STATS]  Requests: 14 overall, 14 last interval, 0.0% cache hits, 57.1% rate hits
[STATS]  Averages: 6.1 overall, 1.4 last interval, 1.4 top
[STATS]  Contents: 4 rules, 0 cached requests, 0 cached dns results, 1 rate limits
[STATS]    14 matches for id:  R02
[STATS]     6 matches for id:  DEFAULT

postfwd --dumpcache

%rate_cache -> %[email protected] -> @list                              
 -> 'RULE003_limit_mail_per_hour+4_600'
%rate_cache -> %[email protected] -> %RULE003_limit_mail_per_hour+4_600 -> $action  
 -> 'REJECT limit of 4 mail per hour exceeded [$$sasl_username - $$ratecount hits]'
%rate_cache -> %[email protected] -> %RULE003_limit_mail_per_hour+4_600 -> $count
 -> '4'
%rate_cache -> %[email protected] -> %RULE003_limit_mail_per_hour+4_600 -> $maxcount
 -> '4'
%rate_cache -> %[email protected] -> %RULE003_limit_mail_per_hour+4_600 -> $rule
 -> 'RULE003_limit_mail_per_hour'
%rate_cache -> %[email protected] -> %RULE003_limit_mail_per_hour+4_600 -> $time
 -> '1557307982.47915'
%rate_cache -> %[email protected] -> %RULE003_limit_mail_per_hour+4_600 -> $ttl
 -> '600'
%rate_cache -> %[email protected] -> %RULE003_limit_mail_per_hour+4_600 -> $type
 -> 'rate'
%rate_cache -> %[email protected] -> %RULE003_limit_mail_per_hour+4_600 -> $until
 -> '1557308582.47915'
%config_cache -> %/etc/postfwd.cf -> $lastread -> '1557307967.18951'
%config_cache -> %/etc/postfwd.cf -> @ruleset  -> 'HASH(0xa0b1be8),HASH(0xa0d0080)'
%match_cache -> $DEFAULT                     -> '4'
%match_cache -> $RULE003_limit_mail_per_hour -> '4'

$until - $time = $ttl

Cleanup Cache

  • --delcache <item>        removes an item from the request cache
  • --delrate <item>         removes an item from the rate cache

 


獨立 Log File

 

/etc/rc.d/init.d/postfwd

PFWARG=" ... --facility local5"

/etc/rsyslog.conf

local5.*                                                -/var/log/postfwd

service rsyslog restart

vi /etc/logrotate.d/syslog

/var/log/postfwd
............

other log opts

--shortlog

-S, --summary <int>          show some usage statistics every <int> seconds

 


Bypass Rule - Jump

 

##################################################### macros
&&TRUSTED_USERS {
        [email protected]
        [email protected]
}

##################################################### trusted users jump. bypass default user rule
id=R001; &&TRUSTED_USERS; action=jump(R101)

##################################################### default user 400 mail/day
id=R011
  sasl_username=~/^(\S+)$/
  action=rate(sasl_username/400/86400/REJECT limit of 400 mail per day. exceeded [$$sasl_username - $$ratecount hits])

##################################################### trust user 1000 mails/day
id=R101; &&TRUSTED_USERS
  action=rate(sasl_username/1000/86400/REJECT limit of 1000 mail per day. exceeded [$$sasl_username - $$ratecount hits])

##################################################### default
# default: log the request and finish
id=DEFAULT; action=DUNNO

 


Integration via xinetd

 

 * 用 xinetd 時, ratecount 會唔 work

vim /etc/services

# postfwd port
postfwd     10040/tcp

# install

yum install -y xinetd

/etc/xinetd.d/postfwd

service postfwd
{
        interface       = 127.0.0.1
        socket_type     = stream
        protocol        = tcp
        wait            = no
        user            = nobody
        server          = /usr/sbin/postfwd
        server_args     = -f /etc/postfwd.cf
        disable         = no
}

systemctl enable xinetd

systemctl start xinetd

 


Bounce mail

 

Your message did not reach some or all of the intended recipients.

      Subject:    test@1801
      Sent:    6/10/2019 6:02 PM

The following recipient(s) cannot be reached:

      'tim.lau' on 6/10/2019 6:02 PM
            Server error: '554 5.7.1 <r@r>: Recipient address rejected: 
                  limit of 30 mail per 10 min. exceeded [s@s - 31 hits]'

 


Outbound Statistics

 

#!/bin/bash

# output to nginx the 'statistics' folder
msg=/usr/share/nginx/html/statistics/index.html

# filter out low usage user
quota=1

#### html header
cat > $msg << EOF
<html>
<meta http-equiv="refresh" content="10; url=/statistics"/>
</head>
<pre>`date` (update every 5 min.)
==================================
EOF

#### checking
/usr/sbin/postfwd --dumpcache |\
awk \
'$7 == "$count"{ \
        sub(/%sasl_username=/,"",$3); \
        gsub(/[^0-9]/,"",$9); \
        if (int($9) >= '$quota'){printf $3 " " $9}}\
 $7 == "$maxcount"{ \
        gsub(/[^0-9]/,"",$9); \
        {print "/"$9}}
' >> $msg

#### html footer
echo '<pre></html>' >> $msg

mon spam

 

[方法 1] mail-outbound-warming.sh

#!/bin/bash

[email protected]
LOGFILE=/var/log/maillog
MSG=/tmp/mail-outbound-warming.txt

tail -1000 $LOGFILE | grep -m 2 'action=REJECT' > $MSG
if [[ $? -eq 0 ]]; then
        md5sum -c $MSG.md5 &> /dev/null
        if [[ $? -ne 0 ]]; then
                md5sum $MSG > $MSG.md5
                echo "Sent alarm mail to $ADMIN"
                mail -s 'sent out over limit' $ADMIN < $MSG
        else
                echo "No new alarm"
        fi
else
        echo "No new threat"
fi

[方法 2] mail-outbound-warming.sh

#!/bin/bash

msg=/tmp/mail-outbound-warming.txt
[email protected]

# 必須要是 $maxcount
quota=100

################

/usr/sbin/postfwd --dumpcache |\
awk '($7 == "$count") \
{ sub(/%sasl_username=/,"",$3); \
gsub(/[^0-9]/,"",$9); \
if (int($9) >= '$quota'){print $3 " " $9}}' \
> $msg

if [ -s $msg ]; then
        echo 'Over Usage' | mail -s 'Over Usage' -a $msg $admin
fi

 


限制某 IP 的來信數量

 

# Client IP Message Rate Limits

id=ip_msg_30sec
  action=rate(client_address/180/30/REJECT: $$client_address: too many messages.)
id=ip_msg_1hr
  action=rate(client_address/1800/3600/REJECT: $$client_address: too many messages.)
id=ip_msg_4hr
  action=rate(client_address/3600/14400/REJECT: $$client_address: too many messages.)

 


Internal Users

 

假設有 forwarding:

[email protected]  -> [email protected]
[email protected] -> [email protected]

rule

&&INTERNAL_USERS {
        [email protected]
        [email protected]
}

id=R010; &&INTERNAL_USERS;              \
  sasl_username !~ /^(\[email protected])$/; \
  action=REJECT "Block by admin";