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
%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";