Merge tag 'upstream/1.16'

Upstream version 1.16
This commit is contained in:
Jan Wagner 2013-11-05 17:32:50 +01:00
commit af9371f56e
5 changed files with 752 additions and 168 deletions

View file

@ -1,3 +1,28 @@
1.16
=====
- bugfix: this is a bugfix release for 1.15. anyone affected is encouraged to upgrade.
detail: the default behavior for the '=' operator with numeric items
(size, recipient_count, ...) changed with version 1.15 to '==' (equals to).
now these items are compared '>=' (greater than) again.
note: if you are using 1.15 and you are not able upgrade for some reason,
please change '=' to '>=' in your ruleset where you mean 'greater than'.
1.15
=====
- feature: items may now be retrieved from files using "item=file:/some/where"
more information in the postfwd manual (FILES section)
- feature: helo_address, and sender_(ns|mx)_addrs can now be csv items
- feature: new rcpt() command counts recipients for rate limits (thanks to Sahil Tandon)
- code: redirect syslog to stdout for --kill, --reload and --showconfig
- code: option --reload (HUP signal) now reloads config, if the file is unchanged
- code: configuration parser improvements:
* rules without defined action will be skipped at configuration stage
* undefined ACLs will now be detected and skipped at configuration stage
* parser timeout skips loading a rule after 4s, to prevent problems with
large files or loops. use --config_timeout to override
- bugfix: documentation fixed (missing "action=" in ask() examples)
1.14
=====
- feature: new compare operators *

View file

@ -1,10 +1,8 @@
<?xml version="1.0" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>postfwd - postfix firewall daemon</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rev="made" href="mailto:feedback@suse.de" />
<link rev="made" href="mailto:root@localhost" />
</head>
<body style="background-color: white">
@ -22,6 +20,7 @@
<li><a href="#introduction">INTRODUCTION</a></li>
<li><a href="#configuration">CONFIGURATION</a></li>
<li><a href="#items">ITEMS</a></li>
<li><a href="#files">FILES</a></li>
<li><a href="#actions">ACTIONS</a></li>
<li><a href="#macros_acls">MACROS/ACLS</a></li>
<li><a href="#plugins">PLUGINS</a></li>
@ -97,7 +96,8 @@
--dns_timeout_interval interval in seconds for dns timeout maximum counter
--dns_max_ns_lookups max names to look up with sender_ns_addrs
--dns_max_mx_lookups max names to look up with sender_mx_addrs
-I, --instantcfg re-reads rulefiles for every new request</pre>
-I, --instantcfg re-reads rulefiles for every new request
--config_timeout &lt;i&gt; parser timeout in seconds</pre>
<pre>
Informational (use only at command-line!):
-C, --showconfig shows ruleset summary, -v for verbose
@ -148,11 +148,15 @@ is not important. So the following would lead to the same result as the previous
<p>The way how request items are compared to the ruleset can be influenced in the following way:</p>
<pre>
====================================================================
ITEM==VALUE true if ITEM equals VALUE
ITEM&gt;=VALUE true if ITEM &gt;= VALUE
ITEM&lt;=VALUE true if ITEM &lt;= VALUE
ITEM~=VALUE true if ITEM ~= /^VALUE$/i
ITEM=VALUE default behaviour (see ITEMS section)
ITEM == VALUE true if ITEM equals VALUE
ITEM =&gt; VALUE true if ITEM &gt;= VALUE
ITEM =&lt; VALUE true if ITEM &lt;= VALUE
ITEM =~ VALUE true if ITEM ~= /^VALUE$/i
ITEM != VALUE false if ITEM equals VALUE
ITEM !&gt; VALUE false if ITEM &gt;= VALUE
ITEM !&lt; VALUE false if ITEM &lt;= VALUE
ITEM !~ VALUE false if ITEM ~= /^VALUE$/i
ITEM = VALUE default behaviour (see ITEMS section)
====================================================================</pre>
<p>To identify single rules in your log files, you may add an unique identifier for each of it:</p>
<pre>
@ -213,13 +217,19 @@ arguments. Please see the COMMAND LINE section below for more information on thi
recipient_domain</pre>
<pre>
helo_address - postfwd tries to look up the helo_name. use
helo_address=!!(0.0.0.0/0) to check for unknown.</pre>
helo_address=!!(0.0.0.0/0) to check for unknown.
Please do not use this for positive access control
(whitelisting), as it might be forged.</pre>
<pre>
sender_ns_names, - postfwd tries to look up the names/ip addresses
sender_ns_addrs of the nameservers for the sender domain part.</pre>
sender_ns_addrs of the nameservers for the sender domain part.
Please do not use this for positive access control
(whitelisting), as it might be forged.</pre>
<pre>
sender_mx_names, - postfwd tries to look up the names/ip addresses
sender_mx_addrs of the mx records for the sender domain part.</pre>
sender_mx_addrs of the mx records for the sender domain part.
Please do not use this for positive access control
(whitelisting), as it might be forged.</pre>
<pre>
version - postfwd version, contains &quot;postfwd n.nn&quot;
this enables version based checks in your rulesets
@ -298,6 +308,9 @@ Pattern matching is performed case insensitive.</p>
<p>Any item can be negated by preceeding '!!' to it, e.g.:</p>
<pre>
id=TLS001 ; hostname=!!^secure\.trust\.local$ ; action=REJECT only secure.trust.local please</pre>
<p>or using the right compare operator:</p>
<pre>
id=USER01 ; sasl_username !~ /^(bob|alice)$/ ; action=REJECT who is that?</pre>
<p>To avoid confusion with regexps or simply for better visibility you can use '!!(...)':</p>
<pre>
id=USER01 ; sasl_username=!!( (bob|alice) ) ; action=REJECT who is that?</pre>
@ -310,6 +323,78 @@ Pattern matching is performed case insensitive.</p>
Use the '-vv' option to debug.</p>
<p>
</p>
<h2><a name="files">FILES</a></h2>
<p>Since postfwd1 v1.15 and postfwd2 v0.18 long item lists can be stored in separate files:</p>
<pre>
id=R001 ; ccert_fingerprint==<a href="file:/etc/postfwd/wl_ccerts">file:/etc/postfwd/wl_ccerts</a> ; action=DUNNO</pre>
<p>postfwd will read a list of items (one item per line) from /etc/postfwd/wl_ccerts. comments are allowed:</p>
<pre>
# client1
11:22:33:44:55:66:77:88:99
# client2
22:33:44:55:66:77:88:99:00
# client3
33:44:55:66:77:88:99:00:11</pre>
<p>To use existing tables in key=value format, you can use:</p>
<pre>
id=R001 ; ccert_fingerprint==table:/etc/postfwd/wl_ccerts ; action=DUNNO</pre>
<p>This will ignore the right-hand value. Items can be mixed:</p>
<pre>
id=R002 ; action=REJECT \
client_name==unknown; \
client_name==<a href="file:/etc/postfwd/blacklisted">file:/etc/postfwd/blacklisted</a></pre>
<p>and for non pcre (comma separated) items:</p>
<pre>
id=R003 ; action=REJECT \
client_address==10.1.1.1, <a href="file:/etc/postfwd/blacklisted">file:/etc/postfwd/blacklisted</a></pre>
<pre>
id=R004 ; action=REJECT \
rbl=myrbl.home.local, zen.spamhaus.org, <a href="file:/etc/postfwd/rbls_changing">file:/etc/postfwd/rbls_changing</a></pre>
<p>You can check your configuration with the --show_config option at the command line:</p>
<pre>
# postfwd --showconfig --rule='action=DUNNO; client_address=10.1.0.0/16, <a href="file:/etc/postfwd/wl_clients">file:/etc/postfwd/wl_clients</a>, 192.168.2.1'</pre>
<p>should give something like:</p>
<pre>
Rule 0: id-&gt;&quot;R-0&quot;; action-&gt;&quot;DUNNO&quot;; client_address-&gt;&quot;=;10.1.0.0/16, =;194.123.86.10, =;186.4.6.12, =;192.168.2.1&quot;</pre>
<p>If a file can not be read, it will be ignored:</p>
<pre>
# postfwd --showconfig --rule='action=DUNNO; client_address=10.1.0.0/16, <a href="file:/etc/postfwd/wl_clients">file:/etc/postfwd/wl_clients</a>, 192.168.2.1'
[LOG warning]: error: file /etc/postfwd/wl_clients not found - file will be ignored ?
Rule 0: id-&gt;&quot;R-0&quot;; action-&gt;&quot;DUNNO&quot;; client_address-&gt;&quot;=;10.1.0.0/16, =;192.168.2.1&quot;</pre>
<p>File items are evaluated at configuration stage. Therefore postfwd needs to be reloaded if a file has changed.</p>
<p>If you want to specify a file, that will be reloaded for each request, you can use lfile: and ltable:</p>
<pre>
id=R001; client_address=lfile:/etc/postfwd/client_whitelist; action=dunno</pre>
<p>This will check the modification time of /etc/postfwd/client_whitelist every time the rule is evaluated and reload it as
necessary. Of course this might increase the system load, so please use it with care.</p>
<p>The --showconfig option illustrates the difference:</p>
<pre>
## evaluated at configuration stage
# postfwd2 --nodaemon -L --rule='client_address=table:/etc/postfwd/clients; action=dunno' -C
Rule 0: id-&gt;&quot;R-0&quot;; action-&gt;&quot;dunno&quot;; client_address-&gt;&quot;=;1.1.1.1, =;1.1.1.2, =;1.1.1.3&quot;</pre>
<pre>
## evaluated for any rulehit
# postfwd2 --nodaemon -L --rule='client_address=ltable:/etc/postfwd/clients; action=dunno' -C
Rule 0: id-&gt;&quot;R-0&quot;; action-&gt;&quot;dunno&quot;; client_address-&gt;&quot;=;ltable:/etc/postfwd/clients&quot;</pre>
<p>Files can refer to other files. The following is valid.</p>
<pre>
-- FILE /etc/postfwd/rules.cf --
id=R001; client_address=<a href="file:/etc/postfwd/clients_master.cf">file:/etc/postfwd/clients_master.cf</a>; action=DUNNO</pre>
<pre>
-- FILE /etc/postfwd/clients_master.cf --
192.168.1.0/24
<a href="file:/etc/postfwd/clients_east.cf">file:/etc/postfwd/clients_east.cf</a>
<a href="file:/etc/postfwd/clients_west.cf">file:/etc/postfwd/clients_west.cf</a></pre>
<pre>
-- FILE /etc/postfwd/clients_east.cf --
192.168.2.0/24</pre>
<pre>
-- FILE /etc/postfwd/clients_west.cf --
192.168.3.0/24</pre>
<p>Remind that there is currently no loop detection (/a/file calls /a/file) and that this feature is only available
with postfwd1 v1.15 and postfwd2 v0.18 and higher.</p>
<p>
</p>
<h2><a name="actions">ACTIONS</a></h2>
<p><em>General</em></p>
<p>Actions will be executed, when all rule items have matched a request (or at least one of any item list). You can refer to
@ -370,16 +455,25 @@ rule containing only an action statement:</p>
# size limit 1.5mb per hour per client
id=SIZE01 ; state==END_OF_DATA ; client_address==!!(10.1.1.1); \
action==size($$client_address/1572864/3600/450 4.7.1 sorry, max 1.5mb per hour)</pre>
<pre>
rcpt (&lt;item&gt;/&lt;max&gt;/&lt;time&gt;/&lt;action&gt;)
this command works similar to the rate() command with the difference, 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 ; state==END_OF_DATA ; client_address==!!(10.1.1.1); \
action==rcpt($$client_address/3/3600/450 4.7.1 sorry, max 3 recipients per hour)</pre>
<pre>
ask (&lt;addr&gt;:&lt;port&gt;[:&lt;ignore&gt;])
allows to delegate the policy decision to another policy service (e.g. postgrey). the first
and the second argument (address and port) are mandatory. a third optional argument may be
specified to tell postfwd to ignore certain answers and go on parsing the ruleset:
# example1: query postgrey and return it's answer to postfix
id=GREY; client_address==10.1.1.1; ask(127.0.0.1:10031)
id=GREY; client_address==10.1.1.1; action=ask(127.0.0.1:10031)
# example2: query postgrey but ignore it's answer, if it matches 'DUNNO'
# and continue parsing postfwd's ruleset
id=GREY; client_address==10.1.1.1; ask(127.0.0.1:10031:^dunno$)</pre>
id=GREY; client_address==10.1.1.1; action=ask(127.0.0.1:10031:^dunno$)</pre>
<pre>
wait (&lt;delay&gt;)
pauses the program execution for &lt;delay&gt; seconds. use this for
@ -650,6 +744,10 @@ The following arguments will control it's behaviour in this case.</p>
without restarting. Though files will be read only if necessary
(which means their access times changed since last read) this might
significantly increase system load.</pre>
<pre>
--config_timeout (default=3)
timeout in seconds to parse a single configuration line. if exceeded, the rule will
be skipped. this is used to prevent problems due to large files or loops.</pre>
<p><em>Informational arguments</em></p>
<p>These arguments are for command line usage only. Never ever use them with postfix spawn!</p>
<pre>

View file

@ -51,6 +51,7 @@ SYNOPSIS
--dns_max_ns_lookups max names to look up with sender_ns_addrs
--dns_max_mx_lookups max names to look up with sender_mx_addrs
-I, --instantcfg re-reads rulefiles for every new request
--config_timeout <i> parser timeout in seconds
Informational (use only at command-line!):
-C, --showconfig shows ruleset summary, -v for verbose
@ -115,11 +116,15 @@ DESCRIPTION
in the following way:
====================================================================
ITEM==VALUE true if ITEM equals VALUE
ITEM>=VALUE true if ITEM >= VALUE
ITEM<=VALUE true if ITEM <= VALUE
ITEM~=VALUE true if ITEM ~= /^VALUE$/i
ITEM=VALUE default behaviour (see ITEMS section)
ITEM == VALUE true if ITEM equals VALUE
ITEM => VALUE true if ITEM >= VALUE
ITEM =< VALUE true if ITEM <= VALUE
ITEM =~ VALUE true if ITEM ~= /^VALUE$/i
ITEM != VALUE false if ITEM equals VALUE
ITEM !> VALUE false if ITEM >= VALUE
ITEM !< VALUE false if ITEM <= VALUE
ITEM !~ VALUE false if ITEM ~= /^VALUE$/i
ITEM = VALUE default behaviour (see ITEMS section)
====================================================================
To identify single rules in your log files, you may add an unique
@ -187,12 +192,18 @@ DESCRIPTION
helo_address - postfwd tries to look up the helo_name. use
helo_address=!!(0.0.0.0/0) to check for unknown.
Please do not use this for positive access control
(whitelisting), as it might be forged.
sender_ns_names, - postfwd tries to look up the names/ip addresses
sender_ns_addrs of the nameservers for the sender domain part.
Please do not use this for positive access control
(whitelisting), as it might be forged.
sender_mx_names, - postfwd tries to look up the names/ip addresses
sender_mx_addrs of the mx records for the sender domain part.
Please do not use this for positive access control
(whitelisting), as it might be forged.
version - postfwd version, contains "postfwd n.nn"
this enables version based checks in your rulesets
@ -284,6 +295,10 @@ DESCRIPTION
id=TLS001 ; hostname=!!^secure\.trust\.local$ ; action=REJECT only secure.trust.local please
or using the right compare operator:
id=USER01 ; sasl_username !~ /^(bob|alice)$/ ; action=REJECT who is that?
To avoid confusion with regexps or simply for better visibility you can
use '!!(...)':
@ -299,6 +314,97 @@ DESCRIPTION
be performed as case insensitive exact match. Use the '-vv' option to
debug.
FILES
Since postfwd1 v1.15 and postfwd2 v0.18 long item lists can be stored in
separate files:
id=R001 ; ccert_fingerprint==file:/etc/postfwd/wl_ccerts ; action=DUNNO
postfwd will read a list of items (one item per line) from
/etc/postfwd/wl_ccerts. comments are allowed:
# client1
11:22:33:44:55:66:77:88:99
# client2
22:33:44:55:66:77:88:99:00
# client3
33:44:55:66:77:88:99:00:11
To use existing tables in key=value format, you can use:
id=R001 ; ccert_fingerprint==table:/etc/postfwd/wl_ccerts ; action=DUNNO
This will ignore the right-hand value. Items can be mixed:
id=R002 ; action=REJECT \
client_name==unknown; \
client_name==file:/etc/postfwd/blacklisted
and for non pcre (comma separated) items:
id=R003 ; action=REJECT \
client_address==10.1.1.1, file:/etc/postfwd/blacklisted
id=R004 ; action=REJECT \
rbl=myrbl.home.local, zen.spamhaus.org, file:/etc/postfwd/rbls_changing
You can check your configuration with the --show_config option at the
command line:
# postfwd --showconfig --rule='action=DUNNO; client_address=10.1.0.0/16, file:/etc/postfwd/wl_clients, 192.168.2.1'
should give something like:
Rule 0: id->"R-0"; action->"DUNNO"; client_address->"=;10.1.0.0/16, =;194.123.86.10, =;186.4.6.12, =;192.168.2.1"
If a file can not be read, it will be ignored:
# postfwd --showconfig --rule='action=DUNNO; client_address=10.1.0.0/16, file:/etc/postfwd/wl_clients, 192.168.2.1'
[LOG warning]: error: file /etc/postfwd/wl_clients not found - file will be ignored ?
Rule 0: id->"R-0"; action->"DUNNO"; client_address->"=;10.1.0.0/16, =;192.168.2.1"
File items are evaluated at configuration stage. Therefore postfwd needs
to be reloaded if a file has changed.
If you want to specify a file, that will be reloaded for each request,
you can use lfile: and ltable:
id=R001; client_address=lfile:/etc/postfwd/client_whitelist; action=dunno
This will check the modification time of /etc/postfwd/client_whitelist
every time the rule is evaluated and reload it as necessary. Of course
this might increase the system load, so please use it with care.
The --showconfig option illustrates the difference:
## evaluated at configuration stage
# postfwd2 --nodaemon -L --rule='client_address=table:/etc/postfwd/clients; action=dunno' -C
Rule 0: id->"R-0"; action->"dunno"; client_address->"=;1.1.1.1, =;1.1.1.2, =;1.1.1.3"
## evaluated for any rulehit
# postfwd2 --nodaemon -L --rule='client_address=ltable:/etc/postfwd/clients; action=dunno' -C
Rule 0: id->"R-0"; action->"dunno"; client_address->"=;ltable:/etc/postfwd/clients"
Files can refer to other files. The following is valid.
-- FILE /etc/postfwd/rules.cf --
id=R001; client_address=file:/etc/postfwd/clients_master.cf; action=DUNNO
-- FILE /etc/postfwd/clients_master.cf --
192.168.1.0/24
file:/etc/postfwd/clients_east.cf
file:/etc/postfwd/clients_west.cf
-- FILE /etc/postfwd/clients_east.cf --
192.168.2.0/24
-- FILE /etc/postfwd/clients_west.cf --
192.168.3.0/24
Remind that there is currently no loop detection (/a/file calls /a/file)
and that this feature is only available with postfwd1 v1.15 and postfwd2
v0.18 and higher.
ACTIONS
*General*
@ -371,15 +477,24 @@ DESCRIPTION
id=SIZE01 ; state==END_OF_DATA ; client_address==!!(10.1.1.1); \
action==size($$client_address/1572864/3600/450 4.7.1 sorry, max 1.5mb per hour)
rcpt (<item>/<max>/<time>/<action>)
this command works similar to the rate() command with the difference, 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 ; state==END_OF_DATA ; client_address==!!(10.1.1.1); \
action==rcpt($$client_address/3/3600/450 4.7.1 sorry, max 3 recipients per hour)
ask (<addr>:<port>[:<ignore>])
allows to delegate the policy decision to another policy service (e.g. postgrey). the first
and the second argument (address and port) are mandatory. a third optional argument may be
specified to tell postfwd to ignore certain answers and go on parsing the ruleset:
# example1: query postgrey and return it's answer to postfix
id=GREY; client_address==10.1.1.1; ask(127.0.0.1:10031)
id=GREY; client_address==10.1.1.1; action=ask(127.0.0.1:10031)
# example2: query postgrey but ignore it's answer, if it matches 'DUNNO'
# and continue parsing postfwd's ruleset
id=GREY; client_address==10.1.1.1; ask(127.0.0.1:10031:^dunno$)
id=GREY; client_address==10.1.1.1; action=ask(127.0.0.1:10031:^dunno$)
wait (<delay>)
pauses the program execution for <delay> seconds. use this for
@ -671,6 +786,10 @@ DESCRIPTION
(which means their access times changed since last read) this might
significantly increase system load.
--config_timeout (default=3)
timeout in seconds to parse a single configuration line. if exceeded, the rule will
be skipped. this is used to prevent problems due to large files or loops.
*Informational arguments*
These arguments are for command line usage only. Never ever use them

View file

@ -1,4 +1,4 @@
.\" Automatically generated by Pod::Man v1.37, Pod::Parser v1.32
.\" Automatically generated by Pod::Man v1.37, Pod::Parser v1.14
.\"
.\" Standard preamble:
.\" ========================================================================
@ -128,8 +128,8 @@
.rm #[ #] #H #V #F C
.\" ========================================================================
.\"
.IX Title "MANUAL1 8"
.TH MANUAL1 8 "2009-06-27" "perl v5.8.8" "User Contributed Perl Documentation"
.IX Title "POSTFWD 1"
.TH POSTFWD 1 "2009-09-03" "perl v5.8.5" "User Contributed Perl Documentation"
.SH "NAME"
postfwd \- postfix firewall daemon
.SH "SYNOPSIS"
@ -175,7 +175,7 @@ postfwd [\s-1OPTIONS\s0] [\s-1SOURCE1\s0, \s-1SOURCE2\s0, ...]
\& --cleanup-rates cleanup interval in seconds for rate cache
.Ve
.PP
.Vb 16
.Vb 17
\& Optional:
\& -t, --test testing, always returns "dunno"
\& -v, --verbose verbose logging, use twice (-vv) to increase level
@ -192,6 +192,7 @@ postfwd [\s-1OPTIONS\s0] [\s-1SOURCE1\s0, \s-1SOURCE2\s0, ...]
\& --dns_max_ns_lookups max names to look up with sender_ns_addrs
\& --dns_max_mx_lookups max names to look up with sender_mx_addrs
\& -I, --instantcfg re-reads rulefiles for every new request
\& --config_timeout <i> parser timeout in seconds
.Ve
.PP
.Vb 7
@ -261,13 +262,17 @@ is not important. So the following would lead to the same result as the previous
.PP
The way how request items are compared to the ruleset can be influenced in the following way:
.PP
.Vb 7
.Vb 11
\& ====================================================================
\& ITEM==VALUE true if ITEM equals VALUE
\& ITEM>=VALUE true if ITEM >= VALUE
\& ITEM<=VALUE true if ITEM <= VALUE
\& ITEM~=VALUE true if ITEM ~= /^VALUE$/i
\& ITEM=VALUE default behaviour (see ITEMS section)
\& ITEM == VALUE true if ITEM equals VALUE
\& ITEM => VALUE true if ITEM >= VALUE
\& ITEM =< VALUE true if ITEM <= VALUE
\& ITEM =~ VALUE true if ITEM ~= /^VALUE$/i
\& ITEM != VALUE false if ITEM equals VALUE
\& ITEM !> VALUE false if ITEM >= VALUE
\& ITEM !< VALUE false if ITEM <= VALUE
\& ITEM !~ VALUE false if ITEM ~= /^VALUE$/i
\& ITEM = VALUE default behaviour (see ITEMS section)
\& ====================================================================
.Ve
.PP
@ -352,19 +357,25 @@ Rules can span multiple lines by adding a trailing backslash \*(L"\e\*(R" charac
\& recipient_domain
.Ve
.PP
.Vb 2
.Vb 4
\& helo_address - postfwd tries to look up the helo_name. use
\& helo_address=!!(0.0.0.0/0) to check for unknown.
\& Please do not use this for positive access control
\& (whitelisting), as it might be forged.
.Ve
.PP
.Vb 2
.Vb 4
\& sender_ns_names, - postfwd tries to look up the names/ip addresses
\& sender_ns_addrs of the nameservers for the sender domain part.
\& Please do not use this for positive access control
\& (whitelisting), as it might be forged.
.Ve
.PP
.Vb 2
.Vb 4
\& sender_mx_names, - postfwd tries to look up the names/ip addresses
\& sender_mx_addrs of the mx records for the sender domain part.
\& Please do not use this for positive access control
\& (whitelisting), as it might be forged.
.Ve
.PP
.Vb 6
@ -464,6 +475,12 @@ Any item can be negated by preceeding '!!' to it, e.g.:
\& id=TLS001 ; hostname=!!^secure\e.trust\e.local$ ; action=REJECT only secure.trust.local please
.Ve
.PP
or using the right compare operator:
.PP
.Vb 1
\& id=USER01 ; sasl_username !~ /^(bob|alice)$/ ; action=REJECT who is that?
.Ve
.PP
To avoid confusion with regexps or simply for better visibility you can use '!!(...)':
.PP
.Vb 1
@ -480,6 +497,122 @@ Request attributes can be compared by preceeding '$$' characters, e.g.:
.PP
This is only valid for \s-1PCRE\s0 values (see list above). The comparison will be performed as case insensitive exact match.
Use the '\-vv' option to debug.
.Sh "\s-1FILES\s0"
.IX Subsection "FILES"
Since postfwd1 v1.15 and postfwd2 v0.18 long item lists can be stored in separate files:
.PP
.Vb 1
\& id=R001 ; ccert_fingerprint==file:/etc/postfwd/wl_ccerts ; action=DUNNO
.Ve
.PP
postfwd will read a list of items (one item per line) from /etc/postfwd/wl_ccerts. comments are allowed:
.PP
.Vb 6
\& # client1
\& 11:22:33:44:55:66:77:88:99
\& # client2
\& 22:33:44:55:66:77:88:99:00
\& # client3
\& 33:44:55:66:77:88:99:00:11
.Ve
.PP
To use existing tables in key=value format, you can use:
.PP
.Vb 1
\& id=R001 ; ccert_fingerprint==table:/etc/postfwd/wl_ccerts ; action=DUNNO
.Ve
.PP
This will ignore the right-hand value. Items can be mixed:
.PP
.Vb 3
\& id=R002 ; action=REJECT \e
\& client_name==unknown; \e
\& client_name==file:/etc/postfwd/blacklisted
.Ve
.PP
and for non pcre (comma separated) items:
.PP
.Vb 2
\& id=R003 ; action=REJECT \e
\& client_address==10.1.1.1, file:/etc/postfwd/blacklisted
.Ve
.PP
.Vb 2
\& id=R004 ; action=REJECT \e
\& rbl=myrbl.home.local, zen.spamhaus.org, file:/etc/postfwd/rbls_changing
.Ve
.PP
You can check your configuration with the \-\-show_config option at the command line:
.PP
.Vb 1
\& # postfwd --showconfig --rule='action=DUNNO; client_address=10.1.0.0/16, file:/etc/postfwd/wl_clients, 192.168.2.1'
.Ve
.PP
should give something like:
.PP
.Vb 1
\& Rule 0: id->"R-0"; action->"DUNNO"; client_address->"=;10.1.0.0/16, =;194.123.86.10, =;186.4.6.12, =;192.168.2.1"
.Ve
.PP
If a file can not be read, it will be ignored:
.PP
.Vb 3
\& # postfwd --showconfig --rule='action=DUNNO; client_address=10.1.0.0/16, file:/etc/postfwd/wl_clients, 192.168.2.1'
\& [LOG warning]: error: file /etc/postfwd/wl_clients not found - file will be ignored ?
\& Rule 0: id->"R-0"; action->"DUNNO"; client_address->"=;10.1.0.0/16, =;192.168.2.1"
.Ve
.PP
File items are evaluated at configuration stage. Therefore postfwd needs to be reloaded if a file has changed.
.PP
If you want to specify a file, that will be reloaded for each request, you can use lfile: and ltable:
.PP
.Vb 1
\& id=R001; client_address=lfile:/etc/postfwd/client_whitelist; action=dunno
.Ve
.PP
This will check the modification time of /etc/postfwd/client_whitelist every time the rule is evaluated and reload it as
necessary. Of course this might increase the system load, so please use it with care.
.PP
The \-\-showconfig option illustrates the difference:
.PP
.Vb 3
\& ## evaluated at configuration stage
\& # postfwd2 --nodaemon -L --rule='client_address=table:/etc/postfwd/clients; action=dunno' -C
\& Rule 0: id->"R-0"; action->"dunno"; client_address->"=;1.1.1.1, =;1.1.1.2, =;1.1.1.3"
.Ve
.PP
.Vb 3
\& ## evaluated for any rulehit
\& # postfwd2 --nodaemon -L --rule='client_address=ltable:/etc/postfwd/clients; action=dunno' -C
\& Rule 0: id->"R-0"; action->"dunno"; client_address->"=;ltable:/etc/postfwd/clients"
.Ve
.PP
Files can refer to other files. The following is valid.
.PP
.Vb 2
\& -- FILE /etc/postfwd/rules.cf --
\& id=R001; client_address=file:/etc/postfwd/clients_master.cf; action=DUNNO
.Ve
.PP
.Vb 4
\& -- FILE /etc/postfwd/clients_master.cf --
\& 192.168.1.0/24
\& file:/etc/postfwd/clients_east.cf
\& file:/etc/postfwd/clients_west.cf
.Ve
.PP
.Vb 2
\& -- FILE /etc/postfwd/clients_east.cf --
\& 192.168.2.0/24
.Ve
.PP
.Vb 2
\& -- FILE /etc/postfwd/clients_west.cf --
\& 192.168.3.0/24
.Ve
.PP
Remind that there is currently no loop detection (/a/file calls /a/file) and that this feature is only available
with postfwd1 v1.15 and postfwd2 v0.18 and higher.
.Sh "\s-1ACTIONS\s0"
.IX Subsection "ACTIONS"
\&\fIGeneral\fR
@ -562,16 +695,27 @@ postfwd actions control the behaviour of the program. Currently you can specify
\& action==size($$client_address/1572864/3600/450 4.7.1 sorry, max 1.5mb per hour)
.Ve
.PP
.Vb 8
\& rcpt (<item>/<max>/<time>/<action>)
\& this command works similar to the rate() command with the difference, 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 ; state==END_OF_DATA ; client_address==!!(10.1.1.1); \e
\& action==rcpt($$client_address/3/3600/450 4.7.1 sorry, max 3 recipients per hour)
.Ve
.PP
.Vb 9
\& ask (<addr>:<port>[:<ignore>])
\& allows to delegate the policy decision to another policy service (e.g. postgrey). the first
\& and the second argument (address and port) are mandatory. a third optional argument may be
\& specified to tell postfwd to ignore certain answers and go on parsing the ruleset:
\& # example1: query postgrey and return it's answer to postfix
\& id=GREY; client_address==10.1.1.1; ask(127.0.0.1:10031)
\& id=GREY; client_address==10.1.1.1; action=ask(127.0.0.1:10031)
\& # example2: query postgrey but ignore it's answer, if it matches 'DUNNO'
\& # and continue parsing postfwd's ruleset
\& id=GREY; client_address==10.1.1.1; ask(127.0.0.1:10031:^dunno$)
\& id=GREY; client_address==10.1.1.1; action=ask(127.0.0.1:10031:^dunno$)
.Ve
.PP
.Vb 3
@ -962,6 +1106,12 @@ These parameters influence the way postfwd is working. Any of them can be combin
\& significantly increase system load.
.Ve
.PP
.Vb 3
\& --config_timeout (default=3)
\& timeout in seconds to parse a single configuration line. if exceeded, the rule will
\& be skipped. this is used to prevent problems due to large files or loops.
.Ve
.PP
\&\fIInformational arguments\fR
.PP
These arguments are for command line usage only. Never ever use them with postfix spawn!

View file

@ -25,7 +25,7 @@ use vars qw(@ISA);
# Program constants
our($NAME) = 'postfwd';
our($VERSION) = '1.14';
our($VERSION) = '1.16';
# Networking options (use -i, -p and -R to change)
our($def_net_pid) = "/var/run/".$NAME.".pid";
@ -38,6 +38,7 @@ our($def_net_group) = "nobody";
our($def_dns_queuesize) = "300";
our($def_dns_retries) = "3";
our($def_dns_timeout) = "14";
our($def_config_timeout) = "3";
our($reply_maxlen) = "512";
# change this, to match your POD requirements
@ -49,7 +50,7 @@ our($cmd_pager) = "more";
# default action, do not change
# unless you really know why
our($default_action) = "dunno";
our($default_action) = "DUNNO";
# default maximum values for the score() command
# if exceeded, the specified action will be returned
@ -113,6 +114,14 @@ our($COMP_RHSBL_KEY_CLIENT) = "rhsbl_client";
our($COMP_RHSBL_KEY_SENDER) = "rhsbl_sender";
our($COMP_RHSBL_KEY_RCLIENT) = "rhsbl_reverse_client";
our($COMP_RHSBL_KEY_HELO) = "rhsbl_helo";
# file items
our($COMP_CONF_FILE) = 'cfile|file';
our($COMP_CONF_TABLE) = 'ctable|table';
our($COMP_LIVE_FILE) = 'lfile';
our($COMP_LIVE_TABLE) = 'ltable';
our($COMP_TABLES) = qr/^($COMP_CONF_TABLE|$COMP_LIVE_TABLE)$/i;
our($COMP_CONF_FILE_TABLE) = qr/^($COMP_CONF_FILE|$COMP_CONF_TABLE):(.+)$/i;
our($COMP_LIVE_FILE_TABLE) = qr/^($COMP_LIVE_FILE|$COMP_LIVE_TABLE):(.+)$/i;
# date checks
our($COMP_DATE) = "date";
our($COMP_TIME) = "time";
@ -136,7 +145,7 @@ our($COMP_VAR) = "[\$][\$]";
# date calculations
our($COMP_DATECALC) = "($COMP_DATE|$COMP_TIME|$COMP_DAYS|$COMP_MONTHS)";
# these items allow whitespace-or-comma-separated values
our($COMP_CSV) = "($COMP_NETWORK_CIDRS|$COMP_RBL_KEY|$COMP_RHSBL_KEY|$COMP_RHSBL_KEY_CLIENT|$COMP_RHSBL_KEY_HELO|$COMP_RHSBL_KEY_SENDER|$COMP_RHSBL_KEY_RCLIENT|$COMP_DATECALC)";
our($COMP_CSV) = "($COMP_NETWORK_CIDRS|$COMP_RBL_KEY|$COMP_RHSBL_KEY|$COMP_RHSBL_KEY_CLIENT|$COMP_RHSBL_KEY_HELO|$COMP_RHSBL_KEY_SENDER|$COMP_RHSBL_KEY_RCLIENT|$COMP_DATECALC|$COMP_HELO_ADDR|$COMP_NS_ADDR|$COMP_MX_ADDR)";
# dont treat these as lists
our($COMP_SINGLE) = "($COMP_ID|$COMP_ACTION|$COMP_SCORES|$COMP_RBL_CNT|$COMP_RHSBL_CNT)";
@ -148,6 +157,7 @@ our($syslog_options) = "pid";
our($syslog_socktype) = 'unix';
our($syslog_maxlen) = 0;
our($syslog_safe) = 0;
our($syslog_unsafe_charset) = qr/[^\x20-\x7E]/;
if ( defined $Sys::Syslog::VERSION and $Sys::Syslog::VERSION ge '0.15' ) {
# use 'native' when Sys::Syslog >= 0.15
$syslog_socktype = 'native';
@ -203,7 +213,7 @@ use vars qw(
$opt_norulelog $opt_summary $net_interface $net_port
$net_user $net_group $net_chroot $net_pid $net_proto
$opt_perfmon $opt_test $opt_verbose $opt_noidlestats
$opt_cache_rdomain_only $opt_cache_no_size
$opt_cache_rdomain_only $opt_cache_no_size $config_timeout
$opt_cache_no_sender $opt_no_rulestats $opt_kill $opt_hup
$opt_showconfig $opt_stdoutlog $opt_shortlog $dns_async_txt
$DNS $Reload_Conf $dns_queuesize $dns_retries $dns_timeout
@ -239,7 +249,7 @@ sub mylog {
sub mylogs {
my($prio) = shift(@_);
my($msg) = shift(@_);
$msg =~ s/\%/%%/g;
$msg =~ s/\%/%%/g; $msg =~ s/$syslog_unsafe_charset/?/g;
mylog $prio, $msg;
};
#
@ -486,96 +496,160 @@ sub devar_item {
# preparses configuration line for ACL syntax
#
sub acl_parser {
my($myline) = @_;
if ( $myline =~ /^\s*($COMP_ACL[\-\w]+)\s*{\s*(.*?)\s*;\s*}[\s;]*$/ ) {
$ACLs{$1} = $2; $myline = "";
} else {
while ( $myline =~ /($COMP_ACL[\-\w]+)/) {
my($acl) = $1; $myline =~ s/\s*$acl\s*/$ACLs{$acl}/g if exists($ACLs{$acl});
my($file,$num,$myline) = @_;
if ( $myline =~ /^\s*($COMP_ACL[\-\w]+)\s*{\s*(.*?)\s*;\s*}[\s;]*$/ ) {
$ACLs{$1} = $2; $myline = "";
} else {
while ( $myline =~ /($COMP_ACL[\-\w]+)/) {
my($acl) = $1;
if ( $acl and defined $ACLs{$acl} ) {
$myline =~ s/\s*$acl\s*/$ACLs{$acl}/g;
} else {
#return "action=note(undefined macro '$acl')";
mylogs 'warning', "file $file, ignoring line $num: undefined macro '$acl'";
return "";
};
};
};
};
return $myline;
}
return $myline;
};
#
# prepares pcre item
#
sub prepare_pcre {
my($item) = shift; undef my $neg;
# temporarily remove negation
$item = $neg if ($neg = deneg_item($item));
# allow // regex
$item =~ s/^\/?(.*?)\/?$/$1/;
# tested slow
#$item = qr/$item/i;
# re-enable negation
$item = "!!($item)" if $neg;
return $item;
};
#
# prepares file item
#
sub prepare_file {
my($forced_reload,$type,$cmp,$file) = @_; my(@result) = (); undef my $fh;
my($is_table) = ($type =~ /^$COMP_TABLES$/);
unless (-e $file) {
mylogs 'warning', "error: $type:$file not found - will be ignored";
return @result;
};
if ( not($forced_reload) and (defined $Config_Cache{$file}{lastread}) and ($Config_Cache{$file}{lastread} > (stat $file)[9]) ) {
mylogs $syslog_priority, "$type:$file unchanged - using cached content (mtime: "
.(stat $file)[9].", cache: $Config_Cache{$file}{lastread})"
if ($opt_verbose > 1);
return @{$Config_Cache{$file}{content}};
};
unless (open ($fh, "<$file")) {
mylogs 'warning', "error: could not open $type:$file - $! - will be ignored";
return @result;
};
mylogs $syslog_priority, "reading $type:$file" if ($opt_verbose > 1);
while (<$fh>) {
chomp;
s/#.*//g;
next if /^\s*$/;
s/\s+[^\s]+$// if $is_table;
s/^\s+//; s/\s+$//;
push @result, prepare_item($forced_reload, $cmp, $_);
}; close ($fh);
# update Config_Cache
$Config_Cache{$file}{lastread} = time;
@{$Config_Cache{$file}{content}} = @result;
mylogs $syslog_priority, "read ".($#result + 1)." items from $type:$file" if ($opt_verbose > 1);
return @result;
};
#
# prepares ruleset item
#
sub prepare_item {
my($forced_reload,$cmp,$item) = @_; my(@result) = (); undef my $type;
if ($item =~ /$COMP_CONF_FILE_TABLE/) {
return prepare_file ($forced_reload, $1, $cmp, $2);
} elsif ($cmp eq '=~' or $cmp eq '!~') {
return $cmp.";".prepare_pcre($item);
} else {
return $cmp.";".$item;
};
};
#
# parses configuration line
#
sub parse_config_line {
my($mynum, $myindex, $myline) = @_;
my(%myrule) = ();
my($mykey, $myvalue, $mycomp, $neg);
if ( $myline = acl_parser ($myline) ) {
unless ( $myline =~ /^\s*[^=\s]+\s*$COMP_SEPARATOR\s*([^;\s]+\s*)+(;\s*[^=\s]+\s*$COMP_SEPARATOR\s*([^;\s]+\s*)+)*[;\s]*$/ ) {
warn "warning: ignoring invalid line ".$mynum.": \"".$myline."\"";
} else {
# separate items
foreach (split ";", $myline) {
# remove whitespaces around
s/^\s*(.*?)\s*($COMP_SEPARATOR)\s*(.*?)\s*$/$1$2$3/;
( ($mycomp = $2) =~ /^([\<\>\~])=$/ ) and $mycomp = "=$1";
($mykey, $myvalue) = split /$COMP_SEPARATOR/, $_, 2;
if ($mykey =~ /^$COMP_CSV$/) {
$myvalue =~ s/\s*-\s*/-/g if ($mykey =~ /^$COMP_DATECALC$/);
$myvalue =~ s/\s*,\s*/,/g;
map { push ( @{$myrule{$mykey}}, $mycomp.";".$_ ) } ( split ",", $myvalue );
} elsif ($mykey =~ /^$COMP_SINGLE$/) {
mylogs "notice", "warning: Rule $myindex (line $mynum):"
." overriding $mykey=\"".$myrule{$mykey}."\""
." with $mykey=\"$myvalue\""
if (defined $myrule{$mykey});
$myrule{$mykey} = $myvalue;
} else {
if ( $mycomp eq '=~' or $mycomp eq '!~') {
# temporarily remove negation
$myvalue = $neg if ($neg = deneg_item($myvalue));
# allow // regex
$myvalue =~ s/^\/?(.*?)\/?$/$1/;
# tested, slower
#$myvalue = qr/$myvalue/i;
# re-enable negation
$myvalue = "!!($myvalue)" if $neg;
my($forced_reload, $myfile, $mynum, $myindex, $myline) = @_;
my(%myrule) = ();
my($mykey, $myvalue, $mycomp);
eval {
local $SIG{'__DIE__'};
local $SIG{'ALRM'} = sub { $myline =~ s/[ \t][ \t]*/ /g; mylogs 'warning', "timeout after ".$config_timeout."s at parsing Rule $myindex ($myfile line $mynum): \"$myline\""; %myrule = (); die };
my $prevalert = alarm($config_timeout) if $config_timeout;
if ( $myline = acl_parser ($myfile, $mynum, $myline) ) {
unless ( $myline =~ /^\s*[^=\s]+\s*$COMP_SEPARATOR\s*([^;\s]+\s*)+(;\s*[^=\s]+\s*$COMP_SEPARATOR\s*([^;\s]+\s*)+)*[;\s]*$/ ) {
mylogs 'warning', "ignoring invalid $myfile line ".$mynum.": \"".$myline."\"";
} else {
# separate items
foreach (split ";", $myline) {
# remove whitespaces around
s/^\s*(.*?)\s*($COMP_SEPARATOR)\s*(.*?)\s*$/$1$2$3/;
( ($mycomp = $2) =~ /^([\<\>\~])=$/ ) and $mycomp = "=$1";
($mykey, $myvalue) = split /$COMP_SEPARATOR/, $_, 2;
if ($mykey =~ /^$COMP_SINGLE$/) {
mylogs 'notice', "notice: Rule $myindex ($myfile line $mynum):"
." overriding $mykey=\"".$myrule{$mykey}."\""
." with $mykey=\"$myvalue\""
if (defined $myrule{$mykey});
$myrule{$mykey} = $myvalue;
} elsif ($mykey =~ /^$COMP_CSV$/) {
$myvalue =~ s/\s*,\s*/,/g;
map { push @{$myrule{$mykey}}, prepare_item ($forced_reload, $mycomp, $_) } ( split /\s*,\s*/, $myvalue );
} else {
push @{$myrule{$mykey}}, prepare_item ($forced_reload, $mycomp, $myvalue);
};
push ( @{$myrule{$mykey}}, $mycomp.";".$myvalue );
};
unless (exists($myrule{$COMP_ACTION})) {
mylogs 'warning', "Rule ".$myindex." ($myfile line ".$mynum."): contains no action and will be ignored";
return (%myrule = ());
};
unless (exists($myrule{$COMP_ID})) {
$myrule{$COMP_ID} = "R-".$myindex;
mylogs 'notice', "notice: Rule $myindex ($myfile line $mynum): contains no rule identifier - will use \"$myrule{id}\"" if $opt_verbose;
};
mylogs $syslog_priority, "loaded: Rule $myindex ($myfile line $mynum): id->\"$myrule{id}\" action->\"$myrule{action}\"" if $opt_verbose;
};
unless (exists($myrule{$COMP_ACTION})) {
$myrule{$COMP_ACTION} = "WARN rule found but no action was defined";
mylogs "notice", "warning: Rule ".$myindex." (line ".$mynum."): contains no action - default will be used";
};
unless (exists($myrule{$COMP_ID})) {
$myrule{$COMP_ID} = "R-".$myindex;
mylogs "notice", "notice: Rule $myindex (line $mynum): contains no rule identifier - will use \"$myrule{id}\"" if $opt_verbose;
};
mylogs $syslog_priority, "loaded: Rule $myindex (line $mynum): id->\"$myrule{id}\" action->\"$myrule{action}\"" if $opt_verbose;
};
alarm($prevalert) if $config_timeout;
};
};
return %myrule;
}
return %myrule;
};
#
# parses configuration file
#
sub read_config_file {
my($myindex, $myfile) = @_;
my($forced_reload, $myindex, $myfile) = @_;
my(%myrule, @myruleset) = ();
my($mybuffer) = "";
my($mybuffer) = ""; undef my $fh;
unless (-e $myfile) {
warn "error: file ".$myfile." not found - file will be ignored";
} else {
unless (open (IN, "<$myfile")) {
warn "error: could not open ".$myfile." - file will be ignored";
unless (open ($fh, "<$myfile")) {
warn "error: could not open ".$myfile." - $! - file will be ignored";
} else {
mylogs $syslog_priority, "reading file $myfile" if $opt_verbose;
while (<IN>) {
while (<$fh>) {
chomp;
s/(\"|#.*)//g;
next if /^\s*$/;
if (/(.*)\\\s*$/) { $mybuffer = $mybuffer.$1; next; };
%myrule = parse_config_line ($., ($#myruleset+$myindex+1), $mybuffer.$_);
%myrule = parse_config_line ($forced_reload, $myfile, $., ($#myruleset+$myindex+1), $mybuffer.$_);
push ( @myruleset, { %myrule } ) if (%myrule);
$mybuffer = "";
};
close (IN);
close ($fh);
mylogs $syslog_priority, "loaded: Rules $myindex - ".($myindex + $#myruleset)." from file \"$myfile\"" if $opt_verbose;
};
};
@ -585,6 +659,7 @@ sub read_config_file {
# reads all configuration items
#
sub read_config {
my($forced_reload) = shift;
my(%myrule, @myruleset) = ();
my($mytype,$myitem,$config);
@ -595,17 +670,17 @@ sub read_config {
for $config (@Configs) {
($mytype,$myitem) = split '::', $config;
if ($mytype eq "r" or $mytype eq "rule") {
%myrule = parse_config_line (0, ($#Rules + 1), $myitem);
%myrule = parse_config_line ($forced_reload, 'RULE', 0, ($#Rules + 1), $myitem);
push ( @Rules, { %myrule } ) if (%myrule);
} elsif ($mytype eq "f" or $mytype eq "file") {
if ( (defined $Config_Cache{$myitem}{lastread}) and ($Config_Cache{$myitem}{lastread} > (stat $myitem)[9]) ) {
if ( not($forced_reload) and (defined $Config_Cache{$myitem}{lastread}) and ($Config_Cache{$myitem}{lastread} > (stat $myitem)[9]) ) {
mylogs $syslog_priority,
"file \"$myitem\" unchanged - using cached ruleset (mtime: ".(stat $myitem)[9].",
cache: $Config_Cache{$myitem}{lastread})"
if $opt_verbose;
push ( @Rules, @{$Config_Cache{$myitem}{ruleset}} );
} else {
@myruleset = read_config_file (($#Rules+1), $myitem);
@myruleset = read_config_file ($forced_reload, ($#Rules+1), $myitem);
if (@myruleset) {
push ( @Rules, @myruleset );
$Config_Cache{$myitem}{lastread} = time;
@ -614,8 +689,12 @@ sub read_config {
};
};
};
# update Rule by ID hash
map { $Rule_by_ID{$Rules[$_]{$COMP_ID}} = $_ } (0 .. $#Rules);
if ($#Rules < 0) {
mylogs 'warning', "critical: no rules found - i feel useless (have you set -f or -r?)";
} else {
# update Rule by ID hash
map { $Rule_by_ID{$Rules[$_]{$COMP_ID}} = $_ } (0 .. $#Rules);
};
}
#
# displays configuration
@ -931,7 +1010,7 @@ sub postfwd_items {
my($myresult) = undef;
mylogs $syslog_priority, "type numeric : \"$myitem\" \"$cmp\" \"$val\"" if ($opt_verbose > 1);
$myitem ||= "0"; $val ||= "0";
if ( ($cmp eq '==') or ($cmp eq '=') ) {
if ($cmp eq '==') {
$myresult = ($myitem == $val);
} elsif ($cmp eq '=<') {
$myresult = ($myitem <= $val);
@ -968,7 +1047,7 @@ sub postfwd_items {
my($cmp,$val,$myitem,%request) = @_;
my($myresult) = undef;
my($imon) = (split (',', $myitem))[4]; $imon ||= 0;
my($rmin,$rmax) = split ('-', $val);
my($rmin,$rmax) = split (/\s*-\s*/, $val);
$rmin = ($rmin) ? (($rmin =~ /^\d$/) ? $rmin : $months{$rmin}) : $imon;
$rmax = ($rmax) ? (($rmax =~ /^\d$/) ? $rmax : $months{$rmax}) : (($val =~ /-/) ? $imon : $rmin);
mylogs $syslog_priority, "type months : \"$imon\" \"$cmp\" \"$rmin\"-\"$rmax\""
@ -981,7 +1060,7 @@ sub postfwd_items {
my($cmp,$val,$myitem,%request) = @_;
my($myresult) = undef;
my($iday) = (split (',', $myitem))[6]; $iday ||= 0;
my($rmin,$rmax) = split ('-', $val);
my($rmin,$rmax) = split (/\s*-\s*/, $val);
$rmin = ($rmin) ? (($rmin =~ /^\d$/) ? $rmin : $weekdays{$rmin}) : $iday;
$rmax = ($rmax) ? (($rmax =~ /^\d$/) ? $rmax : $weekdays{$rmax}) : (($val =~ /-/) ? $iday : $rmin);
mylogs $syslog_priority, "type days : \"$iday\" \"$cmp\" \"$rmin\"-\"$rmax\""
@ -994,7 +1073,7 @@ sub postfwd_items {
my($cmp,$val,$myitem,%request) = @_;
my($myresult) = undef;
my($isec,$imin,$ihour,$iday,$imon,$iyear) = split (',', $myitem);
my($rmin,$rmax) = split ('-', $val);
my($rmin,$rmax) = split (/\s*-\s*/, $val);
my($idat) = ($iyear + 1900) . ((($imon+1) < 10) ? '0'.($imon+1) : ($imon+1)) . (($iday < 10) ? '0'.$iday : $iday);
$rmin = ($rmin) ? join ('', reverse split ('\.', $rmin)) : $idat;
$rmax = ($rmax) ? join ('', reverse split ('\.', $rmax)) : (($val =~ /-/) ? $idat : $rmin);
@ -1008,7 +1087,7 @@ sub postfwd_items {
my($cmp,$val,$myitem,%request) = @_;
my($myresult) = undef;
my($isec,$imin,$ihour,$iday,$imon,$iyear) = split (',', $myitem);
my($rmin,$rmax) = split ('-', $val);
my($rmin,$rmax) = split (/\s*-\s*/, $val);
my($idat) = (($ihour < 10) ? '0'.$ihour : $ihour) . (($imin < 10) ? '0'.$imin : $imin) . (($isec < 10) ? '0'.$isec : $isec);
$rmin = ($rmin) ? join ('', split ('\:', $rmin)) : $idat;
$rmax = ($rmax) ? join ('', split ('\:', $rmax)) : (($val =~ /-/) ? $idat : $rmin);
@ -1229,7 +1308,7 @@ sub postfwd_items {
type => $mycmd,
maxcount => $ratecount,
ttl => $ratetime,
count => ( ($mycmd eq 'size') ? $request{size} : 1 ),
count => ( ($mycmd eq 'size') ? $request{size} : (($mycmd eq 'rcpt') ? $request{recipient_count} : 1 ) ),
time => $now,
rule => $Rules[$index]{$COMP_ID},
action => $ratecmd,
@ -1246,6 +1325,8 @@ sub postfwd_items {
},
# size() command
"size" => sub { return &{$postfwd_actions{rate}}(@_); },
# rcpt() command
"rcpt" => sub { return &{$postfwd_actions{rate}}(@_); },
# wait() command
"wait" => sub {
my($index,$now,$mycmd,$myarg,$myline,%request) = @_;
@ -1309,7 +1390,7 @@ sub postfwd_items {
mylogs ('notice', "rule: $index got invalid answer '$sendstr' from $myarg");
};
} else {
mylogs ('notice', "Could not open socket to '$myarg'");
mylogs ('notice', "Could not open socket to '$myarg' - $!");
};
return ($stop,$index,$myaction,$myline,%request);
},
@ -1356,19 +1437,24 @@ sub get_plugins {
# use: compare_item ( $TYPE, $RULEITEM, $MINIMUMHITS, $REQUESTITEM, %REQUEST, %REQUESTINFO );
#
sub compare_item {
my($mykey,$mymask,$mymin,$myitem, %request) = @_;
my($mykey,$mymask,$mymin,$myitem,%request) = @_;
my($val,$var,$cmp,$neg,$myresult,$postfwd_compare_proc);
my($rcount) = 0;
$mymin ||= 1;
#
# determine the right compare function
$postfwd_compare_proc = (defined $postfwd_compare{$mykey}) ? $mykey : "default";
#
# save list due to possible modification
my @items = @{$mymask};
# now compare request to every single item
ITEM: foreach (@{$mymask}) {
ITEM: foreach (@items) {
($cmp, $val) = split ";";
next ITEM unless ($cmp and $val and $mykey);
# prepare_file
if ($val =~ /$COMP_LIVE_FILE_TABLE/) {
push @items, prepare_file (0, $1, $cmp, $2);
next ITEM;
};
mylogs $syslog_priority, "compare $mykey: \"$myitem\" \"$cmp\" \"$val\"" if ($opt_verbose > 1);
$val = $neg if ($neg = deneg_item($val));
mylogs $syslog_priority, "deneg $mykey: \"$myitem\" \"$cmp\" \"$val\"" if ($neg and ($opt_verbose > 1));
@ -1695,7 +1781,7 @@ sub smtpd_access_policy {
if ( $Reload_Conf ) {
undef $Reload_Conf;
show_stats;
read_config;
read_config(1);
};
# clear dnsbl timeout counters
@ -1724,7 +1810,8 @@ sub smtpd_access_policy {
next RATES unless ( $request{$checkreq} and (defined $Rates{$request{$checkreq}}) );
if ( ($now - $Rates{$request{$checkreq}}{"time"}) > $Rates{$request{$checkreq}}{ttl} ) {
# renew rate
$Rates{$request{$checkreq}}{count} = ( ($Rates{$request{$checkreq}}{type} eq 'size') ? $request{size} : 1 );
$Rates{$request{$checkreq}}{count} = ( ($Rates{$request{$checkreq}}{type} eq 'size') ? $request{size} :
(($Rates{$request{$checkreq}}{type} eq 'rcpt') ? $request{recipient_count} : 1 ) );
$Rates{$request{$checkreq}}{"time"} = $now;
mylogs $syslog_priority, "[RATE] renewing rate object ".$request{$checkreq}
." [type: ".$Rates{$request{$checkreq}}{type}
@ -1733,7 +1820,8 @@ sub smtpd_access_policy {
if ($opt_verbose > 1);
} else {
# increase rate
$Rates{$request{$checkreq}}{count} += ( ($Rates{$request{$checkreq}}{type} eq 'size') ? $request{size} : 1 );
$Rates{$request{$checkreq}}{count} += ( ($Rates{$request{$checkreq}}{type} eq 'size') ? $request{size} :
(($Rates{$request{$checkreq}}{type} eq 'rcpt') ? $request{recipient_count} : 1 ) );
mylogs $syslog_priority, "[RATE] increasing rate object ".$request{$checkreq}
." to ".$Rates{$request{$checkreq}}{count}
." [type: ".$Rates{$request{$checkreq}}{type}
@ -1829,10 +1917,10 @@ sub smtpd_access_policy {
} else {
# refresh config if '-I' was set
read_config if $opt_instantconfig;
read_config(0) if $opt_instantconfig;
if ($#Rules < 0) {
warn "critical: no rules found - i feel useless (have you set -f or -r?)";
mylogs 'warning', "critical: no rules found - i feel useless (have you set -f or -r?)";
} else {
@ -1918,6 +2006,33 @@ sub smtpd_access_policy {
return $myaction;
};
# process delegation protocol input
sub process_input {
my($client,$msg,$attr) = @_;
# remember argument=value
if ( $msg =~ /^([^=]{1,512})=(.{0,512})/ ) {
$$attr{$1} = $2;
# evaluate request
} elsif ( $msg eq '' ) {
map { mylogs $syslog_priority, "Attribute: $_=$$attr{$_}" } (keys %$attr) if ($opt_verbose > 1);
unless ( (defined $$attr{request}) and ($$attr{request} eq "smtpd_access_policy") ) {
mylogs 'warning', "Ignoring unrecognized request type: '".((defined $$attr{request}) ? substr($$attr{request},0,100) : '')."'";
} else {
my $action = smtpd_access_policy(%$attr) || $default_action;
mylogs $syslog_priority, "Action: $action" if ($opt_verbose > 1);
if ($client) {
print $client ("action=$action\n\n");
} else {
print STDOUT ("action=$action\n\n");
};
%$attr = ();
};
# unknown command
} else {
mylogs 'warning', "Ignoring garbage '".substr($msg, 0, 100)."'";
};
};
#### MAIN ####
@ -1970,6 +2085,7 @@ GetOptions ( "term|kill|stop|k" => \$opt_kill,
'noidlestats' => \$opt_noidlestats,
'no-idlestats' => \$opt_noidlestats,
's|scores=s' => \%opt_scores,
'config_timeout=i' => \$config_timeout,
'f|file=s' => sub{ my($opt,$value) = @_; push (@Configs, $opt.'::'.$value) },
'r|rule=s' => sub{ my($opt,$value) = @_; push (@Configs, $opt.'::'.$value) },
'plugins=s' => \@Plugins,
@ -1983,6 +2099,7 @@ GetOptions ( "term|kill|stop|k" => \$opt_kill,
) or pod2usage (-msg => "\nPlease see \"".$NAME." -m\" for detailed instructions.\n", -verbose => 1);
$opt_verbose = 0 unless $opt_verbose;
$opt_stdoutlog = 1 if ($opt_kill or $opt_hup or $opt_showconfig);
# terminate at -k or --kill
if ($opt_kill) {
@ -2002,7 +2119,7 @@ openlog $syslog_name, $syslog_options, $syslog_facility;
mylogs "notice", $NAME." ".$VERSION." starting" if $opt_daemon;
# read configuration
read_config;
read_config(1);
if ($opt_showconfig) {
show_config;
exit 1;
@ -2041,6 +2158,7 @@ $net_pid ||= $def_net_pid;
$dns_queuesize ||= $def_dns_queuesize;
$dns_retries ||= $def_dns_retries;
$dns_timeout ||= $def_dns_timeout;
$config_timeout ||= $def_config_timeout;
$syslog_name ||= $NAME;
$net_interface = ( $net_interface =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) ? $1 : $def_net_interface;
$net_port = ( $net_port =~ /^(\d+|[-\|\@\/\w. ]+)$/ ) ? $1 : $def_net_port;
@ -2053,6 +2171,7 @@ $dns_queuesize = ( $dns_queuesize =~ /^(\d+)$/ ) ? $1 : $dns_queuesize;
$dns_retries = ( $dns_retries =~ /^(\d+)$/ ) ? $1 : $dns_retries;
$dns_timeout = ( $dns_timeout =~ /^(\d+)$/ ) ? $1 : $dns_timeout;
$syslog_name = ( $syslog_name =~ /^(.+)$/ ) ? $1 : $NAME;
$config_timeout = ( $config_timeout =~ /^(\d+)$/ ) ? $1 : $def_config_timeout;
# Unbuffer standard output.
select((select(STDOUT), $| = 1)[0]);
@ -2144,26 +2263,7 @@ if ($opt_daemon) {
# check request line and print output
next unless defined $1;
$request = $1;
if ($request =~ /([^=]+)=(.*)/) {
$myattr{substr($1, 0, 512)} = substr($2, 0, 512);
} elsif ($request eq '') {
if ($opt_verbose > 1) {
for (keys %myattr) {
mylogs $syslog_priority, "Client: $client Attribute: $_=$myattr{$_}";
};
};
unless ( (defined $myattr{request}) and ($myattr{request} eq "smtpd_access_policy") ) {
warn "ignoring unrecognized request type: '".($myattr{request} || '')."'";
} else {
my($action) = substr ( smtpd_access_policy(%myattr), 0, $reply_maxlen ) if $reply_maxlen;
mylogs $syslog_priority, "Client: $client Action: $action" if $opt_verbose;
print $client "action=$action\n\n";
$Counter_Requests++; $Counter_Interval++;
};
} else {
chop $request if $request;
warn "error: ignoring garbage".( ($opt_verbose) ? " from $client" : "")." \"".$request."\"";
};
process_input ($client, $request, \%myattr);
};
};
@ -2178,26 +2278,7 @@ if ($opt_daemon) {
s/^([^\r\n]*)\r?\n//;
next unless defined $1;
$request = $1;
if ($request =~ /([^=]+)=(.*)/) {
$myattr{substr($1, 0, 512)} = substr($2, 0, 512);
} elsif ($request eq '') {
if ($opt_verbose > 1) {
for (keys %myattr) {
mylogs $syslog_priority, "Attribute: $_=$myattr{$_}";
};
};
unless ( (defined $myattr{request}) and ($myattr{request} eq "smtpd_access_policy") ) {
warn "ignoring unrecognized request type: '".($myattr{request} || '')."'";
} else {
my($action) = substr ( smtpd_access_policy(%myattr), 0, $reply_maxlen ) if $reply_maxlen;
mylogs $syslog_priority, "Action: $action" if $opt_verbose;
myprint "action=$action\n\n";
$Counter_Requests++; $Counter_Interval++;
};
} else {
chop $request if $request;
warn "error: ignoring garbage \"".$request."\"";
};
process_input (undef, $request, \%myattr);
};
# finishing
@ -2264,6 +2345,7 @@ postfwd [OPTIONS] [SOURCE1, SOURCE2, ...]
--dns_max_ns_lookups max names to look up with sender_ns_addrs
--dns_max_mx_lookups max names to look up with sender_mx_addrs
-I, --instantcfg re-reads rulefiles for every new request
--config_timeout <i> parser timeout in seconds
Informational (use only at command-line!):
-C, --showconfig shows ruleset summary, -v for verbose
@ -2402,12 +2484,18 @@ Rules can span multiple lines by adding a trailing backslash "\" character:
helo_address - postfwd tries to look up the helo_name. use
helo_address=!!(0.0.0.0/0) to check for unknown.
Please do not use this for positive access control
(whitelisting), as it might be forged.
sender_ns_names, - postfwd tries to look up the names/ip addresses
sender_ns_addrs of the nameservers for the sender domain part.
Please do not use this for positive access control
(whitelisting), as it might be forged.
sender_mx_names, - postfwd tries to look up the names/ip addresses
sender_mx_addrs of the mx records for the sender domain part.
Please do not use this for positive access control
(whitelisting), as it might be forged.
version - postfwd version, contains "postfwd n.nn"
this enables version based checks in your rulesets
@ -2494,6 +2582,10 @@ Any item can be negated by preceeding '!!' to it, e.g.:
id=TLS001 ; hostname=!!^secure\.trust\.local$ ; action=REJECT only secure.trust.local please
or using the right compare operator:
id=USER01 ; sasl_username !~ /^(bob|alice)$/ ; action=REJECT who is that?
To avoid confusion with regexps or simply for better visibility you can use '!!(...)':
id=USER01 ; sasl_username=!!( (bob|alice) ) ; action=REJECT who is that?
@ -2508,6 +2600,92 @@ This is only valid for PCRE values (see list above). The comparison will be perf
Use the '-vv' option to debug.
=head2 FILES
Since postfwd1 v1.15 and postfwd2 v0.18 long item lists can be stored in separate files:
id=R001 ; ccert_fingerprint==file:/etc/postfwd/wl_ccerts ; action=DUNNO
postfwd will read a list of items (one item per line) from /etc/postfwd/wl_ccerts. comments are allowed:
# client1
11:22:33:44:55:66:77:88:99
# client2
22:33:44:55:66:77:88:99:00
# client3
33:44:55:66:77:88:99:00:11
To use existing tables in key=value format, you can use:
id=R001 ; ccert_fingerprint==table:/etc/postfwd/wl_ccerts ; action=DUNNO
This will ignore the right-hand value. Items can be mixed:
id=R002 ; action=REJECT \
client_name==unknown; \
client_name==file:/etc/postfwd/blacklisted
and for non pcre (comma separated) items:
id=R003 ; action=REJECT \
client_address==10.1.1.1, file:/etc/postfwd/blacklisted
id=R004 ; action=REJECT \
rbl=myrbl.home.local, zen.spamhaus.org, file:/etc/postfwd/rbls_changing
You can check your configuration with the --show_config option at the command line:
# postfwd --showconfig --rule='action=DUNNO; client_address=10.1.0.0/16, file:/etc/postfwd/wl_clients, 192.168.2.1'
should give something like:
Rule 0: id->"R-0"; action->"DUNNO"; client_address->"=;10.1.0.0/16, =;194.123.86.10, =;186.4.6.12, =;192.168.2.1"
If a file can not be read, it will be ignored:
# postfwd --showconfig --rule='action=DUNNO; client_address=10.1.0.0/16, file:/etc/postfwd/wl_clients, 192.168.2.1'
[LOG warning]: error: file /etc/postfwd/wl_clients not found - file will be ignored ?
Rule 0: id->"R-0"; action->"DUNNO"; client_address->"=;10.1.0.0/16, =;192.168.2.1"
File items are evaluated at configuration stage. Therefore postfwd needs to be reloaded if a file has changed.
If you want to specify a file, that will be reloaded for each request, you can use lfile: and ltable:
id=R001; client_address=lfile:/etc/postfwd/client_whitelist; action=dunno
This will check the modification time of /etc/postfwd/client_whitelist every time the rule is evaluated and reload it as
necessary. Of course this might increase the system load, so please use it with care.
The --showconfig option illustrates the difference:
## evaluated at configuration stage
# postfwd2 --nodaemon -L --rule='client_address=table:/etc/postfwd/clients; action=dunno' -C
Rule 0: id->"R-0"; action->"dunno"; client_address->"=;1.1.1.1, =;1.1.1.2, =;1.1.1.3"
## evaluated for any rulehit
# postfwd2 --nodaemon -L --rule='client_address=ltable:/etc/postfwd/clients; action=dunno' -C
Rule 0: id->"R-0"; action->"dunno"; client_address->"=;ltable:/etc/postfwd/clients"
Files can refer to other files. The following is valid.
-- FILE /etc/postfwd/rules.cf --
id=R001; client_address=file:/etc/postfwd/clients_master.cf; action=DUNNO
-- FILE /etc/postfwd/clients_master.cf --
192.168.1.0/24
file:/etc/postfwd/clients_east.cf
file:/etc/postfwd/clients_west.cf
-- FILE /etc/postfwd/clients_east.cf --
192.168.2.0/24
-- FILE /etc/postfwd/clients_west.cf --
192.168.3.0/24
Remind that there is currently no loop detection (/a/file calls /a/file) and that this feature is only available
with postfwd1 v1.15 and postfwd2 v0.18 and higher.
=head2 ACTIONS
I<General>
@ -2576,15 +2754,24 @@ postfwd actions control the behaviour of the program. Currently you can specify
id=SIZE01 ; state==END_OF_DATA ; client_address==!!(10.1.1.1); \
action==size($$client_address/1572864/3600/450 4.7.1 sorry, max 1.5mb per hour)
rcpt (<item>/<max>/<time>/<action>)
this command works similar to the rate() command with the difference, 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 ; state==END_OF_DATA ; client_address==!!(10.1.1.1); \
action==rcpt($$client_address/3/3600/450 4.7.1 sorry, max 3 recipients per hour)
ask (<addr>:<port>[:<ignore>])
allows to delegate the policy decision to another policy service (e.g. postgrey). the first
and the second argument (address and port) are mandatory. a third optional argument may be
specified to tell postfwd to ignore certain answers and go on parsing the ruleset:
# example1: query postgrey and return it's answer to postfix
id=GREY; client_address==10.1.1.1; ask(127.0.0.1:10031)
id=GREY; client_address==10.1.1.1; action=ask(127.0.0.1:10031)
# example2: query postgrey but ignore it's answer, if it matches 'DUNNO'
# and continue parsing postfwd's ruleset
id=GREY; client_address==10.1.1.1; ask(127.0.0.1:10031:^dunno$)
id=GREY; client_address==10.1.1.1; action=ask(127.0.0.1:10031:^dunno$)
wait (<delay>)
pauses the program execution for <delay> seconds. use this for
@ -2876,6 +3063,11 @@ These parameters influence the way postfwd is working. Any of them can be combin
(which means their access times changed since last read) this might
significantly increase system load.
--config_timeout (default=3)
timeout in seconds to parse a single configuration line. if exceeded, the rule will
be skipped. this is used to prevent problems due to large files or loops.
I<Informational arguments>
These arguments are for command line usage only. Never ever use them with postfix spawn!