POSTFWD3(1)
postfwd3 โ postfix firewall daemon ๐
๐ธ NAME ๐ธ
postfwd3 โ postfix firewall daemon. That's it. That's the whole personality. ๐
โจ SYNOPSIS โจ
Okay bestie, here's how you summon the daemon:
postfwd3 [OPTIONS] [COMMAND]
postfwd3 [OPTIONS] --cmd [SOURCE1, SOURCE2, ...]
๐ Ruleset: (at least one, multiple use is allowed):
-f,--file <file>โ reads rules from<file>-r,--rule <rule>โ adds<rule>to config-s,--scores <v>=<r>โ returns<r>when score exceeds<v>
๐ Settings: (multiple use allowed)
-F,--loadsettings <file>โ loads program settings from<file>--savesettings <file>โ saves program settings to<file>--showsettingsโ exports program settings to stdout
๐ Server:
-i,--interface <dev>โ listen on interface<dev>-p,--port <port>โ listen on port<port>--proto <proto>โ socket type (tcporunix)--server_socket <sock>โ e.g.tcp:127.0.0.1:10045-u,--user <name>โ set uid to user<name>-g,--group <name>โ set gid to group<name>--umask <mask>โ umask for master filepermissions--server_umask <mask>โ umask for server filepermissions--pidfile <path>โ create pidfile under<path>--min_servers <i>โ spawn at least<i>children--max_servers <i>โ do not spawn more than<i>children--min_spare_servers <i>โ minimum idle children--max_spare_servers <i>โ maximum idle children
๐ Cache:
-c,--cache <int>โ sets the request-cache timeout to<int>seconds--cleanup-requests <int>โ cleanup interval in seconds for request cache--cache_interface <dev>โ listen on interface<dev>--cache_port <port>โ listen on port<port>--cache_proto <proto>โ socket type (tcporunix)--cache_socket <sock>โ e.g.tcp:127.0.0.1:10043--cache_umask <mask>โ umask for cache filepermissions--cacheid <list>โ list of request items for cache-id--cacheid_md5โ cacheid =>md5sum(request)--cache-rdomain-onlyโ skip recipient localpart for cache-id--cache-no-senderโ skip sender address for cache-id--cache-no-sizeโ skip size for cache-id--no_parent_request_cacheโ disable parent request cache--no_parent_rate_cacheโ disable parent rate cache--no_parent_dns_cacheโ disable parent dns cache (default)--no_parent_group_cacheโ disable parent group cache--no_parent_cacheโ disable all parent caches
๐ Groups:
--default_group_ttl <i>โ default group TTL--group_maxitems <i>โ max items per group--cleanup-groups <int>โ cleanup interval in seconds for group objects
๐ Rates:
--cleanup-rates <int>โ cleanup interval in seconds for rate cache
๐ Control:
-k,--kill,--stopโ terminatepostfwd3--reload,--hupโ reloadpostfwd3--watchdog <w>โ watchdog timer in seconds--respawn <r>โ respawn delay in seconds--failures <f>โ max respawn failure counter-d,--daemonโ execute program in background--nodaemonโ execute program in foreground--daemons <list>โ list of daemons to start--personality <type>โ type of policy server, allows'PreFork'or'Multiplex'--autopersonalityโ determine personality by program name (see manpage)--noautopersonalityโ don't do it (see above :)--v1โ set personality to'Multiplex'--v2โ set personality to'PreFork'--dumpcacheโ show cache contents--dumpstatsโ show statistics-R,--chroot <path>โ chroot to<path>before start--delcache <item>โ removes an item from the request cache--delrate <item>โ removes an item from the rate cache
๐ DNS:
-n,--nodnsโ skip any dns based test--dns_timeout <i>โ dns query timeout in seconds--dns_timeout_max <i>โ disable dnsbl after<i>timeouts--dns_timeout_interval <i>โ reenable dnsbl after<i>seconds--cache-rbl-timeout <i>โ default dns ttl if not specified in ruleset--cache-rbl-default <s>โ default dns pattern if not specified in ruleset--cleanup-rbls <i>โ cleanup old dns cache items every<i>seconds--dns_async_txtโ perform dnsbl A and TXT lookups simultaneously--dns_max_ns_lookupsโ max names to look up withsender_ns_addrs--dns_max_mx_lookupsโ max names to look up withsender_mx_addrs--ipv6_dnsblโ enables dnsbl checks for IPv6 addresses
๐ Optional:
-t,--testโ testing, always returns"dunno"-S,--summary <i>โ show stats every<i>seconds--noidlestatsโ disables statistics when idle--norulestatsโ disables per rule statistics-I,--instantcfgโ reloads ruleset on every new request--config_timeout <i>โ parser timeout in seconds--keep_groupsโ do not clear group cache on reload--save_groups <file>โ save and load group cache on disk--keep_ratesโ do not clear rate limit counters on reload--save_rates <file>โ save and load rate limits on disk-A,--aggregate_addrsโ pre-compute ip address lists to subnets--no_netaddrโ don't useNetAddr::IPfunctions--no_netcidrโ don't useNet::CIDR::Litefunctions--cidr_method=sโ use method<s>for network checks
๐ Plugins:
--plugins <file>โ loads postfwd plugins from file
๐ Logging:
-l,--logname <label>โ label for syslog messages--facility <s>โ use syslog facility<s>--socktype <s>โ use syslog socktype<s>--nodnslogโ do not log dns results--anydnslogโ log any dns (even cached) results--norulelogโ do not log rule actions--nolog|--perfmonโ no logging at all-v,--verboseโ verbose logging, use twice to increase--debug <s>โ list of debugging classes--debugclassesโ shows all available debug classes and exits the program
๐ Information (use only at command-line!):
-h,--helpโ display this help and exit-m,--manualโ shows program manual-V,--versionโ output version information and exit-D,--defaultsโ showpostfwd3settings and exit-C,--showconfigโ showpostfwd3ruleset and exit (-vallowed)-L,--stdoutโ redirect syslog messages to stdout (--stdoutlogworks for compatibility)--stdinโ pull request from stdin instead of a network socket--cmdโ shorthand form of--stdin,--stdoutand--nodaemon-q,--quietโ no syslogging, no stdout (-Pworks for compatibility)
๐ Commands:
startโ start the program [default]stopโ same as--stopreloadโ same as--reloaddumpcacheโ same as--dumpcachedumpstatsโ same as--dumpstatsdefaultsโ same as--defaultsshowsettingsโ same as--showsettingsdelcache <item>โ same as--delcache <item>delrate <item>โ same as--delrate <item>
๐ Obsolete (only for compatibility with postfwd v1):
--shortlog,--dns_queuesize,--dns_retries
๐ DESCRIPTION ๐
๐ซ INTRODUCTION ๐ซ
postfwd3 is written to combine complex postfix restrictions in a ruleset similar to those of the most firewalls. The program uses the postfix policy delegation protocol to control access to the mail system before a message has been accepted (please visit <http://www.postfix.org/SMTPD_POLICY_README.html> for more information).
postfwd3 allows you to choose an action (e.g. reject, dunno) for a combination of several smtp parameters (like sender and recipient address, size or the client's TLS fingerprint). Also it offers simple macros/acls which should allow straightforward and easy-to-read configurations.
Features:
- Complex combinations of smtp parameters
- Combined RBL/RHSBL lookups with arbitrary actions depending on results
- Scoring system
- Date/time based rules
- Macros/ACLs, Dynamic groups, Negation
- Compare request attributes (e.g.
client_nameandhelo_name) - Internal caching for requests and dns lookups
- Built in statistics for rule efficiency analysis
๐ซ CONFIGURATION ๐ซ
A configuration line consists of optional item=value pairs, separated by semicolons (;) and the appropriate desired action:
[ <item1>=<value>; <item2>=<value>; ... ] action=<result>
Example:
client_address=192.168.1.1 ; sender==no@bad.local ; action=REJECT
This will deny all mail from 192.168.1.1 with envelope sender no@bad.local. The order of the elements is not important. So the following would lead to the same result as the previous example:
action=REJECT ; client_address=192.168.1.1 ; sender==no@bad.local
The way how request items are compared to the ruleset can be influenced in the following way:
okay so before we go any further, here's the โจvibe-check tableโจ of comparison operators, because this is literally the grammar that runs the whole show:
====================================================================
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
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)
====================================================================
So like, to actually find your own rules again in your log files (iykyk), bestie, you can slap a unique identifier on every single one of them:
id=R_001 ; action=REJECT ; client_address=192.168.1.1 ; sender==no@bad.local
You can totally use those identifiers as the target for the jump() command (the ACTIONS section below has the tea on that, don't worry). Leading or trailing whitespace characters? Ignored. And use '#' to comment your config. Others will appreciate it. Fr fr, they will. ๐ซถ
A ruleset is just one or a whole bunch of rules, and you can load them from files or pass them as command line arguments. The COMMAND LINE section below spills all of that. ๐
Okay so since postfwd version 1.30, rules spanning span multiple lines can be defined by prefixing the following lines with one or multiple whitespace characters (or '}' for macros):
id=RULE001
client_address=192.168.1.0/24
sender==no@bad.local
action=REJECT no access
But postfwd versions prior to 1.30 require trailing ';' and '\'-characters, so if you're on the old stuff:
id=RULE001; \
client_address=192.168.1.0/24; \
sender==no@bad.local; \
action=REJECT no access
โจ ITEMS โจ
Here's the roster, queen by queen:
idโ a unique rule id, which can be used for log analysis. ids also serve as targets for the"jump"command.date,timeโ a time or date range within the specified rule shall hit
# FORMAT:
# Feb, 29th
date=29.02.2008
# Dec, 24th - 26th
date=24.12.2008-26.12.2008
# from today until Nov, 23rd
date=-23.09.2008
# from April, 1st until today
date=01.04.2008-
days,monthsโ a range of weekdays (Sun-Sat) or months (Jan-Dec) within the specified rule shall hit.scoreโ when the specified score is hit (peep the ACTIONS section) the specified action will be returned to postfix. Heads up: scores are set global until redefined! ๐request_scoreโ this value allows to access a request's score. it may be used as variable ($$request_score).rbl,rhsbl,rhsbl_client,rhsbl_sender,rhsbl_reverse_clientโ query the specified RBLs/RHSBLs, possible values are:<name>[/<reply>/<maxcache>, <name>/<reply>/<maxcache>](defaults:reply=^127\.0\.0\.\d+$maxcache=3600) the results of allrhsbl_*queries will be combined inrhsbl_count(see below).rblcount,rhsblcountโ minimum RBL/RHSBL hitcounts to match. if not specified a single RBL/RHSBL hit will match the rbl/rhsbl items. you may specify'all'to evaluate all items, and use it as variable in an action (see ACTIONS section) (default:1)sender_localpart,sender_domainโ the local-/domainpart of the sender addressrecipient_localpart,recipient_domainโ the local-/domainpart of the recipient addresshelo_addressโ postfwd3 tries to look up thehelo_name. usehelo_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,sender_ns_addrsโ postfwd3 tries to look up the names/ip addresses 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,sender_mx_addrsโ postfwd3 tries to look up the names/ip addresses 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โ postfwd3 version, contains"postfwd3 n.nn"this enables version based checks in your rulesets (e.g. for migration). works with old versions too, because a non-existing item always returns false:
# version >= 1.10
id=R01; version~=1\.[1-9][0-9]; sender_domain==some.org \
; action=REJECT sorry no access
postfwd_portโ postfwd server port, allows to use the same ruleset different instances:
# rule only hits for instance at port 10045
id=PORT10045; postfwd_port==10045; action=DUNNO
postfwd_interfaceโ postfwd server initerface (yes bestie, the manpage really spells it that way, we don't make the rules ๐ญ), seepostfwd_portratecountโ only available forrate(),size()andrcpt()actions. contains the actual limit counter:
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])
Besides these you can specify any attribute of the postfix policy delegation protocol. Feel free to combine them the way you need it (have a look at the EXAMPLES section below, it eats ๐ ).
Most values can be specified as regular expressions (PCRE). Please see the table below for details:
# ==========================================================
# ITEM=VALUE TYPE
# ==========================================================
id=something mask = string
date=01.04.2007-22.04.2007 mask = date (DD.MM.YYYY-DD.MM.YYYY)
time=08:30:00-17:00:00 mask = time (HH:MM:SS-HH:MM:SS)
days=Mon-Wed mask = weekdays (Mon-Wed) or numeric (1-3)
months=Feb-Apr mask = months (Feb-Apr) or numeric (1-3)
score=5.0 mask = maximum floating point value
rbl=zen.spamhaus.org mask = <name>/<reply>/<maxcache>[,...]
rblcount=2 mask = numeric, will match if rbl hits >= 2
helo_address=<a.b.c.d/nn> mask = CIDR[,CIDR,...]
sender_ns_names=some.domain.tld mask = PCRE
sender_mx_names=some.domain.tld mask = PCRE
sender_ns_addrs=<a.b.c.d/nn> mask = CIDR[,CIDR,...]
sender_mx_addrs=<a.b.c.d/nn> mask = CIDR[,CIDR,...]
# ------------------------------
# Postfix version 2.1 and later:
# ------------------------------
client_address=<a.b.c.d/nn> mask = CIDR[,CIDR,...]
client_name=another.domain.tld mask = PCRE
reverse_client_name=another.domain.tld mask = PCRE
helo_name=some.domain.tld mask = PCRE
sender=foo@bar.tld mask = PCRE
recipient=bar@foo.tld mask = PCRE
recipient_count=5 mask = numeric, will match if recipients >= 5
# ------------------------------
# Postfix version 2.2 and later:
# ------------------------------
sasl_method=plain mask = PCRE
sasl_username=you mask = PCRE
sasl_sender= mask = PCRE
size=12345 mask = numeric, will match if size >= 12345
ccert_subject=blackhole.nowhere.local mask = PCRE (only if tls verified)
ccert_issuer=John+20Doe mask = PCRE (only if tls verified)
ccert_fingerprint=AA:BB:CC:DD:EE:... mask = PCRE (do NOT use "..." here)
# ------------------------------
# Postfix version 2.3 and later:
# ------------------------------
encryption_protocol=TLSv1/SSLv3 mask = PCRE
encryption_cipher=DHE-RSA-AES256-SHA mask = PCRE
encryption_keysize=256 mask = numeric, will match if keysize >= 256
...
the current list can be found at <http://www.postfix.org/SMTPD_POLICY_README.html>. Please read carefully about which attribute can be used at which level of the smtp transaction (e.g. size will only work reliably at END-OF-MESSAGE level). Pattern matching is performed case insensitive. โจ
Multiple use of the same item is allowed and will compared as logical OR, which means that this will work as expected:
id=TRUST001; action=OK; encryption_keysize=64
ccert_fingerprint=11:22:33:44:55:66:77:88:99
ccert_fingerprint=22:33:44:55:66:77:88:99:00
ccert_fingerprint=33:44:55:66:77:88:99:00:11
sender=@domain\.local$
client_address, rbl and rhsbl items may also be specified as whitespace-or-comma-separated values:
id=SKIP01; action=dunno
client_address=192.168.1.0/24, 172.16.254.23
id=SKIP02; action=dunno
client_address= 10.10.3.32 10.216.222.0/27
The following items must be unique:
id, minimum and maximum values, rblcount and rhsblcount
Any item can be negated by preceeding '!!' to it, e.g.:
...and if you're like "wait, what does !! even do", bestie, it's the inverted match. So this:
id=HOST001 ; hostname == !!secure.trust.local ; action=REJECT only secure.trust.local please
is the same energy as using the right compare operator instead:
id=HOST001 ; hostname != secure.trust.local ; action=REJECT only secure.trust.local please
And to avoid confusion with regexps, or literally just for better visibility, you can wrap it in !!(...) like this:
id=USER01 ; sasl_username =~ !!( /^(bob|alice)$/ ) ; action=REJECT who is that?
Request attributes can be compared by preceeding $$ characters, e.g.:
id=R-003 ; client_name = !! $$helo_name ; action=WARN helo does not match DNS
# or
id=R-003 ; client_name = !!($$(helo_name)) ; action=WARN helo does not match DNS
Heads up: this is only valid for PCRE values (see the list above, bestie). The comparison will be performed as case insensitive exact match. Use the -vv option to debug. ๐
Now the special items that get reset for any new rule ๐ซง:
rblcountโ contains the number of RBL answersrhsblcountโ contains the number of RHSBL answersmatchesโ contains the number of matched itemsdnsbltextโ contains the dns TXT part of all RBL and RHSBL replies in the formrbltype:rblname:<txt>; rbltype:rblname:<txt>; ...
And these special items get changed for any matching rule โจ:
request_hitsโ contains ids of all matching rules
Which basically means it might be necessary to save them, if you plan to use these values in later rules. Like, if you don't save them, they're gone, gone, gone ๐:
# set vals
id=RBL01 ; rhsblcount=all; rblcount=all
action=set(HIT_rhls=$$rhsblcount,HIT_rbls=$$rblcount,HIT_txt=$$dnsbltext)
rbl=list.dsbl.org, bl.spamcop.net, dnsbl.sorbs.net, zen.spamhaus.org
rhsbl_client=rddn.dnsbl.net.au, rhsbl.ahbl.org, rhsbl.sorbs.net
rhsbl_sender=rddn.dnsbl.net.au, rhsbl.ahbl.org, rhsbl.sorbs.net
# compare
id=RBL02 ; HIT_rhls>=1 ; HIT_rbls>=1 ; action=554 5.7.1 blocked using $$HIT_rhls RHSBLs and $$HIT_rbls RBLs [INFO: $$HIT_txt]
id=RBL03 ; HIT_rhls>=2 ; action=554 5.7.1 blocked using $$HIT_rhls RHSBLs [INFO: $$HIT_txt]
id=RBL04 ; HIT_rbls>=2 ; action=554 5.7.1 blocked using $$HIT_rbls RBLs [INFO: $$HIT_txt]
โจ FILES โจ
Since postfwd1 v1.15 and postfwd3 v0.18, long item lists can be stored in separate files ๐๏ธ:
id=R001 ; ccert_fingerprint==file:/etc/postfwd/wl_ccerts ; action=DUNNO
So postfwd3 will read a list of items (one item per line) from /etc/postfwd/wl_ccerts. And yes, 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
If you wanna use existing tables in key=value format, you can do:
id=R001 ; ccert_fingerprint==table:/etc/postfwd/wl_ccerts ; action=DUNNO
Note: this will ignore the right-hand value.
And the items can be mixed together, no problem ๐ :
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 vibe-check your configuration with the --show_config option at the command line ๐:
# postfwd3 --showconfig --rule='action=DUNNO; client_address=10.1.0.0/16, file:/etc/postfwd/wl_clients, 192.168.2.1'
and it should give you 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 ๐:
# postfwd3 --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. Which means postfwd3 needs to be reloaded if a file has changed. Periodt.
But wait โ 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, watch this:
## evaluated at configuration stage
# postfwd3 --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
# postfwd3 --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"
Oh and โ plot twist โ files can refer to other files ๐ฆ. The following is valid:
-- FILE /etc/postfwd/rules.cf --
id=R01; 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
BIG caveat bestie: there is currently no loop detection (/a/filecalls/a/file) ๐ and this feature is only available with postfwd1 v1.15 and postfwd3 v0.18 and higher.
๐ ACTIONS ๐
General
Actions will be executed when all rule items have matched a request (or at least one of any item list). And you can refer to request attributes by preceeding $$ characters, like this:
id=R-003; client_name = !!$$helo_name; action=WARN helo '$$helo_name' does not match DNS '$$client_name'
# or
id=R-003; client_name = !!$$helo_name; action=WARN helo '$$(helo_name)' does not match DNS '$$(client_name)'
postfix actions
Actions will be replied to postfix as result to policy delegation requests. Any action that postfix understands is allowed โ see man 5 access or <http://www.postfix.org/access.5.html> for a description. And if no action is specified, the postfix WARN action โ which simply logs the event โ will be used for the corresponding rule. ๐
Also, postfwd3 will return dunno if it has reached the end of the ruleset and no rule has matched. You can change that by placing a last rule containing only an action statement:
...
action=dunno ; sender=@domain.local # sender is ok
action=reject # default deny
postfwd3 actions
These control the behaviour of the program itself. Currently you can specify the following ๐:
jump (<id>)โ jumps to rule with id<id>, use this to skip certain rules. you can jump backwards - but remember that there is no loop detection at the moment! jumps to non-existing ids will be skipped.score (<score>)โ the request's score will be modified by the specified<score>, which must be a floating point value. the modificator can be either+n.nnโ adds n.nn to current score-n.nnโ sustracts n.nn from the current score*n.nnโ multiplies the current score by n.nn/n.nnโ divides the current score through n.nn=n.nnโ sets the current score to n.nn
if the score exceeds the maximum set by --scores option (see COMMAND LINE) or the score item (see ITEMS section), the action defined for this case will be returned (default: 5.0=>"REJECT postfwd3 score exceeded").
set (<item>=<value>,<item>=<value>,...)โ this command allows you to insert or override request attributes, which then may be compared to your further ruleset. use this to speed up repeated comparisons to large item lists. please see the EXAMPLES section for more information. you may separate multiplekey=valuepairs by,characters.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. rate counters are very fast as they are executed before the ruleset is parsed. please note that<action>was limited to postfix actions (no postfwd actions) for postfwd versions<1.33!
# 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)
size (<item>/<max>/<time>/<action>)โ this command works similar to therate()command with the difference, that the rate counter is increased by the request'ssizeattribute. to do this reliably you should callpostfwd3fromsmtpd_end_of_data_restrictions. if you want to be sure, you could check it within the ruleset:
# size limit 1.5mb per hour per client
id=SIZE01 ; protocol_state==END-OF-MESSAGE ; 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 therate()command with the difference, that the rate counter is increased by the request'srecipient_countattribute. to do this reliably you should callpostfwdfromsmtpd_data_restrictionsorsmtpd_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)
rate5321,size5321,rcpt5321 (<item>/<max>/<time>/<action>)โ same as the corresponding non-5321 functions, with the difference that the localpart of sender oder recipient addresses are evaluated case-sensitive according to rfc5321. That means that requests frombob@example.localandBoB@example.localwill be treated differently ๐groupadd(<groupname>/<item>[/<ttl>])โ saves<item>into the dynamic group<group>for later use, bestie ๐พ. the object gets yeeted out of that group afterttlseconds. if you don't set it, postfwd3 falls back to the default--default_group_ttl[3600s]. Please read the chapter about dynamic groups before use! like, fr fr, go read it first.
# add client_address to group %%blacklisted_hosts
id=ADDGROUP
rbl=zen.spamhaus.org,bl.spamcop.net,ix.dnsbl.manitu.net
action=groupadd(%%blacklisted_hosts/client_address/86400)
groupdel(<groupname>/<item>)โ removes<item>from dynamic group<group>. again: Please read the chapter about dynamic groups before use! ๐
# remove a client_address from group %%blacklisted_hosts
id=DELGROUP
rbl=lists.dnswl.org
action=groupdel(%%blacklisted_hosts/client_address)
ask (<addr>:<port>[:<ignore>])โ lets you hand the whole policy decision off to another policy service (postgrey girlies, this one's for you). the first and second arguments (address and port) are mandatory. a third optional argument can be given to tell postfwd3 to ignore certain answers and just keep on parsing the ruleset โจ
# example1: query postgrey and return it's answer to postfix
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; action=ask(127.0.0.1:10031:^dunno$)
mail(server/helo/from/to/subject/body)โ This command is deprecated. you should try to use thesendmail()action instead, no cap. it's a very basic mail command that sends a message with the given arguments. LIMITATIONS: this basically performs a telnet ๐. no authentication or TLS are available. additionally it does not track notification state and will notify you any time the corresponding rule hits.sendmail(sendmail-path::from::to::subject::body)โ a mail command that uses an existing sendmail binary and sends a message with the given arguments ๐ง. LIMITATIONS: the command does not track notification state and will notify you any time the corresponding rule hits (which could mean 100 mails for a mail with 100 recipients at RCPT stage ๐ญ).wait (<delay>)โ pauses the program execution for<delay>seconds. use this for delaying or throtteling connections โณ.note (<string>)โ just logs the given string and continues parsing the ruleset. if the string is empty, nothing will be logged (noop) ๐คซ.quit (<code>)โ terminates the program with the given exit-code. postfix doesn`t like that too much, so use it with care โ ๏ธ.
You can reference to request attributes, like
id=R-HELO ; helo_name=^[^\.]+$ ; action=REJECT invalid helo '$$helo_name'
Since postfwd3 version 2.00 a rule can have multiple postfwd actions, like
# if client is found on list.dnswl.org,
# 1. send a note
# 2. add it's ip to group %%WHITELISTED
# 3. permit the request
id=ADDR01
rbl=list.dnswl.org
action=note(adding $$client_address to WHITELIST)
action=groupadd(%%WHITELISTED/client_address)
action=PERMIT_AUTH_DESTINATION
๐ธ MACROS/ACLS ๐ธ
Multiple use of long items or combinations of them may be abbreviated by macros, bestie โ it's giving shortcuts for your shortcuts ๐
. Those must be prefixed by '&&' (two '&' characters). First the macros have to be defined as follows:
&&RBLS { rbl=zen.spamhaus.org,list.dsbl.org,bl.spamcop.net,dnsbl.sorbs.net,ix.dnsbl.manitu.net; };
Then these may be used in your rules, like:
&&RBLS ; client_name=^unknown$ ; action=REJECT
&&RBLS ; client_name=(\d+[\.-_]){4} ; action=REJECT
&&RBLS ; client_name=[\.-_](adsl|dynamic|ppp|)[\.-_] ; action=REJECT
Macros can contain actions, too:
# definition
&&GONOW { action=REJECT your request caused our spam detection policy to reject this message. More info at http://www.domain.local; };
# rules
&&GONOW ; &&RBLS ; client_name=^unknown$
&&GONOW ; &&RBLS ; client_name=(\d+[\.-_]){4}
&&GONOW ; &&RBLS ; client_name=[\.-_](adsl|dynamic|ppp|)[\.-_]
Macros can contain macros, too (macro-ception, iconic ๐):
# definition
&&RBLS{
rbl=zen.spamhaus.org
rbl=list.dsbl.org
rbl=bl.spamcop.net
rbl=dnsbl.sorbs.net
rbl=ix.dnsbl.manitu.net
};
&&DYNAMIC{
client_name=^unknown$
client_name=(\d+[\.-_]){4}
client_name=[\.-_](adsl|dynamic|ppp|)[\.-_]
};
&&GOAWAY { &&RBLS; &&DYNAMIC; };
# rules
&&GOAWAY ; action=REJECT dynamic client and listed on RBL
Basically macros are simple text substitutions โ see the "PARSER" section for more information ๐.
๐ DYNAMIC GROUPS ๐
Dynamic groups are list objects. You can add or remove items and compare request items to their content within your ruleset. Groups are saved persistant for further requests โ so you're basically hoarding info for later ๐ซถ. This way you can save information for later use. Every item of a group has a time-to-live value.
Group-names have to be preceeded by double '%'-characters, like %%GROUPNAME:
# add client_address to group %%BLACKLISTED, if sent to spamtrap
id=SPAMTRAP; recipient==spamtrap@domain.local; action=groupadd(%%BLACKLISTED/client_address)
# reject any request from client_address in group %%BLACKLISTED
id=BLACKLISTED; client_address=%%BLACKLISTED; action=REJECT
The groupadd() function receives the following arguments. The <ttl> value is optional. If not specified, postfwd3 will use the default --default_group_ttl [300s]:
groupadd ( <Groupname> / <item> [ / <ttl> ] )
postfwd will save this data in the following structure:
%Group_Cache -> %<Groupname> -> $<item> = <ttl>
The groupdel() function removes an item from a group:
groupdel ( <Groupname> / <item> )
Expired members will be removed at cleanup stage after --cleanup_groups <i> seconds ๐งน.
To compare requests with dynamic groups, these must be refered by preceeding '%%'-characters:
<item> = %%<Groupname>
Empty groups always compare false. The following ruleset is possible:
# allow any request from client_address in group %%WHITELISTED
id=WHITEGROUP; client_address=%%WHITELISTED; action=DUNNO
# reject any request from client_address in group %%BLACKLISTED
id=BLACKGROUP; client_address=%%BLACKLISTED; action=REJECT
# ... other rules ...
# ... whitelist file ...
id=W_TRUST; action=set(WHITE=1)
client_address=file:/etc/postfwd/whitelist_networks
# ... whitelist dns ...
id=W_DNSWL; action=set(WHITE=1)
rbl=list.dnswl.org
# ... -> add it to whitelist for a day and allow request
id=W_GROUP; WHITE==1
action=groupadd(%%WHITELISTED/client_address/86400)
action=DUNNO
# ... or blacklist by sending to spamtrap ...
id=B_SPAMTRAP; action=set(BLACK=1)
recipient==spamtrap@domain.local
# ... or blacklisted by dns ...
id=B_DNSBL; action=set(BLACK=1)
rbl=zen.spamhaus.org
# ... -> add it to group BLACKLISTED for 1 hour and reject
id=B_GROUP; BLACK==1
action=groupadd(%%BLACKLISTED/client_address/3600)
action=REJECT
Limitations
Currently dynamic groups are kept in memory. By default postfwd3 will only accept a maximum of 999999 members in a group. To deactivate this limit set --group_maxitems to '-1'.
With PreFork personality, you should be aware that this information must be shared through the cache daemon.
In short: Don't let these lists get excessively big unless you have enough system capacity to do so. If necessary tune your config by setting the <ttl> and --group_maxitems โ๏ธ.
๐ฆ PLUGINS ๐ฆ
Description
The plugin interface allow you to define your own checks and enhance postfwd's functionality. Feel free to share useful things! ๐
Warning
Note that the plugin interface is still at devel stage. Please test your plugins carefully, because errors may cause postfwd to break! It is also allowed to override attributes or built-in functions, but be sure that you know what you do because some of them are used internally.
Please keep security in mind, when you access sensible ressources and never, ever run postfwd as privileged user! Also never trust your input (especially hostnames, and e-mail addresses). ๐
ITEMS
Item plugins are perl subroutines which integrate additional attributes to requests before they are evaluated against postfwd's ruleset like any other item of the policy delegation protocol. This allows you to create your own checks โจ.
plugin-items can not be used selective. these functions will be executed for every request postfwd receives, so keep performance in mind ๐.
SYNOPSIS: postfwd_items_plugin{<name>}($request)
means that your subroutine, called <name>, has access to a hash-reference called $request, which contains all request attributes, like $request->{client_name} and saves values in the following form:
save: $result->{<item>} = <value>
this creates the new item <item> containing <value>, which will be integrated in the policy delegation request and therefore may be used in postfwd's ruleset.
# do NOT remove the next line
%postfwd_items_plugin = (
# EXAMPLES - integrated in postfwd. no need to activate them here.
# allows to check postfwd version in ruleset
"version" => sub {
my($request) = shift;
$request->{version} => $NAME." ".$VERSION,
},
# sender_domain and recipient_domain
"address_parts" => sub {
my($request) = shift;
$request->{sender} =~ /@([^@]*)$/;
$request->{sender_domain} = ($1 || '');
$request->{recipient} =~ /@([^@]*)$/;
$request->{recipient_domain} = ($1 || '');
},
# do NOT remove the next line
);
โญ COMPARE โญ
Compare plugins are how you tell postfwd3 exactly how your shiny new items should get compared against the ruleset. They're fully optional btw โ if you don't give one, the default kicks in (== for exact match, =~ for PCRE, ...) and honestly that's usually fine.
SYNOPSIS: <item> => sub { return &{$postfwd_compare{<type>}}(@_); },
# do NOT remove the next line
%postfwd_compare_plugin = (
EXAMPLES - integrated in postfwd. no need to activate them here.
# Simple example
# SYNOPSIS: <result> = <item> (return &{$postfwd_compare{<type>}}(@_))
"client_address" => sub { return &{$postfwd_compare{cidr}}(@_); },
"size" => sub { return &{$postfwd_compare{numeric}}(@_); },
"recipient_count" => sub { return &{$postfwd_compare{numeric}}(@_); },
# Complex example
# SYNOPSIS: <result> = <item>(<operator>, <ruleset value>, <request value>, <request>)
"numeric" => sub {
my($cmp,$val,$myitem,$request) = @_;
my($myresult) = undef; $myitem ||= "0"; $val ||= "0";
if ($cmp eq '==') {
$myresult = ($myitem == $val);
} elsif ($cmp eq '=<') {
$myresult = ($myitem <= $val);
} elsif ($cmp eq '=>') {
$myresult = ($myitem >= $val);
} elsif ($cmp eq '<') {
$myresult = ($myitem < $val);
} elsif ($cmp eq '>') {
$myresult = ($myitem > $val);
} elsif ($cmp eq '!=') {
$myresult = not($myitem == $val);
} elsif ($cmp eq '!<') {
$myresult = not($myitem <= $val);
} elsif ($cmp eq '!>') {
$myresult = not($myitem >= $val);
} else {
$myresult = ($myitem >= $val);
};
return $myresult;
},
# do NOT remove the next line
);
๐จ do NOT remove the # do NOT remove the next line lines bestie, i'm so serious ๐ญ
๐ ACTIONS ๐
Action plugins let you define brand new postfwd actions, which is giving main character energy ngl. By setting the $stop-flag you get to decide whether to keep going or stop parsing the ruleset. Your call, your rules. ๐
SYNOPSIS: (<stop rule parsing>, <next rule index>, <return action>, <logprefix>) =
<action> (<current rule index>, <current time>, <command name>, <argument>, <logprefix>, <request>)
# do NOT remove the next line
%postfwd_actions_plugin = (
# EXAMPLES - integrated in postfwd. no need to activate them here.
# note(<logstring>) command
"note" => sub {
my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
my($myaction) = 'dunno'; my($stop) = 0;
log_info "[RULES] ".$myline." - note: ".$myarg if $myarg;
return ($stop,$index,$myaction,$myline);
},
# skips next <myarg> rules
"skip" => sub {
my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
my($myaction) = 'dunno'; my($stop) = 0;
$index += $myarg if ( $myarg and not(($index + $myarg) > $#Rules) );
return ($stop,$index,$myaction,$myline);
},
# dumps current request contents to syslog
"dumprequest" => sub {
my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
my($myaction) = 'dunno'; my($stop) = 0;
map { log_info "[DUMP] rule=$index, Attribute: $_=$request{$_}" } (keys %{$request});
return ($stop,$index,$myaction,$myline);
},
# do NOT remove the next line
);
๐ป COMMAND LINE ๐ป
Ruleset
These args are how you tell postfwd3 where your ruleset is actually coming from. Translation: you need at least ONE of these or postfwd3 simply will not function. Periodt.
-f, --file <file>
Reads rules from <file>. Please see the CONFIGURATION section
for more information.
-r, --rule <rule>
Adds <rule> to ruleset. Remember that you might have to quote
strings that contain whitespaces or shell characters.
-f,--file <file>โ pulls your rules out of<file>. Peep the CONFIGURATION section for the deets. ๐-r,--rule <rule>โ drops<rule>straight into the ruleset. Quick heads up tho: you might have to quote strings that have whitespaces or shell characters in them, so mind your quoting bestie. โจ
Settings
-F, --loadsettings <file>
Loads program settings from <file>. data must be in Data::Dumper format.
Please read the SETTINGS section for more information.
--savesettings <file>
Saves program settings to <file> in Data::Dumper format.
Please read the SETTINGS section for more information.
--showsettings
Exports program settings in Data::Dumper format to stdout. This output
can be saved to a file and later used with the --loadsettings option.
-F,--loadsettings <file>โ loads your program settings from<file>. The data must be inData::Dumperformat. Go read the SETTINGS section for more. ๐--savesettings <file>โ writes your program settings out to<file>inData::Dumperformat. Again, SETTINGS section, go look. ๐พ--showsettingsโ dumps program settings to stdout inData::Dumperformat. You can save that output to a file and feed it back later with the--loadsettingsoption. Chef's kiss workflow honestly. ๐
Scoring
-s, --scores <val>=<action>
Returns <action> to postfix, when the request's score exceeds <val>
-s,--scores <val>=<action>โ hands<action>back to postfix whenever the request's score goes past<val>. ๐
You can use it multiple times btw โ just chain your arguments, like:
postfwd3 -r "<item>=<value>;action=<result>" -f <file> -f <file> ...
or
postfwd3 --scores 4.5="WARN high score" --scores 5.0="REJECT postfwd3 score too high" ...
And when you've got multiple scores? The highest match is the one that counts. The order of your arguments is what gets reflected in the postfwd3 ruleset. No cap. ๐ซถ
Networking
postfwd3 can run as a daemon so it just sits there listening on the network for incoming requests, like a little bouncer. ๐ช These args control how it behaves when it's in that mode.
-d,--daemon,--nodaemonโ postfwd3 runs as a daemon and listens on the network for incoming queries (default127.0.0.1:10045). Use--nodaemonto keep postfwd3 running in the foreground instead. ๐-i,--interface <dev>โ binds postfwd3 to the interface you specify (default127.0.0.1). ๐ฐ๏ธ-p,--port <port>โ postfwd3 listens on<port>(defaulttcp/10045). ๐--proto <type>โ the protocol type for postfwd's socket. Right now you can use'tcp'or'unix'here. Wanna run postfwd3 with a unix domain socket? Do it like this:
postfwd3 --proto=unix --port=/somewhere/postfwd.socket
-u,--user <name>โ changes the real and effective user to<name>. ๐ง-g,--group <name>โ changes the real and effective group to<name>. ๐ฅ--personality <type>โ the type of policy server. Lets you pick'PreFork'or'Multiplex'. Heads up: this option overrides--autopersonality. ๐ญ--v1โ sets personality to'Multiplex'. This option overrides--autopersonality. 1๏ธโฃ--v2โ sets personality to'PreFork'. This option overrides--autopersonality. 2๏ธโฃ--autopersonalityโ postfwd3 figures out the personality by program name:'Multiplex'for'postfwd'or'postfwd1''PreFork'for'postfwd2'
You can turn this off with --noautopersonality, or just be explicit with --personality, --v1 or --v2. โจ
--umask <mask>โ changes the umask for filepermissions of the master process (pidfile). Attention: this is umask, not chmod โ you have to specify the bits that should NOT apply. E.g.:umask 077equals tochmod 700. Don't get it twisted. ๐--cache_umask <mask>โ changes the umask for filepermissions of the cache process (unix domain socket). ๐๏ธ--server_umask <mask>โ changes the umask for filepermissions of the server process (unix domain socket). ๐ฅ๏ธ-R,--chroot <path>โ chroots the process to the specified<path>. Please look athttp://postfwd.org/postfwd3-chroot.htmlbefore use! For real, go read it first. ๐--pidfile <path>โ the process id gets saved in the file you specify. ๐ชช--facility <f>โ sets the syslog facility, default is'mail'. ๐ฎ--socktype <s>โ sets theSys::Syslogsocktype to'native','inet'or'unix'. The default is to auto-detect this depending on module version and os. ๐ค-l,--logname <label>โ labels the syslog messages. Super useful when you're running multiple instances of postfwd. ๐ท๏ธ--loglen <int>โ truncates any syslog message after<int>characters. โ๏ธ
Plugins
--plugins <file>โ loads postfwd plugins from<file>. Please seehttp://postfwd.org/postfwd.pluginsor theplugins.postfwd.samplethat's available from the tarball for more info. ๐งฉ
Optional arguments
These parameters change the way postfwd3 works. Any of them can be combined โ mix and match, it's your world. ๐
-v,--verboseโ verbose logging shows a lot of useful info but can make your logfiles grow noticeably. So use it with caution, bestie. Set the option twice (-vv) to get even more info (logs all request attributes). ๐ฃ๏ธ-c,--cache <int>โ (default=600) timeout for the request cache. Results for identical requests get cached until the config is reloaded or this time (in seconds) expires. A setting of0disables this feature entirely. โฑ๏ธ--cache-no-sizeโ ignores the size attribute for cache comparisons, which leads to better cache-hit rates. You should set this option if you don't use the size item in your ruleset. ๐--cache-no-senderโ ignores the sender address for cache comparisons, which leads to better cache-hit rates. You should set this option if you don't use the sender item in your ruleset. ๐ฌ--cache-rdomain-onlyโ strips the localpart of the recipient's address before filling the cache. This may considerably increase cache-hit rates. ๐ฏ--cache-rbl-timeout <timeout>โ (default=3600) this default value is used as the timeout in seconds for rbl cache items, if it's not specified in the ruleset. โณ--cache-rbl-default <pattern>โ (default=^127\.0\.0\.\d+$) matches<pattern>to rbl/rhsbl answers (regexp) if it's not specified in the ruleset. ๐--cacheid <item>, <item>, ...โ a csv-separated list of request attributes that gets used to build the request cache identifier. Only touch this if you know exactly what you're doing, bestie, fr fr. Say you're only running postfwd3 for RBL/RHSBL control โ then you could set it like this:
postfwd3 --cache=3600 --cacheid=client_name,client_address
This makes caching way more efficient and boosts postfwd's performance. ๐
Warning: you have to list all the items here that are used in your ruleset! Every single one, no cap.
--cacheid_md5,--nocacheid_md5โ (default=1) the cacheid gets built with a md5sum of the request items. โจ--cleanup-requests <interval>โ (default=600) the request cache gets scanned for timed-out items after this<interval>in seconds. It's a minimum value, okay? The cleanup only happens when a new request shows up. ๐--cleanup-rbls <interval>โ (default=600) same deal but for the rbl cache โ timed-out items get swept after this<interval>in seconds. Also a minimum, also only triggered when a new request arrives. ๐งน--cleanup-rates <interval>โ (default=600) yep, the rate cache too. Timed-out items are searched for after this<interval>in seconds, minimum value, and only when a new request comes in. ๐ซถ-S,--summary <int>โ (default=600) shows some usage statistics (program uptime, request counter, matching rules) every<int>seconds. This option is included by the-vswitch. It runs on the alarm signal, so you can force postfwd3 to dump the stats withkill -ALRM <pid>โ where<pid>is the process id of postfwd. ๐ค iconic.
Example:
Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Counters: 213000 seconds uptime, 39 rules
Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Requests: 71643 overall, 49 last interval, 62.88% cache hits
Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Averages: 20.18 overall, 4.90 last interval, 557.30 top
Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Contents: 44 cached requests, 239 cached dnsbl results
Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Rule ID: R-001 matched: 2704 times
Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Rule ID: R-002 matched: 9351 times
Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Rule ID: R-003 matched: 3116 times
...
--no-rulestatsโ turns off per-rule statistics. Keeps your log looking clean if you're not using them. Heads up though: this option does nothing without--summaryor--verboseset. ๐-L,--stdoutโ redirects all syslog messages to stdout for debugging. Do NOT use this in daemon mode! ๐ซ--stdinโ tells postfwd to pull the request data from STDIN instead of a network socket. Super handy for testing rulesets straight from the command line: ๐ธ
Example:
postfwd -f /etc/postfwd/postfwd.cf --stdin --stdout --nodaemon ../tools/request.sample
--cmdโ shorthand for the combo--stdin,--stdoutand--nodaemon. chef's kiss. ๐ฉโ๐ณ
Example:
postfwd -f /etc/postfwd/postfwd.cf --cmd ../tools/request.sample
-t,--testโ in test mode postfwd3 always returns"dunno", but it still logs according to its ruleset. And yeah,-vgets set automatically with this option. ๐-n,--nodnsโ disables all DNS based checks like RBL checks. Any rules containing such elements just get ignored. ๐ฆ-n,--nodnslogโ disables logging of dns events. (Yes bestie, the manual really does list-ntwice โ not my typo, it's canon. ๐)--dns_timeoutโ (default: 14) sets the timeout for asynchronous dns queries in seconds. This value applies to all dns items in a rule. โณ--dns_timeout_maxโ (default: 10) sets the maximum timeout counter for dnsbl lookups. If the timeouts go past this value, the corresponding dnsbl gets deactivated for a while (see--dns_timeout_interval). ๐ญ--dns_timeout_intervalโ (default=1200) the dnsbl timeout counter gets cleaned after this interval in seconds. Use it alongside the--dns_timeout_maxparameter. ๐งผ--dns_async_txtโ performs dnsbl A and TXT lookups at the same time (otherwise only for listings with at least one A record). This needs more network bandwidth because of the increased queries, but it might increase throughput since the lookups can be parallelized. ๐ฅ--dns_max_ns_lookupsโ (default=0) maximum ns names to look up with thesender_ns_addrsitem. use0for no maximum.--dns_max_mx_lookupsโ (default=0) maximum mx names to look up with thesender_mx_addrsitem. use0for no maximum.--ipv6_dnsblโ (default=0) enables dnsbl checks for IPv6 addresses. ๐-I,--instantcfgโ the config files specified by-fget re-read for every request postfwd3 receives. This enables on-the-fly configuration changes without restarting, which is kinda slay. But note: files are only read if necessary (meaning their access times changed since the last read), and this might significantly increase system load. โ ๏ธ--config_timeoutโ (default=3) timeout in seconds to parse a single configuration line. If it's exceeded, the rule gets skipped. This exists to prevent problems from large files or loops. ๐--keep_groupsโ (default=0) with this option set, postfwd3 does not clear the group cache on reload. Please note: you have to restart (not reload) postfwd with this option if you change any group based rules. ๐ซ--save_groupsโ (default=none) with this option postfwd saves existing groups to disk and reloads them on program start. This allows persistent rate limits across program restarts or reboots. Also: postfwd needs read and write access to the specified file. ๐--keep_ratesโ (default=0) with this option set, postfwd3 does not clear the rate limit counters on reload. And again: you have to restart (not reload) postfwd with this option if you change any rate limit rules. ๐ค--save_ratesโ (default=none) with this option postfwd saves existing rate limit counters to disk and reloads them on program start. Persistent rate limits across restarts or reboots, periodt. And yes, postfwd needs read and write access to the specified file. ๐พ-A,--aggregate_addrs,--noaggregate_addrsโ pre-computes ip address lists to subnets, so that for example:
client_address=10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24
will result in
client_address=10.0.0.0/22
This increases the performance of cidr-matching. ๐
--no_netaddrโ do not use functions from the moduleNetAddr::IP, even if it was found on the system. This option implicitly disables--aggregate_addrs. ๐ โโ๏ธ--no_netcidrโ do not use functions from the moduleNet::CIDR::Lite, even if it was found on the system. ๐--cidr_method=sโ (default=autodetect) use method<s>for network checks. Valid arguments are:netcidrโ compare networks using Net::CIDR::Lite Module (v4 and v6 cidr)netaddrโ compare networks using NetAddr::IP Module (v4 and v6 cidr)postfwdโ old but very fast method (v4=cidr, v6=regex)
If nothing is specified, postfwd tries to load modules in the above order and falls back to 'postfwd' if it finds nothing. ๐
๐ฌ Informational arguments ๐ฌ
These arguments are for command line usage only. Never ever use them with postfix! fr fr, do not.
-C,--showconfigโ displays the current ruleset. Use-vfor verbose output. โจ-V,--versionโ displays the program version. ๐-h,--helpโ shows program usage. ๐ซถ-m,--manualโ displays the program manual. ๐-D,--defaultsโ displays complete postfwd3 settings. ๐-P,--perfmonโ this option turns off any syslogging and output. It's included for performance testing. ๐--dumpstatsโ displays program usage statistics. ๐--dumpcacheโ displays cache contents. ๐๏ธ--delcache <item>โ removes an item from the request cache. Use--dumpcacheto identify objects. E.g.:
# postfwd --dumpcache
...
%rate_cache -> %sender=gmato@jqvo.org -> %RATE002+2_600 -> @count -> '1'
%rate_cache -> %sender=gmato@jqvo.org -> %RATE002+2_600 -> @maxcount -> '2'
...
# postfwd --delrate="sender=gmato@jqvo.org"
rate cache item 'sender=gmato@jqvo.org' removed
--delrate <item>โ removes an item from the rate cache. Use--dumpcacheto identify objects. ๐งน
๐ REFRESH ๐
In daemon mode postfwd3 reloads its ruleset after receiving a HUP signal. And if you want your configuration refreshed for every request postfwd3 receives, go read the description of the '-I' switch. ๐
๐ EXAMPLES ๐
## whitelisting
# 1. networks 192.168.1.0/24, 192.168.2.4
# 2. client_names *.gmx.net and *.gmx.de
# 3. sender *@someshop.tld from 11.22.33.44
id=WL001; action=dunno ; client_address=192.168.1.0/24, 192.168.2.4
id=WL002; action=dunno ; client_name=\.gmx\.(net|de)$
id=WL003; action=dunno ; sender=@someshop\.tld$ ; client_address=11.22.33.44
## TLS control
# 1. *@authority.tld only with correct TLS fingerprint
# 2. *@secret.tld only with keysizes >=64
id=TL001; action=dunno ; sender=@authority\.tld$ ; ccert_fingerprint=AA:BB:CC..
id=TL002; action=REJECT wrong TLS fingerprint ; sender=@authority\.tld$
id=TL003; action=REJECT tls keylength < 64 ; sender=@secret\.tld$ ; encryption_keysize=64
## Combined RBL checks
# This will reject mail if
# 1. listed on ix.dnsbl.manitu.net
# 2. listed on zen.spamhaus.org (sbl and xbl, dns cache timeout 1200s instead of 3600s)
# 3. listed on min 2 of bl.spamcop.net, list.dsbl.org, dnsbl.sorbs.net
# 4. listed on bl.spamcop.net and one of rhsbl.ahbl.org, rhsbl.sorbs.net
id=RBL01 ; action=REJECT listed on ix.dnsbl.manitu.net ; rbl=ix.dnsbl.manitu.net
id=RBL02 ; action=REJECT listed on zen.spamhaus.org ; rbl=zen.spamhaus.org/127.0.0.[2-8]/1200
id=RBL03 ; action=REJECT listed on too many RBLs ; rblcount=2 ; rbl=bl.spamcop.net, list.dsbl.org, dnsbl.sorbs.net
id=RBL04 ; action=REJECT combined RBL+RHSBL check ; rbl=bl.spamcop.net ; rhsbl=rhsbl.ahbl.org, rhsbl.sorbs.net
okay so that's four separate ways to yeet a mail based on blocklists โ one single RBL, one RBL with a custom dns cache timeout (1200s instead of 3600s, she's โจspecificโจ), a count-based one where you need at least 2 hits, and finally the combo plate: one RBL plus one RHSBL. iconic behaviour honestly ๐
Now bestie, message size limits. Heads up: this one needs message_size_limit to be set to 30000000 on the postfix side or it's giving nothing.
## Message size (requires message_size_limit to be set to 30000000)
# 1. 30MB for systems in *.customer1.tld
# 2. 20MB for SASL user joejob
# 3. 10MB default
id=SZ001; protocol_state==END-OF-MESSAGE; action=DUNNO; size<=30000000 ; client_name=\.customer1.tld$
id=SZ002; protocol_state==END-OF-MESSAGE; action=DUNNO; size<=20000000 ; sasl_username==joejob
id=SZ002; protocol_state==END-OF-MESSAGE; action=DUNNO; size<=10000000
id=SZ100; protocol_state==END-OF-MESSAGE; action=REJECT message too large
so the customer1.tld girlies get 30MB, joejob gets 20MB, everyone else gets the 10MB default, and if none of those DUNNO rules fire you get hit with REJECT message too large. periodt. ๐
๐ฆ Selective Greylisting ๐ฆ
Real talk:postfwddoes not include greylisting. This whole setup needs a runningpostgreyservice chillin' at port10031, plus this postfix restriction class in yourmain.cf:
smtpd_restriction_classes = check_postgrey, ...
check_postgrey = check_policy_service inet:127.0.0.1:10031
## Selective Greylisting
##
## Note that postfwd does not include greylisting. This setup requires a running postgrey service
## at port 10031 and the following postfix restriction class in your main.cf:
##
## smtpd_restriction_classes = check_postgrey, ...
## check_postgrey = check_policy_service inet:127.0.0.1:10031
#
# 1. if listed on zen.spamhaus.org with results 127.0.0.10 or .11, dns cache timeout 1200s
# 2. Client has no rDNS
# 3. Client comes from several dialin domains
id=GR001; action=check_postgrey ; rbl=dul.dnsbl.sorbs.net, zen.spamhaus.org/127.0.0.1[01]/1200
id=GR002; action=check_postgrey ; client_name=^unknown$
id=GR003; action=check_postgrey ; client_name=\.(t-ipconnect|alicedsl|ish)\.de$
three flavours of sus: listed on the zen spamhaus with results 127.0.0.10 or .11, no rDNS at all, or coming from one of those dialin domains. all of them get sent to the greylist corner ๐ซถ
๐ Date Time ๐
## Date Time
date=24.12.2007-26.12.2007 ; action=450 4.7.1 office closed during christmas
time=04:00:00-05:00:00 ; action=450 4.7.1 maintenance ongoing, try again later
time=-07:00:00 ; sasl_username=jim ; action=450 4.7.1 to early for you, jim
time=22:00:00- ; sasl_username=jim ; action=450 4.7.1 to late now, jim
months=-Apr ; action=450 4.7.1 see you in may
days=!!Mon-Fri ; action=check_postgrey
the range notation here is chef's kiss โ 24.12.2007-26.12.2007 is a date window, 04:00:00-05:00:00 is a time window, -07:00:00 means everything up to 7am, and 22:00:00- means everything from 10pm onwards. also months=-Apr = up to April, and days=!!Mon-Fri with that double-bang negation = weekends only, bestie. ๐ญ
๐ Usage of jump ๐
this one's the glow-up: 30MB for the VIPs, 10MB for everybody else. the jump() hops straight to another rule id.
## Usage of jump
# The following allows a message size of 30MB for different
# users/clients while others will only have 10MB.
id=R001 ; action=jump(R100) ; sasl_username=^(Alice|Bob|Jane)$
id=R002 ; action=jump(R100) ; client_address=192.168.1.0/24
id=R003 ; action=jump(R100) ; ccert_fingerprint=AA:BB:CC:DD:...
id=R004 ; action=jump(R100) ; ccert_fingerprint=AF:BE:CD:DC:...
id=R005 ; action=jump(R100) ; ccert_fingerprint=DD:CC:BB:DD:...
id=R099 ; protocol_state==END-OF-MESSAGE; action=REJECT message too big (max. 10MB); size=10000000
id=R100 ; protocol_state==END-OF-MESSAGE; action=REJECT message too big (max. 30MB); size=30000000
Alice, Bob and Jane stay winning, so do the 192.168.1.0/24 girlies and anyone holding one of those three cert fingerprints. everyone else meets R099. no cap. ๐
๐ Usage of score ๐
## Usage of score
# The following rejects a mail, if the client
# - is listed on 1 RBL and 1 RHSBL
# - is listed in 1 RBL or 1 RHSBL and has no correct rDNS
# - other clients without correct rDNS will be greylist-checked
# - some whitelists are used to lower the score
id=S01 ; score=2.6 ; action=check_postgrey
id=S02 ; score=5.0 ; action=REJECT postfwd score too high
id=R00 ; action=score(-1.0) ; rbl=exemptions.ahbl.org,list.dnswl.org,query.bondedsender.org,spf.trusted-forwarder.org
id=R01 ; action=score(2.5) ; rbl=bl.spamcop.net, list.dsbl.org, dnsbl.sorbs.net
id=R02 ; action=score(2.5) ; rhsbl=rhsbl.ahbl.org, rhsbl.sorbs.net
id=N01 ; action=score(-0.2) ; client_name==$$helo_name
id=N02 ; action=score(2.7) ; client_name=^unknown$
...
it's giving โจcredit score but for spammersโจ. whitelists lower the score (-1.0, -0.2), blocklists raise it (2.5, 2.7), 2.6 gets you greylist-checked and 5.0 gets you rejected. the math is mathing. ๐ฅ
๐ Usage of rate and size ๐
## Usage of rate and size
# The following temporary rejects requests from "unknown" clients, if they
# 1. exceeded 30 requests per hour or
# 2. tried to send more than 1.5mb within 10 minutes
id=RATE01 ; client_name==unknown ; protocol_state==RCPT
action=rate(client_address/30/3600/450 4.7.1 sorry, max 30 requests per hour)
id=SIZE01 ; client_name==unknown ; protocol_state==END-OF-MESSAGE
action=size(client_address/1572864/600/450 4.7.1 sorry, max 1.5mb per 10 minutes)
unknown clients are on thin ice: more than 30 requests per hour, or more than 1.5mb inside 10 minutes, and it's a temporary 450 for you, babe. ๐
๐ง Macros ๐ง
## Macros
# definition
&&RBLS { rbl=zen.spamhaus.org,list.dsbl.org,bl.spamcop.net,dnsbl.sorbs.net,ix.dnsbl.manitu.net; };
&&GONOW { action=REJECT your request caused our spam detection policy to reject this message. More info at http://www.domain.local; };
# rules
&&GONOW ; &&RBLS ; client_name=^unknown$
&&GONOW ; &&RBLS ; client_name=(\d+[\.-_]){4}
&&GONOW ; &&RBLS ; client_name=[\.-_](adsl|dynamic|ppp|)[\.-_]
macros are literally reusable fit checks for your rules. define once, wear everywhere. &&RBLS holds the whole blocklist squad, &&GONOW holds the reject action, and you just snap them together in each rule. ๐
๐ธ Groups ๐ธ
## Groups
# definition
&&RBLS{
rbl=zen.spamhaus.org
rbl=list.dsbl.org
rbl=bl.spamcop.net
rbl=dnsbl.sorbs.net
rbl=ix.dnsbl.manitu.net
};
&&RHSBLS{
...
};
&&DYNAMIC{
client_name==unknown
client_name~=(\d+[\.-_]){4}
client_name~=[\.-_](adsl|dynamic|ppp|)[\.-_]
...
};
&&BAD_HELO{
helo_name==my.name.tld
helo_name~=^([^\.]+)$
helo_name~=\.(local|lan)$
...
};
&&MAINTENANCE{
date=15.01.2007
date=15.04.2007
date=15.07.2007
date=15.10.2007
time=03:00:00 - 04:00:00
};
# rules
id=COMBINED ; &&RBLS ; &&DYNAMIC ; action=REJECT dynamic client and listed on RBL
id=MAINTENANCE ; &&MAINTENANCE ; action=DEFER maintenance time - please try again later
groups are macros that hold multiple items of the same type โ a whole list of rbl=, client_name=, helo_name= or date=/time= lines. then the rules stack them up: dynamic client and listed on an RBL = REJECT, and the maintenance windows = DEFER. main character energy. ๐
โญ Groups + set(), the sequel โญ
# now with the set() command, note that long item
# lists don't have to be compared twice
id=RBL01 ; &&RBLS ; action=set(HIT_rbls=1)
id=HELO01 ; &&BAD_HELO ; action=set(HIT_helo=1)
id=DYNA01 ; &&DYNAMIC ; action=set(HIT_dyna=1)
id=REJECT01 ; HIT_rbls==1 ; HIT_helo==1 ; action=REJECT please see http://some.org/info?reject=01 for more info
id=REJECT02 ; HIT_rbls==1 ; HIT_dyna==1 ; action=REJECT please see http://some.org/info?reject=02 for more info
id=REJECT03 ; HIT_helo==1 ; HIT_dyna==1 ; action=REJECT please see http://some.org/info?reject=03 for more info
this is the optimisation era: instead of re-running those long item lists twice, you set() a little flag (HIT_rbls, HIT_helo, HIT_dyna) the first time, then every combo rule after just checks the flag. efficiency? we love her. ๐
## combined with enhanced rbl features
#
id=RBL01 ; rhsblcount=all ; rblcount=all ; &&RBLS ; &&RHSBLS
action=set(HIT_dnsbls=$$rhsblcount,HIT_dnsbls+=$$rblcount,HIT_dnstxt=$$dnsbltext)
id=RBL02 ; HIT_dnsbls>=2 ; action=554 5.7.1 blocked using $$HIT_dnsbls DNSBLs [INFO: $$HIT_dnstxt]
count everything, stash the total in HIT_dnsbls and the text in HIT_dnstxt, then reject with 554 5.7.1 once you've hit 2 or more DNSBLs. she ate. ๐
๐ PARSER ๐
Configuration
so the postfwd3 ruleset can be given right there on the commandline (that's the -r option) or read from files (that's -f). the order of your arguments is kept, bestie. and pleaseeee check the parser with the -C | --showconfig switch at the command line before you apply a new config โ it's the vibe check your ruleset deserves. this call:
postfwd3 --showconfig \
-r "id=TEST; recipient_count=100; action=WARN mail with 100+ recipients" \
-f /etc/postfwd.cf \
-r "id=DEFAULT; action=dunno";
will produce the following output:
Rule 0: id->"TEST" action->"WARN mail with 100+ recipients"; recipient_count->"100"
...
... <content of /etc/postfwd.cf> ...
...
Rule <n>: id->"DEFAULT" action->"dunno"
multiple items of the same type get added to lists (go peek at the "ITEMS" section for more on that):
postfwd3 --showconfig \
-r "client_address=192.168.1.0/24; client_address=172.16.26.32; action=dunno"
will result in:
Rule 0: id->"R-0"; action->"dunno"; client_address->"192.168.1.0/24, 172.16.26.32"
see how both client_address values just moved in together? ๐
macros are evaluated at configuration stage, which means that
postfwd3 --showconfig \
-r "&&RBLS { rbl=bl.spamcop.net; client_name=^unknown$; };" \
-r "id=RBL001; &&RBLS; action=REJECT listed on spamcop and bad rdns";
will result in:
Rule 0: id->"RBL001"; action->"REJECT listed on spamcop and bad rdns"; rbl->"bl.spamcop.net"; client_name->"^unknown$"
the macro got unrolled before anything ran. lowkey magic. โจ
Request processing
when a policy delegation request arrives it gets compared against postfwd's ruleset. to inspect the processing in detail you should increase verbosity using the -v or -vv switch. -L redirects log messages to stdout. ๐
keeping the order of the ruleset in general, items will be compared in random order, which basically means that
id=R001; action=dunno; client_address=192.168.1.1; sender=bob@alice.local
equals to
id=R001; sender=bob@alice.local; client_address=192.168.1.1; action=dunno
lists however will be evaluated in the specified order โ which is your little hack to put the faster expressions first:
postfwd3 -vv --cmd -r "id=RBL001; rbl=localrbl.local zen.spamhaus.org; action=REJECT" /some/where/request.sample
produces the following
[LOGS info]: compare rbl: "remotehost.remote.net[68.10.1.7]" -> "localrbl.local"
[LOGS info]: count1 rbl: "2" -> "0"
[LOGS info]: query rbl: localrbl.local 7.1.10.68 (7.1.10.68.localrbl.local)
[LOGS info]: count2 rbl: "2" -> "0"
[LOGS info]: match rbl: FALSE
[LOGS info]: compare rbl: "remotehost.remote.net[68.10.1.7]" -> "zen.spamhaus.org"
[LOGS info]: count1 rbl: "2" -> "0"
[LOGS info]: query rbl: zen.spamhaus.org 7.1.10.68 (7.1.10.68.zen.spamhaus.org)
[LOGS info]: count2 rbl: "2" -> "0"
[LOGS info]: match rbl: FALSE
[LOGS info]: Action: dunno
the negation operator !!(<value>) has the highest priority and therefore gets evaluated first. then variable substitutions are performed:
postfwd3 -vv --cmd -r "id=TEST; action=REJECT; client_name=!!($$heloname)" /some/where/request.sample
will give
[LOGS info]: compare client_name: "unknown" -> "!!($$helo_name)"
[LOGS info]: negate client_name: "unknown" -> "$$helo_name"
[LOGS info]: substitute client_name: "unknown" -> "english-breakfast.cloud8.net"
[LOGS info]: match client_name: TRUE
[LOGS info]: Action: REJECT
๐ง Ruleset evaluation ๐ง
So here's the tea, bestie: a rule hits when all its items have matched โ and for a list, it counts as matched as soon as at least one element of that list matched. But the instant one item fails to compare against the request attribute (or all the elements of a list fail), the parser is like "k bye" and jumps straight to the next rule in the postfwd3 ruleset. No partial credit, no half-sends. ๐
Now, if a rule does match, there are exactly two possibilities:
- Rule returns a postfix action (
dunno,reject, ...) โ the parser stops rule processing and hands the action back to postfix. The other rules? They don't get evaluated. Periodt. - Rule returns a postfwd3 action (
jump(),note(), ...) โ the parser runs that action and then just keeps going with the next rule. Exception alert ๐จ: this does not apply to thejump()orquit()actions โ go read the "ACTIONS" section for the whole story on those. Nothing gets sent to postfix from this path.
And if no rule matched and you've hit the end of the ruleset, postfwd3 returns dunno without logging a single thing โ unless you're in verbose mode. Want different behaviour? Drop a last catch-all rule like this:
... <your rules> ...
id=DEFAULT ; action=dunno
That bad boy will log any request that passes through the whole ruleset without having hit a prior rule. So you'll actually know when something slips through the cracks ๐ซถ
โ๏ธ SETTINGS โ๏ธ
Since version postfwd3 2.00-pre6, the program settings can be pulled from a file, and that info has to be parseable by the perl eval() function. Comments are totally allowed, queen:
# example settings for postfwd3
{
# enable verbose logging
verbose => 1,
# user and group for postfwd
base => {
group => 'postfw',
user => 'postfw'
},
# server settings
server => {
proto => 'tcp',
host => '127.0.0.1',
port => '10099'
}
}
postfwd3 can also export the program settings in that same format using Data::Dumper. Iconic.
# show configuration
postfwd3 --showsettings
# save configuration to file
postfwd3 --savesettings=/path/to/file
# read configuration from file
postfwd3 --loadsettings=/path/to/file
You can use --loadsettings (or its short form -F) multiple times โ multiple usage is allowed, and the order will be kept. And any commandline arguments override whatever got pulled from a file. Slay.
# load base settings, override with node settings and set port and verbose logging
postfwd3 -v -p 10050 -F /path/to/basefile -F /path/to/nodefile
Samples for settings files are distributed in the etc/-folder of the postfwd3 tarball. Go look, it's all there ๐
๐ DEBUGGING ๐
Wanna debug specific steps of the parser? The --debug switch takes a list of debug classes. And since postfwd3 2.00, you can just ask for the whole menu of available debug classes by running:
postfwd3 --debugclasses
No guessing required ๐
๐ INTEGRATION ๐
Integration via daemon mode
The common way to use postfwd3 is to start it as a daemon, listening at a specified tcp port. postfwd3 will spawn multiple child processes, and those communicate with a parent cache. This is the prefered way to use postfwd3 in high volume environments. Start postfwd3 with these parameters:
postfwd3 -d -f /etc/postfwd.cf -i 127.0.0.1 -p 10045 -u nobody -g nobody -S
For efficient caching you should check if you can use the options --cacheid, --cache-rdomain-only, --cache-no-sender and --cache-no-size. ๐
Now check your syslogs (default facility "mail") for a line like:
Aug 9 23:00:24 mail postfwd[5158]: postfwd3 n.nn ready for input
...and use netstat -an|grep 10045 to check for something like
tcp 0 0 127.0.0.1:10045 0.0.0.0:* LISTEN
If everything works, open your postfix main.cf and insert the following:
127.0.0.1:10045_time_limit = 3600 <--- integration
smtpd_recipient_restrictions = permit_mynetworks <--- recommended
reject_unauth_destination <--- recommended
check_policy_service inet:127.0.0.1:10045 <--- integration
Reload your configuration with postfix reload and watch your logs. If it works, you should see lines like the following in your mail log:
Aug 9 23:01:24 mail postfwd[5158]: rule=22, id=ML_POSTFIX, client=english-breakfast.cloud9.net[168.100.1.7], sender=owner-postfix-users@postfix.tld, recipient=someone@domain.local, helo=english-breakfast.cloud9.net, proto=ESMTP, state=RCPT, action=dunno
If you want to check for size or rcpt_count items you must integrate postfwd3 in smtp_data_restrictions or smtpd_end_of_data_restrictions. And of course you can also specify a restriction class and use it in your access tables. First create a file /etc/postfix/policy containing:
domain1.local postfwdcheck
domain2.local postfwdcheck
...
Then postmap that file (postmap hash:/etc/postfix/policy), open your main.cf and enter:
# Restriction Classes
smtpd_restriction_classes = postfwdcheck, <some more>... <--- integration
postfwdcheck = check_policy_service inet:127.0.0.1:10045 <--- integration
127.0.0.1:10045_time_limit = 3600 <--- integration
smtpd_recipient_restrictions = permit_mynetworks, <--- recommended
reject_unauth_destination, <--- recommended
... <--- optional
check_recipient_access hash:/etc/postfix/policy, <--- integration
... <--- optional
Reload postfix and watch your logs. That's it, you're live ๐ฆ
Integration via docker
postfwd can be run in a docker container. The relevant options are --nodaemon and --stdout. More information can be found in the included doc/docker.html or at <http://postfwd.org/docker>. ๐ณ
๐งช TESTING ๐งช
First you have to create a ruleset (see the Configuration section). Check it with
postfwd3 -f /etc/postfwd.cf -C
There's an example policy request distributed with postfwd, called 'request.sample'. Simply change it to meet your requirements and use
postfwd3 --cmd -f /etc/postfwd.cf request.sample
You should get an answer like
action=<whateveryouconfigured>
For network tests I use netcat:
nc 127.0.0.1 10045 <request.sample
...to send a request to postfwd. If you receive nothing, make sure that postfwd3 is actually running and listening on the specified network settings. Vibe check your daemon first, bestie ๐ญ
๐ PERFORMANCE ๐
Some of these proposals might not match your environment. Please check your requirements and test new options carefully!
- use caching options
- use the correct match operator
==,<=,>= - use
^and/or$in regular expressions - use item lists (faster than single rules)
- use
set()action on repeated item lists - use jumps and rate limits
- use a pre-lookup rule for rbl/rhsbls with empty
note()action
๐ SEE ALSO ๐
See <http://www.postfix.org/SMTPD_POLICY_README.html> for a description of how Postfix policy servers work. ๐
๐ DONATIONS ๐
Development, testing and hosting of postfwd consumes time and ressources. It is and will stay free software (see "LICENSE" below). If you want to support this, consider a donation at <https://www.paypal.me/postfwd>. Every little bit is appreciated fr fr ๐ซถ
๐ LICENSE ๐
postfwd3 is free software and released under BSD license, which basically means that you can do what you want as long as you keep the copyright notice:
Copyright (c) 2009, Jan Peter Kessler All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of the authors nor the names of his contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY ME ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
โ๏ธ AUTHOR โ๏ธ
Jan Peter Kessler <info (AT) postfwd (DOT) org>. Let me know, if you have any suggestions. ๐
And that's the whole page, bestie โ we did it ๐ This lil masterpiece is from perl v5.30.3, dated 2020-07-30, and it's POSTFWD3(1). Thanks for reading all the way through, you absolute icon ๐ โจ
๐ฆ where this page comes from
- alpine 3.24: postfwd-doc 2.03-r1 ยท BSD-3-Clause
- arch rolling: postfwd 2.03-3 ยท bsd
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms