policyd-weight-0.1.15.2/0000755000000000000000000000000011675646211013375 5ustar rootrootpolicyd-weight-0.1.15.2/COPYING0000444000000000000000000000171711675646173014443 0ustar rootroot# Copyright (c) 2005-2010 Robert Felber (PC & IT Service Selling-IT, http://www.selling-it.de) # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # A copy of the GPL can be found at http://www.gnu.org/licenses/gpl.txt # Parts of code based on postfix-policyd-spf by Meng Wen Wong, version 1.06, # see http://spf.pobox.com/ policyd-weight-0.1.15.2/contrib/0000755000000000000000000000000010620541576015032 5ustar rootrootpolicyd-weight-0.1.15.2/contrib/rc-scripts/0000755000175000017500000000000010620541576020013 5ustar wernerwernerpolicyd-weight-0.1.15.2/contrib/rc-scripts/freebsd/0000755000175000017500000000000010620541576021425 5ustar wernerwernerpolicyd-weight-0.1.15.2/contrib/rc-scripts/freebsd/policyd-weight.sh0000555000175000017500000000166310620541576024720 0ustar wernerwerner#!/bin/sh # # # PROVIDE: policyd-weight # REQUIRE: LOGIN cleanvar # KEYWORD: shutdown # # Add the following lines to /etc/rc.conf to enable policyd-weight: # policyd_weight_enable (bool): # Set it to "YES" to enable policyd-weight. # Default is "NO". . /etc/rc.subr name="policyd-weight" rcvar=policyd_weight_enable start_cmd=policyd_weight_start stop_cmd=policyd_weight_stop # defaults policyd_weight_enable=${policyd_weight_enable:-"NO"} load_rc_config "policyd_weight" case "$policyd_weight_enable" in [Yy][Ee][Ss] | 1 | [Oo][Nn] | [Tt][Rr][Uu][Ee]) ;; *) echo "To make use of $name set $rcvar=\"YES\" in /etc/rc.conf" ;; esac command="/usr/local/libexec/postfix/policyd-weight" pidfile=/var/run/policyd-weight.pid policyd_weight_start() { /usr/local/libexec/postfix/policyd-weight start } policyd_weight_stop() { echo "Stopping $name" /usr/local/libexec/postfix/policyd-weight stop } run_rc_command "$1" policyd-weight-0.1.15.2/LICENSE0000444000000000000000000004313311675645770014415 0ustar rootroot GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. policyd-weight-0.1.15.2/changes.txt0000644000000000000000000005456611630434211015550 0ustar rootroot0.1.15 beta-2 - (maint) removed rbl.ipv6-world.net 0.1.15 beta-1 - (fix) if the (postfix-ly verified, i.e not unknown) client hostname belongs to the sender domain then this was not honoured WARNING: the fix required a rather invasive reordering, this might have broken other stuff. ALPHA ALPHA ALPHA! - (fix) cache: remove stale lock files if -k is specified. - (info) cache startup logging-context improved - (port) changed shebang to #!/usr/bin/env perl - (feat) Started IPv6 support by Jonas Genannt - (maint) removed dsbl.org, gone 0.1.14 beta-17 - (security) Using File::Spec->canonpath for normalization (trailing slashes) Check ownership of real directories to avoid race attacks for symlinks. Thanks to Robert Buchholz. 0.1.14 beta-16 (not released) - (security) The check for symlinked directories was half complete. perl ignores -l if the argument has a trailing slash. Thanks to Andrej Kacian. 0.1.14 beta-15 - (security) $LOCKPATH and its contents weren't checked for being a symlink which. Thanks to Chris Howells and Andrej Kacian. - (fix) "dedicated" added to the exclusion list for dialup checks. A better approach would be to let the user configure dialup and exclude patterns. 0.1.14 beta-14 - (change) rbls.org link changed to robtext.com - (change) results with 'rc:' as action are not cached - (fix) regexp check for dynamic helo/client did hit also some clients with "static" - (fix) helo numeric check was too fuzzy. - (fix) master didn't read config after policyd-weight reload - (fix) HELO_SEEMS_DIALUP may have scored even if the IP is listed for the sender domain. - (fix) An interrupt of policyd-weight -s may cause a SIGPIPE which killed the cache - (change) Implemented $NS list. Useful for users with split horizon DNS - (fix) don't cache rejections which were deferred (4xx and friends) - (fix) helo_numeric_score didn't catch [n.n.n.n] helos - (fix) Header was not included if $dnsbl_checks_only = 1; and $ADD_X_HEADER = 1; - Thanks to J. Genannt - (fix) Corrected handling of [n.n.n.n] HELOs and address-literals as sender (long standing issue) - (change) Introduced @dnsbl_checks_only_regexps in order to skip DNS checks for certain client hostnames - (change) Added -D (Don't detach) switch for daemon-tools/runit users - (change) Added signals handlers for most of signals so that they are at least logged, also, provide a perl backtrace. - (change) prerequisite steps for providing coredumps (build coredump directories, chdir) - coredumps are non-trivial: we start as root, change uid. At this moment coredumps are denied by kernel in order to protect root-data. The only workaround would be, to start cache and master via system() after changing uid - (change) In daemon mode wrongly crafted policy requests don't lead to a child-exit anymore, only the connection is closed - (change) log-facilities other than 'info' are now mentioned in log-lines - (change) SMTP information such as client, helo, sender and to are now logged in each log-message. If $DEBUG is set this also logs the instance variable. - (fix) rbl_lookup used sometimes 65536 as packet id which appeared to cause problems - (fix) Check for syslog absence. If syslog is not available then log temporarily to $LOCKPATH/polw-emergency.log - (tmpfix) Introduced $TRY_BALANCE which closes connections to smtpds after they got their response in order to avoid too many established smtpd->policyd-weight (child) connections. 0.1.14 beta-5 - (fix) A lookups were not performed if MX lookups returned NXDOMAIN. Thanks to H. Krohns. - (change) Reverse IP == dynhost check corrected such that hosts which have smtp, mail, mx, or static in their FQDN are not treated as dynamic clients. Also the regex for rev.*(home|ip) was corrected. Thanks to H. Krohns. 0.1.14 beta-4 - (fix) Corrected handling of SERVFAIL and timeouts Thanks to H. Krohns - (fix) DNSERRMSG wasn't re-initialized, thus leading to subsequent concatenated responses. Thanks to H. Krohns 0.1.14 beta-3 - (change) Replaced dynablock.njabl.org with pbl.spamhaus.org 0.1.14 beta-2 - (fix) FreeBSD: rc cannot handle programs with "-" (dash) in its name correctly. Init script for FBSD does now provide its own stop|start functions. - (change) removed dnsbl_hits increasements where the check in question was not a dnsbl check, same goes for total_dnsbl_score - (change) Termination of daemon changed. The daemon tries to terminate childrens himself. - (fix) rbl_lookup used sometimes 0 for the random DNS packet identifier. It also checked the presence and correctness of this field wrong. (non critical) - (new) $enforce_dyndns_score added With this one can control how much he wants to avoid dynamic clients which do not use DynDNS - (change) The cache now uses only one query to ask for HAM|SPAM. Thanks to H. Krohns. - (change) the $dnsbl_hits score gets only increases if the RBL is a blacklist. This is required to use whitelists in the $dnsbl_score list. Thanks to 'Steve'. - (change) The cache can now return hard-whitelists. Enforce N days/hours RBL checks for HAM clients. After N days do each P hours or each R requests a RBL check for HAM cached clients. This should save a good amount of RBL DNS traffic. - (change) The delay in seconds for each policy request is now logged via "decided action" - (change) The time required for a cache cleanup is now logged, too. - (scores) @helo_seems_dialup = (1.5,0) 'dsn.rfc-ignorant.org', 3.5, 0, 'DSN_RFCI', ordb.org removed - (fix) alarms didn't work on every platform, leading to timeouts - (fix) BAD_MX might score even though the sender has an A record which matches the client IP - (cleanup) use modules at the beginning, usage() printed as here-doc Suggested by Francis Galiegue - (cleanup) Use A queries instead of TXT queries for RBL lookups - (new) Cache only the IP if the client was too much DNSBL listed or the client seems to be a dialup or the helo/from verification against IP and subnets failed totally Should help against Dictionary Attacks - (fix) On some plattforms setting correct user and groupmemberships is a hard task, we try now two ways to accomplish this - (new) One can define to return a restriction class by setting the appropriate messages to "rc:your_restriction_class" Policyd-weight ignores trailing garbage after rc:text messages Example: $REJECTMSG = "rc:greylist"; - (new) Non-responsive RBLs are skipped for a certain amount of queries. 0.1.14 beta - (fix) $< enabled taint mode - which made policyd-weight crash in case of troublesome config files - (change) Messages due to die() were reported not appropriate. Old $! variables may have lead to false assumptions. - (change) FROM_MATCHES_NOT_HELO now also checks whether the SENDER MX lists a host which matches HELO domain (minimizing false positives or inappropriate spam-scoring) - (vuln fix) When $DEBUG = 1; is used then policyd-weight might be vulnerable to a remote format-string attack if the MTA does not avoid it. Also it is open for a local format-string attack in case of $DEBUG = 1;. A syslog message of the cache query could have taken foreign format-string sequences which would have been executed with old Sys::Syslog modules. - (change) Floating point numbers sanitized - (change) Kids didn't die verbose - (fix) call close on DNS sockets only if a socket exists and is connected - (change) Initialization of syslog socket exits now informative if it cannot be setup. - (change) RHSBL lookups are now half-dnsbl influenced. - (fix) The perl POSIX module of SuSe 9.0 is buggy. We don't use POSIX for setting UID/EUID/GID/EGID anymore but the provided perl vars from man perlvar | less +/UID - (change) -f switch added to hand a configuration file to policyd-weight. - (fix) FROM_NOT_HELO was too heavily DNSBL influenced - (fix) Communication between parent and childs wasn't portable Solaris versions of perl didn't unterstand $sock->send for socketpair() created IPC-channels - (change) RFCI postmaster and abuse list changed from 1 to 0.1 rhsbl_penalty_score changed from 3.3 to 3.1 - (change) MAXIDLE changed from 120 to 240 MIN_PROC changed from 2 to 3 POCACHEMAXSIZE changed from 380 to 480 CACHEMAXSIZE changed from 380 to 480 - (change) Scores are now computed once, and not twice each check. Which is just a bit more CPU friendly. - (change) HELO which couldn't be verified to match IP weren't DNSBL influenced, i.e. the score increases now when also DNSBL-listed. - (change) syslogs the used config file - (change) shows GROUP infos in debug mode - (new) Added "default" action. 'policyd-weight default' will show its defaults and exit. Patch supplied by Philipp Koller, thanks. - (fix) FROM_MULTIPARTED check made less aggressive. Hosts whose sender or helo verify to the client-ip are not checked for multiple lables. This is a cure for yahoo groups - (new) inet/daemon mode support added, default port 12525 - (new) checks for bogus MXes of MAIL FROM arg added (empty, 127/8, 192.168/16, 10/8, 172.16/12) if this check gives true then the mail is DEFERed instead of REJECTed This check also increases results of other checks - (new) Checks for randomized sender addresses added - (change) rhsbl check changed as such, that all rhslb entries are queried now - (change) surbl.org added to the default rhsbl hosts - (fix) The routine to terminate cache versions prior to 0.1.12 beta was buggy. fixed. - (change) Included the possibility to DEFER instead of REJECT mail on configurable strings if the overall score is below DEFER_LEVEL. This way one can DEFER mail on e.g. " IN_SPAMCOP=" listings. - (fix) Log entry for FROM_MATCHES_NOT_HELO corrected - (change/new) Log outputs also recipient - (new) own rbl_lookup routine implemented (for reference see http://www.policyd-weight.org/rbl_lookup.html) This reduces about 85% of total time and 95% CPU time for RBL Lookups. If the routine seems to give buggy results you might try to set $USE_NET_DNS = 1; although it might still be buggy. It uses Net::DNS automatically for perl versions prior to 5.8 - (change/fix) Cache queries made more reliable. Timeouts implemented - (fix) PTR vs HELO/FROM was case-sensitive -------------------------------------------------------------------------------- 0.1.12 beta-4 - don't perform multiparted check of sender domain if client is an MX for that domain (fix) - cache cleanup process optimized by 80% - HELO vs FROM check made more reliable (fix) - CVERSION introduced which replaces the "detect if script has changed" routine which means, that the cache is only being killed on updated when CVERSION changes. -------------------------------------------------------------------------------- 0.1.12 beta - improved multirecipient awareness. It is now possible to build up restriction classes within postfix to either explicitly say "check policy service" or to make user exceptions. This is important for ISP. This was not possible with previous versions. - -d debug switch added. In debug mode nothing is sent to syslog but STDOUT also it turns on Net::DNS debugging It prints some perl/OS/Net:DNS/policyd-weight version infos and configuration this switch is NOT FOR USE IN MASTER.CF - permission/accessibility checks for configuration files added. Syslog if either permission denied, or config is world-writeable. Recommended mode is 0644 and owner root, group root (or wheel on bsd). - cache outsourced to an own cache daemon. Decreases drastically frequent DNS lookups and thus network delays and CPU time. For security reasons policyd-weight must not run as nobody or root. Set up an own user for that and update master.cf (user=$your_user) Several configuration items for the cache have been added - some scores adjusted to let pass DynDNS MX users with a envelope of foo@bar.dyndns.org Also the spamcop score has been lowered - helo_from_mx_eq_ip_score added - some more scores adjusted - FROM Domain vs HELO regex check adjusted - Process UID check added, policyd-weight must have it's own user. Update master.cf - dynmaic clients whose score cause a REJECT will be rejected with a note: "; please relay via your ISP ($from_domain)" - critical fix: First perform Sender Domain MX lookups. If the Client is a MX for that Domain, don't do HELO vs FROM pattern matching. - Halved the weight of RBL results agains HELO/FROM pattern mismatches. - removed scoring for HELO == dynamic host regexp check if client address == dynhost check was true. This might (and will) permit more spam to get through. But also some dynamic host MTAs which don't use dyndns possibilities. -------------------------------------------------------------------------------- 0.1.11 beta - (fix) Using of appropriate methods for fetching truncated packets via TCP Net::DNS version < 0.50: igntc() (ignore truncated packets) Net::DNS version >= 0.50 force_v4() (force IPv4 usage) - X-policyd-weight header for multirecipient mail is now inserted only once - Caching of spam-results happens only if no DNS error (timeout) occured - RHSBL results are appended at the reject-message - Messages to STDERR end now in nirvana to don't confuse the SMTPD STDERR messages caused by a die() end up in syslog - Config errors end in syslog, if config file couldn't be loaded due to a syntax error then we fall back to builtin defaults and append a message to X-policyd-weight header. - Scores for from_match_regex_unverified_helo and helo_ip_in_cl16_subnet adjusted to let pass msn.com mail relayed via hotmail.com - Order and scores for RHSBL entries adjusted - (fix) The special recipients postmaster and abuse pass now with DUNNO instant. This was the case for virtual domains. - (fix) The array for the reverse IP lookup result was build wrong, in some circumstances this may lead to an empty array and thus some _badly_ configured mailer with incorrect DNS (those with broken forward DNS) may have been blocked. - (fix) NULL (<>) Sender now pass (RFC compliance) - LOG_BAD_RBL_ONLY added which logs only successfull RBL hits. If there was no RBL hit, but the "good" score was not equal zero, it is logged though. Default is 1 (ON). -------------------------------------------------------------------------------- 0.1.10 beta - Caching of positive and negative results added - (fix) improved error-handling on DNS timeouts and empty objects. - code optimizations DNS Resolver is created in main reverse IP records get fetched only one time - cosmetic changes (leading tabs substituted with blanks) -------------------------------------------------------------------------------- 0.1.9 beta - RHSBL support added - dnsbl_checks_only switch added - X-policyd-weight: header on/off switchable - DNSBLMAXSCORE added - config file support added - multipart FROM check/scoring added - Reverse IP == dynhost check added - Net::DNS retries and retry interval changed - Net::DNS support for persistant udp sockets added - Net::DNS igntc option set to on (0.53 has bugs with truncated packets and tcp connections) - minor code cleanups (loops removed, regexps optimized, etc) for speedup - FreeBSD: first GPLed version -------------------------------------------------------------------------------- 0.1.8.1 beta - set under GPL (http://www.gnu.org/licenses/gpl.txt) -------------------------------------------------------------------------------- 0.1.8 beta - Return DUNNO in case of IPv6 Clients - Splitted NJABL to treat dyn RBL listed clients different - some regex made case-insensitive - More details for the foreign MTA if HELO checks failed - Little cleanups for better reading -------------------------------------------------------------------------------- 0.1.7 beta - REV_IP_EQ_HELO_DOMAIN regex corrected again - DNSBL scores adjusted - $total_dnsbl_score added which holds the overall score of positive DNSBL scores. This affects HELO/IP verification - Return message for too many DNSBL hits changed, rbl.org link added to this message - Mails pass now with PREPEND instead of DUNNO and adds a X-policyd-weight header containing the detailed score evaluation plus rate -------------------------------------------------------------------------------- 0.1.6 beta - if HELO IP is in /24 of Client IP then it is treated as helo_ok (this cause less false positives for MTAs which use a different HELO hostname/IP than MTA's hostname/IP; but are in the same domain/subnet - badly written/administrated www mail interfaces are such a candidate) -------------------------------------------------------------------------------- 0.1.5 beta - Cleanup (@array[0] changed to $array[0]) - regexp for REV_IP_EQ_HELO_DOMAIN corrected (again) - typos fixed - HELO_IP_IN_CL_SUBNET made configurable -------------------------------------------------------------------------------- 0.1.4 beta - checks for dialup HELOs added - failed HELO checks for dialup HELOs now increase dnsb_hits counter -------------------------------------------------------------------------------- 0.1.3 beta - regexp for REV_IP_EQ_HELO_DOMAIN corrected -------------------------------------------------------------------------------- 0.1.2 beta - REV_IP_EQ_HELO_DOMAIN check rewritten. It checks now only the part before TLD. HELO foo.bar.com Client Host: blah.bar.com It checks now, whether the client or HELO "bar" matches against HELO or client "bar". -------------------------------------------------------------------------------- 0.1.1 beta - REV_IP_EQ_HELO_DOMAIN did not really a domain check, now it does. -------------------------------------------------------------------------------- 0.1.0 beta - state changed to beta - some planned knobs removed - name changed to policyd-weight -------------------------------------------------------------------------------- 0.0.18 alpha - changed /24 score to -0.6 - FROM_MATCHES_NOT_HELO gets extra score per DNSBL hit - if correct MX record for helo, it gets plus -0.5 -------------------------------------------------------------------------------- 0.0.17 alpha - using now MAXDNSBLHITS. Above this level the mail gets REJECTed immediately. - checking client IP against helo IPs now also tries a /24 check as last resort. The results of this check may reduce the score by -0.20. A CIDR check will never be performed as this is too expensive. -------------------------------------------------------------------------------- 0.0.16 alpha - added ix.dnsbl.manitu.net -------------------------------------------------------------------------------- 0.0.15 alpha - (fix) gettings MX/A records now also asks the MAIL FROM: domain/host (reducing "false positives" if client messed up HELO but the from domain has correct DNS records and matches client IP) -------------------------------------------------------------------------------- 0.0.14 alpha - If MX/A query failed, it gets lower scored than MX/A forged - More verbose output - If _ALL_ DNS queries returned NXDOMAIN then return with 450 and DNSERRMSG when not too much dnsbl listed -------------------------------------------------------------------------------- 0.0.13 alpha - (fix) getting MX/A records of HELO now also asks parent domains -------------------------------------------------------------------------------- 0.0.12 alpha - (fix) perl DNS module caused warnings and server misconfigured errors when MX record pointed to a CNAME and we treated it as A-record (CNAME RR: print $foo->address == error) -------------------------------------------------------------------------------- 0.0.11 alpha - added dnsbl.org -------------------------------------------------------------------------------- 0.0.10 alpha - set $VERBOSE to default 0 -------------------------------------------------------------------------------- 0.0.9 alpha - removed all other handlers, since the # push @foo, "bar"; seems to be ignored on some systems (NOTE: '#`-lines should NEVER get parsed by perl) NOTE: I am dumb. VERBOSE was default 1, and my syslog debug ends not in maillog. I thought it ignored the commenting of "testing". -------------------------------------------------------------------------------- 0.0.8 alpha - Changed spamcop back to 4 since it would outweight legitimate mails if they are accidentially listed in spamcop (happened in the past (gmx, web.de)) And that is not the purpose of this script. -------------------------------------------------------------------------------- 0.0.7 alpha - Gave spamcop a score of 8 because it seems reasonable and updated fast and is not a DUL list -------------------------------------------------------------------------------- 0.0.6 alpha - Client IPs which had no MX, A, PTR record at all did not get scored extra. - tuned scores some more - unneeded handlers removed from code (cleanup) policyd-weight-0.1.15.2/todo.txt0000644000000000000000000000342011630434225015071 0ustar rootrootItem Status Req. for In Progress: P stable Done in devel: D ------------------------------------------------------------------------------- - introduce IPv6 RBLs only for IPv6 clients Y - Multiplexing policy requests and DNS Y queries via select() - probably milter capabilities - probably external hooks at certain stages - porting for other MTAs (helpers welcome) - packaging for other systems (helpers welcome) - IPv6 support Need someone who can do this, since we have no IPv6 environment for testing here. For now IPv6 clients are recognized and pass unchecked. - proper documentation (helpers welcome) P Y - man page for config settings P Y - probably SPF support (scored) Problem: DUL listed DynDNS MX users might get rejected if they use a "mail from:" like user@yahoo.com but deliver direct. - SNSD (Spammy NS Detection) Y Shall detect NS servers which excessive host Spam-Domains - ADD (Abused Domain Detection) Y Shall detect Domains which are frequently forged and may not be used by Dynamic Clients unless they got a DynDNS MX as verified HELO - postfix restriction classes support P Y (postfix doesn't recognize "restrictionclass some explanatory text" as restriction-class-request even though "restrictionclass" is defined in main.cf) - review cache efficiency and avoidance of P Y dictionary attacks policyd-weight-0.1.15.2/man/0000755000000000000000000000000011675645501014151 5ustar rootrootpolicyd-weight-0.1.15.2/man/man8/0000755000000000000000000000000011675645570015022 5ustar rootrootpolicyd-weight-0.1.15.2/man/man8/policyd-weight.80000644000000000000000000001152511346523475020041 0ustar rootroot.TH policyd-weight 8 "Aug 25th, 2006" .SH "NAME" policyd-weight \- weighted SMTP policy daemon .SH "STATUS" Beta, Documentation incomplete .SH "SYNOPSIS" .na .nf .fi \fBpolicyd-weight\fR [\fB-option\fR] [\fB-option2 \fR] \fIcommand\fR .SH "DESCRIPTION" .ad .fi \fBpolicyd-weight\fR(8) is a SMTP policy daemon written in \fBperl\fR(1) for \fBpostfix\fR(1). It evaluates based on RBL/RHSBL results, HELO and MAIL FROM domain and subdomain arguments and the client IP address the possibility of forgery or SPAM. It is designed to be called before the SMTP DATA command at the RCPT TO stage. This way it is a) possible to reject a mail attempt before the body has been received and b) to keep multirecipient mail intact, i.e. provide the functionality of selective usage based on recipients. To make \fBpolicyd-weight\fR(8) work with \fBpostfix\fR(1), it is required to add a system account for $USER (default: polw) Policyd-weight can operate in master.cf \fBor\fR daemon mode. In master.cf mode it uses postfix' \fBspawn\fR(8), which results in number of simultanous requests perl instances. In daemon mode it uses shared memory and forks on load, and only if all childs are busy. At the time of writing the man-pages for policyd-weight assume a postfix installation. It has been reported that policyd-weight works with other MTAs like Exim, too. .SH "SETUP" .SH "master.cf mode:" .IP "\fBmaster.cf:\fR" .in -7 policy unix - n n - - spawn user=polw .br \ \ \ argv=/usr/bin/perl /usr/local/bin/policyd-weight .IP "\fBmain.cf:\fR" .in -7 smtpd_recipient_restrictions = .br \ \ \ permit_mynetworks, .br \ \ \ ... authenticated permits ... .br \ \ \ reject_unauth_destination, .br \ \ \ ... whitelists, role accounts, clients ... .br \ \ \ check_policy_service unix:private/policy .br .in 7 .SH "daemon mode:" start the daemon with \fBpolicyd-weight start\fR. Poliyd-weight then listens on $TCP_PORT (default: 12525) for policy requests. To make postfix talk to that port do following changes to main.cf: .IP "\fBmain.cf:\fR" .in -7 smtpd_recipient_restrictions = .br \ \ \ permit_mynetworks, .br \ \ \ ... authenticated permits ... .br \ \ \ reject_unauth_destination, .br \ \ \ ... whitelists, role accounts, clients ... .br \ \ \ check_policy_service inet:127.0.0.1:12525 .br .in 7 It is possible to have more than one postfix server talk to the \fBdaemonized\fR policyd-weight by configuring each postfix machine to query the policy server with check_policy_service inet:IP:12525 where IP is the host on which policyd-weight runs. Please note that \fBcheck_policy_service\fR should come at last, or at least \fBafter\fR reject_unauth_destination, or else you may become an open relay. .SH "COMMANDS" .ad .fi Following commands exist and are reserved for daemon mode only: .IP "\fBstart\fR start the policy server" .IP "\fBstop\fR stop the policy server" .IP "\fBrestart\fR restart the policy server" .IP "\fBreload\fR tells the policy server to reload its configuration" .IP "\fBdefaults\fR prints the default settings to STDOUT and exits" .SH "OPTIONS" .ad .fi .IP "\fB-d\fR operate in debug mode" \fBNot for use in master.cf\fR. In debug mode everything is reported on STDOUT instead of \fBsyslog\fR(3). Also an own debug cache daemon will be spawned. The socket-file is named after the value of $SPATH with ".debug" as suffix. .IP "\fB-f\fR /path/to/file Pass a configuration file to policyd-weight .IP "\fB-h\fR show help" .IP "\fB-k\fR kill cache daemon" \fBNot for use in master.cf\fR. Together with \fB-d\fR this kills the debug cache daemon. Without \fB-d\fR it kills the global running cache daemon. .IP "\fB-s\fR show cache entries" \fBNot for use in master.cf\fR. .IP "\fB-v\fR show version" .SH "LOGGING" .ad .fi Logging is done via \fBsyslog\fR(3) with facility "mail" and priority "info". For a complete list of log entries and their correspondending configuration parameters refer to \fBpolicyd-weight.conf\fR(5). .SH "BUGS" .na .nf Please report bugs to r.felber@ek-muc.de .SH "HISTORY" .ad .fi .IP "March 2005" Ralf Hildebrandt (Author of the Book of Postfix) is the spiritual father of policyd-weight. It was his idea to have a scored RBL evaluation, I've added the weighted MAIL FROM/HELO DNS-evaluation. For that purpose I used Meng Wong's spf.pl which was shipped with the postfix source as example. .SH "FILES" .na .nf /etc/policyd-weight.conf, Policyd-weight configuration file /etc/postfix/main.cf, Postfix configuration parameters /etc/postfix/master.cf, Postfix daemon processes .fi .SH "SEE ALSO" .na .nf policyd-weight.conf(5), Policyd-weight configuration file master(5), Postfix master.cf file syntax postconf(5), Postfix main.cf file syntax access(5), Postfix SMTP access control table .SH "LICENSE" .na .nf GNU General Public License .SH "AUTHOR" .na .nf Robert Felber Autohaus Erich Kuttendreier 81827 Munich, Germany policyd-weight-0.1.15.2/man/man5/0000755000000000000000000000000011675645542015016 5ustar rootrootpolicyd-weight-0.1.15.2/man/man5/policyd-weight.conf.50000644000000000000000000002515511346523511020752 0ustar rootroot.TH policyd-weight.conf 5 "Aug 25th, 2006" .ad .fi .SH "NAME" policyd-weight.conf \- policyd-weight configuration parameters .SH "STATUS" Beta, Documentation incomplete .SH "DESCRIPTION" .ad .fi \fBpolicyd-weight\fR uses a \fBperl\fR(1) style configuration file which it reads on startup. The cache re-reads the configuration after \fB$MAINTENANCE_LEVEL\fR (default: 5) queries. If \fB-f\fR is not specified, it searches for configuration files on following locations: .P /etc/policyd-weight.conf .br /usr/local/etc/policyd-weight.conf .br ./policyd-weight.conf .SH "CACHE SETTINGS" .ad .fi .IP "\fB$CACHESIZE\fR (default: 2000)" Set the minimum size of the SPAM cache. .IP "\fB$CACHEMAXSIZE\fR (default: 4000)" Set the maximum size of the SPAM cache. .IP "\fB$CACHEREJECTMSG\fR .br (default: 550 temporarily blocked because of previous errors)" Set the SMTP status code and a explanatory message for rejected mails due to cached results .IP "\fB$NTTL\fR (default: 1) The client is penalized for that many retries. .IP "\fB$NTIME\fR (default: 30) The \fB$NTTL\fR counter will only be decremented if the client waits at least \fB$NTIME\fR seconds. .IP "\fB$POSCACHESIZE\fR (default: 1000)" Set the minimum size of the HAM cache. .IP "\fB$POSCACHEMAXSIZE\fR (default: 2000)" Set the maximum size of the HAM cache. .IP "\fB$PTTL\fR (default: 60)" After that many queries the HAM entry must succeed one run through the RBL checks again. .IP "\fB$PTIME\fR (default: 3h)" after $PTIME in HAM Cache the client must pass one time the RBL checks again. Values must be nonfractal. Accepted time-units: s(econds), m(inutes), h(ours), d(ays) .IP "\fB$TEMP_PTIME\fR (default: 1d)" The client must pass this time the RBL checks in order to be listed as hard-HAM. After this time the client will pass immediately for PTTL within PTIME. Values must be non-fractal. Accepted time-units: s(econds), m(inutes), h(ours), d(ays) .SH "DEBUG SETTINGS" .ad .fi .IP "\fB$DEBUG\fR (default: 0)" Turn debugging on (1) or off (0) .SH "DNS SETTINGS" .ad .fi .IP "\fB$DNS_RETRIES\fR (default: 2)" .br How many times a single DNS query may be repeated .IP "\fB$DNS_RETRY_IVAL\fR (default: 2)" .br Retry a query without response after that many seconds .IP "\fB$MAXDNSERR\fR (default: 3)" .br If that many queries fail, the mail is accepted with \fB$MAXDNSERRMSG\fR. .br In total DNS queries this means: $MAXDNSERR * $DNS_RETRIES .SH "MISC SETTINGS" .ad .fi .IP "\fB$MAINTENANCE_LEVEL\fR (default: 5)" After that many policy requests the cache (and in daemon mode childs) checks for configuration file changes .IP "\fB$MAXIDLECACHE\fR (default: 60)" After that many seconds of being idle the cache checks for configuration file changes. .IP "\fB$PIDFILE\fR (default: /var/run/policyd-weight.pid)" Path and filename to store the master pid (daemon mode) .IP "\fB$LOCKPATH\fR (default: /tmp/.policyd-weight/)" Directory where policyd-weight stores sockets and lock-files/directories. Its argument must contain a trailing slash. .IP "\fB$SPATH\fR (default: $LOCKPATH.'/polw.sock')" Path and filename which the cache has to use for communication. .IP "\fB$TCP_PORT\fR (default: 12525)" TCP port on which the policy server listens (daemon mode) .IP "\fB$BIND_ADDRESS\fR (default: '127.0.0.1')" IP Address on which policyd-weight binds. Currently either only one or all IPs are supported. Specify 'all' if you want to listen on all IPs. .IP "\fB$SOMAXCONN\fR (default: 1024)" Maximum connections which policyd-weight accepts. This is set high enough to cover most scenarios. .IP "\fB$USER\fR (default: polw)" Set the user under which policyd-weight runs .IP "\fB$GROUP\fR (default: $USER)" Set the group under which policyd-weight runs .SH "OUTPUT AND LOG SETTINGS" .ad .fi .IP "\fB$ADD_X_HEADER\fR (default: 1)" Insert a X-policyd-weight: header with evaluation messages. .br 1 = on, 0 = off .IP "\fB$LOG_BAD_RBL_ONLY\fR (default: 1)" Insert only RBL results in logging strings if the RBL score changes the overall score. Thus RBLs with a GOOD SCORE of 0 don't appear in logging strings if the RBL returned no BAD hit. .br 1 = on, 0 = off .IP "\fB$MAXDNSBLMSG\fR (default: 550 Your MTA is listed in too many DNSBLs)" The message sent to the client if it was reject due to \fB$MAXDNSBLHITS\fR and/or \fB$MAXDNSBLSCORE\fR. .IP "\fB$REJECTMSG\fR (default: 550 Mail appeared to be SPAM or forged. Ask your Mail/DNS-Adminisrator to correct HELO and DNS MX settings or to get removed from DNSBLs)" .br Set the SMTP status code for rejected mails and a message why the action was taken .SH "RESOURCE AND OPTIMIZATIONS" .ad .fi .IP "\fB$CHILDIDLE\fR (default: 120)" How many seconds a child may be idle before it dies (daemon mode) .IP "\fB$MAX_PROC\fR (default: 50)" Process limit on how many processes policyd-weight will spawn (daemon mode) .IP "\fB$MIN_PROC\fR (default: 2)" Minimum childs which are kept alive in idle times (daemon mode) .IP "\fB$PUDP\fR (default: 0)" .br Set persistent UDP connections used for DNS queries on (1) or off (0). .SH "SCORE SETTINGS" .ad .fi Positive values indicate a bad (SPAM) score, negative values indicate a good (HAM) score. .IP "\fB@bogus_mx_score\fR (2.1, 0)" If the sender domain has neither MX nor A records or these records resolve to a bogus IP-Address (for instance private networks) then this check asigns the full score of bogus_mx_score. If there is no MX but an A record of the sender domain then it receives a penalty only if DNSBL-listed. Log Entries: \fBBOGUS_MX\fR .in +1 The sender A and MX records are bogus or empty. .in -1 \fBBAD_MX\fR .in +1 The sender domain has an empty or bogus MX record and the client is DNSBL listed. .in -1 Related RFCs: [1918] Address Allocation for Private Internets .br [2821] Simple Mail Transfer Protocol (Sect 3.6 and Sect 5) .IP "\fB@client_ip_eq_helo_score\fR (1.5, -1.25)" Define scores for the match of the reverse record (hostname) against the HELO argument. Reverse lookups are done, if the forward lookups failed and are not trusted. Log Entries: \fBREV_IP_EQ_HELO\fR .in +1 The Client's PTR matched the HELO argument. .in -1 \fBREV_IP_EQ_HELO_DOMAIN\fR .in +1 Domain portions of Client PTR and HELO argument matched. .in -1 \fBRESOLVED_IP_IS_NOT_HELO\fR .in +1 Client PTRs found but did not match HELO argument. .in -1 .IP "\fB@helo_score\fR (1.5, -2)" Define scores for the match of the Client IP and its /24 subnet against the A records of HELO or MAIL FROM domain/host. It also holds the bad score for MX verifications. Log Entries: \fBCL_IP_EQ_HELO_NUMERIC\fR .in +1 Client IP matches the [IPv4] HELO. .in -1 \fBCL_IP_EQ_FROM_IP\fR .in +1 Client IP matches the A record of the MAIL FROM sender domain/host. .in -1 \fBCL_IP_EQ_HELO_IP\fR .in +1 Client IP matches the A record of the HELO argument. .in -1 \fBCL_IP_NE_HELO\fR .in +1 The IP and the /24 subnet did not match A/MX records of HELO and MAIL FROM arguments and their subdomains. .in -1 .IP "\fB@helo_from_mx_eq_ip_score\fR (1.5, -3.1)" Define scores for the match of Client IP against MX records. Positive (SPAM) values are used in case the MAIL FROM matches not the HELO argument \fBAND\fR the client seems to be dynamic \fBAND\fR the client is no MX for HELO and MAIL FROM arguments. The total DNSBL score is added to its bad score. Log Entries: \fBCL_IP_EQ_FROM_MX\fR .in +1 Client IP matches the MAIL FROM domain/host MX record .in -1 \fBCL_IP_EQ_HELO_MX\fR .in +1 Client IP matches the HELO domain/host MX record .in -1 \fBCLIENT_NOT_MX/A_FROM_DOMAIN\fR .in +1 Client is not a verified HELO and doesn't match A/MX records of MAIL FROM argument .in -1 \fBCLIENT/24_NOT_MX/A_FROM_DOMAIN\fR .in +1 Client's subnet does not match A/MX records of the MAIL FROM argument .in -1 .IP "\fB$dnsbl_checks_only\fR (default: 0)" Disable HELO/RHSBL verifications and the like. Do only RBL checks. .br 1 = on, 0 = off .IP "\fB@dnsbl_score\fR (default: see below)" A list of RBLs to be checked. If you want that a host is not being evaluated any further if it is listed on several lists or a very trustworthy list you can control a immediate REJECT with \fB$MAXDNSBLHITS\fR and/or \fB$MAXDNSBLSCORE\fR. A list of RBLs must be build as follows: .br @dnsbl_score = ( .br RBLHOST1, HIT SCORE, MISS SCORE, LOG NAME, .br RBLHOST2, HIT SCORE, MISS SCORE, LOG NAME, .br ... .br ); .br The default is: @dnsbl_score = ( "dynablock.njabl.org", 3.25, 0, "DYN_NJABL", "dnsbl.njabl.org", 4.25, -1.5, "BL_NJABL", "bl.spamcop.net", 1.75, -1.5, "SPAMCOP", "sbl-xbl.spamhaus.org", 4.35, -1.5, "SBL_XBL_SPAMHAUS", "list.dsbl.org", 4.35, 0, "DSBL_ORG", "ix.dnsbl.manitu.net", 4.35, 0, "IX_MANITU", "relays.ordb.org", 3.25, 0, "ORDB_ORG" .br ); .IP "\fB@rhsbl_score\fR (default: see below)" Define a list of RHSBL host which are queried for the sender domain. Results get additionaly scores of 0.5 * DNSBL results and \fB@rhsbl_penalty_score\fR. A list of RHSBL hosts to be queried must be build as follows: .br @rhsbl_score = ( .br RHSBLHOST1, HIT SCORE, MISS SCORE, LOG NAME, .br RHSBLHOST2, HIT SCORE, MISS SCORE, LOG NAME, .br ... .br ); .br The default is: @rhsbl_score = ( "rhsbl.ahbl.org", 1.8, 0, "AHBL", "dsn.rfc-ignorant.org", 3.2, 0, "DSN_RFCI", "postmaster.rfc-ignorant.org", 1 , 0, "PM_RFCI", "abuse.rfc-ignorant.org", 1, 0, "ABUSE_RFCI" .br ); .IP "\fB@rhsbl_penalty_score\fR (3.1, 0)" This score will be added to each RHSBL hit if following criterias are met: Sender has a random local-part (i.e. yztrzgb@example.tld) or MX records of sender domain are bogus or FROM matches not HELO or HELO is untrusted (Forward record matched, reverse record did not match) .IP "\fB$MAXDNSBLHITS\fR (default: 2)" If the client is listed in more than $MAXDNSBLHITS RBLs it will be rejected immediately with \fB$MAXDNSBLMSG\fR and without further evaluation. Results are cached by default. .IP "\fB$MAXDNSBLSCORE\fR (default: 8)" If the BAD SCOREs of \fB@dnsbl_score\fR listed RBLs reach a level greater than $MAXDNSBLSCORE the client will be rejected immediately with \fB$MAXDNSBLMSG\fR and without further evaluation. Results are cached by default. .IP "\fB$REJECTLEVEL\fR (default: 1)" Score results equal or greater than this level will be rejected with \fB$REJECTMSG\fR .SH "SEE ALSO" .na .nf policyd-weight(8), Policyd-weight daemon perl(1), Practical Extraction and Report Language perlsyn(1), Perl syntax access(5), Postfix SMTP access control table .IP .SH "LICENSE" .na .nf GNU General Public License .SH "AUTHOR" .na .nf Robert Felber Autohaus Erich Kuttendreier 81827 Munich, Germany policyd-weight-0.1.15.2/policyd-weight0000755000000000000000000034712411630433740016256 0ustar rootroot#!/usr/bin/env perl # # Copyright (c) 2005-2010 Robert Felber # (PC & IT Service Selling-IT, http://www.selling-it.de) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # A copy of the GPL can be found at http://www.gnu.org/licenses/gpl.txt # # Parts of code based on postfix-policyd-spf by Meng Wen Wong, version 1.06, # see http://spf.pobox.com/ # # AUTHOR: r.felber@selling-it.de # DATE: Tue Oct 19 20:31:50 CET 2009 # NAME: policyd-weight # VERSION: 0.1.15 beta-2 # URL: http://www.policyd-weight.org/ # ---------------------------------------------------------- # minimal documentation # ---------------------------------------------------------- # # Weighted Postfix SMTPD policy server. # # This program assumes you have read Postfix' # README_FILES/SMTPD_POLICY_README # If not, head to: # http://www.postfix.org/SMTPD_POLICY_README.html # # # # Logging is sent to syslogd. # # ---------------------------------------------------------------------- # To run this in init mode: # # % /path/to/policyd-weight start # # /etc/postfix/main.cf: # # smtpd_recipient_restrictions = # ... # reject_unauth_destination # ... # check_policy_service inet:127.0.0.1:12525 # # # NOTE: specify check_policy_service AFTER reject_unauth_destination # or else your system can become an open relay. # begin use strict; use Fcntl; use File::Spec; use Sys::Syslog qw(:DEFAULT setlogsock); use Net::DNS; use Net::IP; use Net::DNS::Packet qw(dn_expand); use IO::Socket::INET; use IO::Socket::UNIX; use IO::Select; use Config; use POSIX; use Carp qw(cluck longmess); use vars qw($csock $s $tcp_socket $sock $new_sock $old_mtime); our $VERSION = "0.1.15 beta-2"; our $CVERSION = 5; # cache interface version our $CMD_DEBUG = 0; # -d switch our $KILL; # -k switch our $STATS; # -s switch our $DAEMONIZE; # start action our $RESTART; # restart action our $RELOAD; # reload action our $STOP; # stop action our $FOREGROUND; my $run_action; # marker whether any action has been used my $conf; # path to config file my $arg_iter; my $ignore; for(@ARGV) { $arg_iter++; next if ($_ eq $ignore); $ignore = ''; if($_ eq "-d") { $^W = 1; $CMD_DEBUG = 1; } elsif($_ eq '-f') { if( -f $ARGV[$arg_iter]) { $conf = $ARGV[$arg_iter]; $ignore = $ARGV[$arg_iter]; next; } else { print "configfile ".$ARGV[$arg_iter]." doesn't exist\n"; exit "-1"; } } elsif($_ eq '-k') { $KILL = 1; } elsif($_ eq '-s') { $STATS = 1; } elsif($_ eq '-D') { $FOREGROUND = 1; } elsif($_ =~ /-[-]*h/) { usage(); } elsif($_ =~ /-[-]*v/) { my $net_dns_ver = Net::DNS->version; my $os = `uname -rs`; print <) { if (/^#--BEGIN_CONFDEF/) { $del = 1; next; } if ($del) { if (/^#--END_CONFDEF/) { last; } else { $_ =~ s/^my / /; print $_; } } } close(POLW); exit; } elsif($_ eq "restart") { usage() if ($run_action); if(!($< == 0 || $CMD_DEBUG)) { die "You must be root in order to use \"restart\"!\n"; } $STOP = 1; $RESTART = 1; $DAEMONIZE = 1; $run_action = 1; } elsif($_ eq "stop") { usage() if ($run_action); if(!($< == 0 || $CMD_DEBUG)) { die "You must be root in order to use \"stop\"!\n"; } $DAEMONIZE = 1; $STOP = 1; $run_action = 1; } elsif($_ eq "reload") { usage() if ($run_action); if(!($< == 0 || $CMD_DEBUG)) { die "You must be root in order to use \"reload\"!\n"; } $RELOAD = 1; } else { print "policyd-weight: unknown option $_\n"; usage(1); } } sub usage { my $ret = shift; print <] [stop|start|restart|defaults] Args in [ ] are optional. Options -D Don't detach master - run master in foreground -d Debug, don't daemonize, log to STDOUT -f /path/to/file Specify a configuration file -h This help -k Kill cache instance -s Show cache entries and exit. With -d show debug cache entries -v Show version and exit Actions stop Stops the policyd-weight daemon, add -k to also Stop the cache. In addition with -d -k it stops the debug cache. start Starts the policyd-weight daemon. Add -d to start a debug session in foregorund. restart Restarts policyd-weight. Together with -d it restarts a debug session in foreground. reload Reload the configuration file defaults Output default configuration If no action is given it waits for data on STDIN. WARNING: do NOT use options or actions in master.cf! EOF exit($ret); } if($CMD_DEBUG) { $^W = 1; print "policyd-weight version: ".$VERSION.", CacheVer: $CVERSION\nSystem: "; system("uname -a"); print "Perl version: ".$]."\n"; } # # store signal-name to number conversions for better accessibility # our %sig_list; my $i; foreach(split(' ', $Config{sig_name})) { $sig_list{$_} = $i++; } # # Print Module Versions if -d requested # if($CMD_DEBUG) { print "Net::DNS version: " . Net::DNS->version . "\n"; } # don't let warnings confuse the SMTP, feed die() lines to syslog $SIG{__DIE__} = sub { mylog(warning=>"err: @_"); }; # ---------------------------------------------------------- # configuration (defaults) # ---------------------------------------------------------- # don't make changes here, instead use/create /etc/policyd-weight.conf # NOTE: use perl syntax inclusive `;' in configuration files. # #--BEGIN_CONFDEF my $DEBUG = 0; # 1 or 0 - don't comment my $REJECTMSG = "550 Mail appeared to be SPAM or forged. Ask your Mail/DNS-Administrator to correct HELO and DNS MX settings or to get removed from DNSBLs"; my $REJECTLEVEL = 1; # Mails with scores which exceed this # REJECTLEVEL will be rejected my $DEFER_STRING = 'IN_SPAMCOP= BOGUS_MX='; # A space separated case-sensitive list of # strings on which if found in the $RET # logging-string policyd-weight changes # its action to $DEFER_ACTION in case # of rejects. # USE WITH CAUTION! # DEFAULT: "IN_SPAMCOP= BOGUS_MX=" my $DEFER_ACTION = '450'; # Possible values: DEFER_IF_PERMIT, # DEFER_IF_REJECT, # 4xx response codes. See also access(5) # DEFAULT: 450 my $DEFER_LEVEL = 5; # DEFER mail only up to this level # scores greater than DEFER_LEVEL will be # rejected # DEFAULT: 5 my $DNSERRMSG = '450 No DNS entries for your MTA, HELO and Domain. Contact YOUR administrator'; my $dnsbl_checks_only = 0; # 1: ON, 0: OFF (default) # If ON request that ALL clients are only # checked against RBLs my @dnsbl_checks_only_regexps = ( # qr/[^.]*(exch|smtp|mx|mail).*\..*\../, # qr/yahoo.com$/ ); # specify a comma-separated list of regexps # for client hostnames which shall only # be RBL checked. This does not work for # postfix' "unknown" clients. # The usage of this should not be the norm # and is a tool for people which like to # shoot in their own foot. # DEFAULT: empty my $LOG_BAD_RBL_ONLY = 1; # 1: ON (default), 0: OFF # When set to ON it logs only RBLs which # affect scoring (positive or negative) ## DNSBL settings my @dnsbl_score = ( # HOST, HIT SCORE, MISS SCORE, LOG NAME 'pbl.spamhaus.org', 3.25, 0, 'DYN_PBL_SPAMHAUS', 'sbl-xbl.spamhaus.org', 4.35, -1.5, 'SBL_XBL_SPAMHAUS', 'bl.spamcop.net', 3.75, -1.5, 'SPAMCOP', 'dnsbl.njabl.org', 4.25, -1.5, 'BL_NJABL', 'ix.dnsbl.manitu.net', 4.35, 0, 'IX_MANITU' #'rbl.ipv6-world.net', 4.25, 0, 'IPv6_RBL' #don't use, kept for testing failures! ); my $MAXDNSBLHITS = 2; # If Client IP is listed in MORE # DNSBLS than this var, it gets # REJECTed immediately my $MAXDNSBLSCORE = 8; # alternatively, if the score of # DNSBLs is ABOVE this # level, reject immediately my $MAXDNSBLMSG = '550 Your MTA is listed in too many DNSBLs'; ## RHSBL settings my @rhsbl_score = ( 'multi.surbl.org', 4, 0, 'SURBL', 'rhsbl.ahbl.org', 4, 0, 'AHBL', 'dsn.rfc-ignorant.org', 3.5, 0, 'DSN_RFCI', 'postmaster.rfc-ignorant.org', 0.1, 0, 'PM_RFCI', 'abuse.rfc-ignorant.org', 0.1, 0, 'ABUSE_RFCI' ); my $BL_ERROR_SKIP = 2; # skip a RBL if this RBL had this many continuous # errors my $BL_SKIP_RELEASE = 10; # skip a RBL for that many times ## cache stuff my $LOCKPATH = '/tmp/.policyd-weight/'; # must be a directory (add # trailing slash) my $SPATH = $LOCKPATH.'/polw.sock'; # socket path for the cache # daemon. my $MAXIDLECACHE = 60; # how many seconds the cache may be idle # before starting maintenance routines # NOTE: standard maintenance jobs happen # regardless of this setting. my $MAINTENANCE_LEVEL = 5; # after this number of requests do following # maintenance jobs: # checking for config changes # negative (i.e. SPAM) result cache settings ################################## my $CACHESIZE = 2000; # set to 0 to disable caching for spam results. # To this level the cache will be cleaned. my $CACHEMAXSIZE = 4000; # at this number of entries cleanup takes place my $CACHEREJECTMSG = '550 temporarily blocked because of previous errors'; my $NTTL = 1; # after NTTL retries the cache entry is deleted my $NTIME = 30; # client MUST NOT retry within this seconds in order # to decrease TTL counter # positve (i.,e. HAM) result cache settings ################################### my $POSCACHESIZE = 1000; # set to 0 to disable caching of HAM. To this number # of entries the cache will be cleaned my $POSCACHEMAXSIZE = 2000; # at this number of entries cleanup takes place my $POSCACHEMSG = 'using cached result'; my $PTTL = 60; # after PTTL requests the HAM entry must # succeed one time the RBL checks again my $PTIME = '3h'; # after $PTIME in HAM Cache the client # must pass one time the RBL checks again. # Values must be nonfractal. Accepted # time-units: s, m, h, d my $TEMP_PTIME = '1d'; # The client must pass this time the RBL # checks in order to be listed as hard-HAM # After this time the client will pass # immediately for PTTL within PTIME ## DNS settings my $DNS_RETRIES = 2; # Retries for ONE DNS-Lookup my $DNS_RETRY_IVAL = 2; # Retry-interval for ONE DNS-Lookup my $MAXDNSERR = 3; # max error count for unresponded queries # in a complete policy query my $MAXDNSERRMSG = 'passed - too many local DNS-errors'; my $PUDP = 0; # persistent udp connection for DNS queries. # broken in Net::DNS version 0.51. Works with # Net::DNS 0.53; DEFAULT: off my $USE_NET_DNS = 0; # Force the usage of Net::DNS for RBL lookups. # Normally policyd-weight tries to use a faster # RBL lookup routine instead of Net::DNS my $NS = ''; # A list of space separated NS IPs # This overrides resolv.conf settings # Example: $NS = '1.2.3.4 1.2.3.5'; # DEFAULT: empty my $IPC_TIMEOUT = 2; # timeout for receiving from cache instance my $TRY_BALANCE = 0; # If set to 1 policyd-weight closes connections # to smtpd clients in order to avoid too many # established connections to one policyd-weight # child # scores for checks, WARNING: they may manipulate eachother # or be factors for other scores. # HIT score, MISS Score my @client_ip_eq_helo_score = (1.5, -1.25 ); my @helo_score = (1.5, -2 ); my @helo_from_mx_eq_ip_score = (1.5, -3.1 ); my @helo_numeric_score = (2.5, 0 ); my @from_match_regex_verified_helo = (1, -2 ); my @from_match_regex_unverified_helo = (1.6, -1.5 ); my @from_match_regex_failed_helo = (2.5, 0 ); my @helo_seems_dialup = (1.5, 0 ); my @failed_helo_seems_dialup = (2, 0 ); my @helo_ip_in_client_subnet = (0, -1.2 ); my @helo_ip_in_cl16_subnet = (0, -0.41 ); my @client_seems_dialup_score = (3.75, 0 ); my @from_multiparted = (1.09, 0 ); my @from_anon = (1.17, 0 ); my @bogus_mx_score = (2.1, 0 ); my @random_sender_score = (0.25, 0 ); my @rhsbl_penalty_score = (3.1, 0 ); my @enforce_dyndns_score = (3, 0 ); my $VERBOSE = 0; my $ADD_X_HEADER = 1; # Switch on or off an additional # X-policyd-weight: header # DEFAULT: on my $DEFAULT_RESPONSE = 'DUNNO default'; # Fallback response in case # the weighted check didn't # return any response (should never # appear). # # Syslogging options for verbose mode and for fatal errors. # NOTE: comment out the $syslog_socktype line if syslogging does not # work on your system. # my $syslog_socktype = 'unix'; # inet, unix, stream, console my $syslog_facility = "mail"; my $syslog_options = "pid"; my $syslog_priority = "info"; my $syslog_ident = "postfix/policyd-weight"; # # Process Options # my $USER = "polw"; # User must be a username, no UID my $GROUP = ""; # specify GROUP if necessary # DEFAULT: empty, will be initialized as # $USER my $MAX_PROC = 50; # Upper limit if child processes my $MIN_PROC = 3; # keep that minimum processes alive my $TCP_PORT = 12525; # The TCP port on which policyd-weight # listens for policy requests from postfix my $BIND_ADDRESS = '127.0.0.1'; # IP-Address on which policyd-weight will # listen for requests. # You may only list ONE IP here, if you want # to listen on all IPs you need to say 'all' # here. Default is '127.0.0.1'. # You need to restart policyd-weight if you # change this. my $SOMAXCONN = 1024; # Maximum of client connections # policyd-weight accepts # Default: 1024 my $CHILDIDLE = 240; # how many seconds a child may be idle before # it dies. my $PIDFILE = "/var/run/policyd-weight.pid"; #--END_CONFDEF $0 = "policyd-weight (master)"; my %cache; my %poscache; my $my_PTIME; my $my_TEMP_PTIME; if(!($conf)) { if( -f "/etc/policyd-weight.conf") { $conf = "/etc/policyd-weight.conf"; } elsif( -f "/etc/postfix/policyd-weight.cf") { $conf = "/etc/postfix/policyd-weight.cf"; } elsif( -f "/usr/local/etc/policyd-weight.conf") { $conf = "/usr/local/etc/policyd-weight.conf"; } elsif( -f "policyd-weight.conf") { $conf = "policyd-weight.conf"; } } my $conf_err; my $conf_str; our $old_mtime; if($conf ne "") { if(sprintf("%04o",(stat($conf))[2]) !~ /(7|6|3|2)$/) { if(open(CONF, $conf)) { read(CONF,$conf_str,-s CONF); close(CONF); #XXX taint $conf_str as $< enables taint mode ($conf_str) = $conf_str =~ m/(.*)/s; eval $conf_str; if($@) { $conf_err = "syntax error in file $conf: ".$@; } else { $old_mtime = (stat($conf))[9]; } } else { $conf_err = "could not open $conf: $!"; } } else { $conf_err = "$conf is world-writeable!"; } } else { $conf = "default settings"; # don't change! required by cache maintenance } our $STAYALIVE; # set group to user if no group has been defined $GROUP = $USER unless $GROUP; if($CMD_DEBUG == 1) { $DEBUG = 1; $conf_str =~ s/\#.*?(\n)/$1/gs; $conf_str =~ s/\n+/\n/g; print "config: $conf\n".$conf_str."\n"; $SPATH .= ".debug"; # chose /tmp for debug pidfiles only if user is not root # if root would store debug pids also in /tmp we would be # open to race attacks if($< != 0) { $PIDFILE = "/tmp/policyd-weight.pid.debug"; } else { $PIDFILE .= ".debug"; } print "debug: using port ".++$TCP_PORT."\n"; print "debug: USER: $USER\n"; print "debug: GROUP: $GROUP\n"; print "debug: issuing user: ".getpwuid($<)."\n"; print "debug: issuing group: ".getpwuid($()."\n"; } $conf_str = ""; # # check for nasty symlinks # check_symlnk('master: init:', $LOCKPATH, $PIDFILE, $SPATH, "$LOCKPATH/cache_lock"); # send HUP to kids if $RELOAD if($RELOAD) { local $SIG{HUP} = 'IGNORE'; open(PF, $PIDFILE) or die "Couldn't open $PIDFILE: $!"; my $pid = ; close(PF); if(!($pid > 0)) { die "pid $pid seems to be wrong" }; print "sending ".-$sig_list{HUP}." to $pid\n"; kill (-$sig_list{HUP}, $pid) or die "err: $!"; exit; } # ---------------------------------------------------------- # initialization # ---------------------------------------------------------- # # This process runs as a daemon, so it can't log to a terminal. Use # syslog so that people can actually see our messages. # if($CMD_DEBUG != 1) { setlogsock($syslog_socktype) or die "setlogsock: $syslog_socktype: $!. If you are on Solaris you might want to set \$syslog_socktype = 'stream';"; openlog($syslog_ident, $syslog_options, $syslog_facility) or die "openlog: $!. If you are on Solaris you might want to set \$syslog_socktype = 'stream';"; } if($KILL) { if((-S $SPATH) && ($csock = IO::Socket::UNIX->new($SPATH))) { cache_query("kill"); $csock->close if ($csock && $csock->connected); unlink $SPATH; } if(-S $SPATH) { mylog(warning=>"-k action but $SPATH still exists, deleting it"); print STDERR "warning: -k action but $SPATH still exists, deleting it\n"; unlink $SPATH or die $!; } if( -d $LOCKPATH.'/cache_lock') { mylog(warning=>'removing stale '.$LOCKPATH.'/cache_lock'); print STDERR 'warning: removing stale '.$LOCKPATH.'/cache_lock'; rmdir $LOCKPATH.'/cache_lock'; } exit unless $STOP or $DAEMONIZE; } if($STATS) { print "*** querying cache for content stats:\n"; cache_query("stats"); exit; } if(!($run_action)) { # don't unlink PIDFILE if policyd-weight # got called without arguments $STAYALIVE = 1; } # re-arrange signal handlers $SIG{__DIE__} = sub { die @_ if index($_[0], 'ETIMEOUT') == 0; mylog(warning=>"err: init: @_"); unlink $PIDFILE unless $STAYALIVE; }; $SIG{'TERM'} = sub { unlink $PIDFILE unless $STAYALIVE; mylog(warning=>'Got SIGTERM. Daemon terminated.'); exit }; $SIG{'QUIT'} = sub { unlink $PIDFILE unless $STAYALIVE; mylog(warning=>"Got SIG@_. Daemon terminated."); exit }; $SIG{'INT'} = sub { unlink $PIDFILE unless $STAYALIVE; mylog(warning=>"Got SIG@_. Daemon terminated."); exit }; $SIG{'PIPE'} = sub { unlink $PIDFILE; mylog(warning=>"Got SIG@_. Daemon terminated."); exit }; $SIG{'SYS'} = sub { unlink $PIDFILE; mylog(warning=>"Got SIG@_. Daemon terminated."); exit }; $SIG{'USR1'} = sub { unlink $PIDFILE; mylog(warning=>"Got SIG@_. Daemon terminated."); exit }; $SIG{'USR2'} = sub { unlink $PIDFILE; mylog(warning=>"Got SIG@_. Daemon terminated."); exit }; if($SIG{'POLL'}) { $SIG{'POLL'} = sub { unlink $PIDFILE; mylog(warning=>"Got SIG@_. Daemon terminated."); exit }; } if($SIG{'UNUSED'}) { $SIG{'UNUSED'} = sub { unlink $PIDFILE; mylog(info=>"Got SIG@_. Daemon terminated."); exit }; } ##### ## core dumpers $SIG{'SEGV'} = sub { $SIG{'ABRT'} = ''; unlink $PIDFILE; mylog(warning=>"Got @_:".longmess(). ". Daemon terminated."); CORE::dump(); exit }; $SIG{'ILL'} = sub { $SIG{"ABRT"} = ''; unlink $PIDFILE; mylog(warning=>"Got @_:".longmess(). ". Daemon terminated."); CORE::dump; exit }; $SIG{'ABRT'} = sub { unlink $PIDFILE; mylog(warning=>"Got @_:".longmess(). ". Daemon terminated."); CORE::dump; exit }; $SIG{'FPE'} = sub { $SIG{"ABRT"} = ''; unlink $PIDFILE; mylog(warning=>"Got @_:".longmess(). ". Daemon terminated."); CORE::dump; exit }; $SIG{'BUS'} = sub { $SIG{"ABRT"} = ''; unlink $PIDFILE; mylog(warning=>"Got @_:".longmess(). ". Daemon terminated."); CORE::dump; exit }; $SIG{'HUP'} = sub { conf_check('master'); }; # # Log an error and abort. # sub fatal_exit { mylog(warning => "fatal_exit: @_"); die "fatal: @_"; } # # Unbuffer standard output. # select((select(STDOUT), $| = 1)[0]); if($VERBOSE == 1) { mylog(debug=>"startup: using $conf"); } my $RETANSW; if($ADD_X_HEADER == 1) { $RETANSW = "PREPEND X-policyd-weight:"; } else { $RETANSW = "DUNNO "; } if($conf_err) { mylog(warning=>"conf-err: ".$conf_err); mylog(warning=>"conf-err: falling back to builtin defaults"); $RETANSW = $RETANSW." using builtin defaults due to config-error"; } our $res=Net::DNS::Resolver->new; $res->retrans($DNS_RETRY_IVAL) unless $DNS_RETRY_IVAL eq ""; $res->retry ($DNS_RETRIES) unless $DNS_RETRIES eq ""; $res->debug (1) if ($CMD_DEBUG == 1); if($NS && $NS =~ /\d/) { my @ns = split(' ', $NS); $res->nameservers(@ns); } # watch the version string, I'm afraid that they change to x.x.x notation if(Net::DNS->version() >= 0.50) { $res->force_v4(1); # force ipv4 usage, autodetection is broken till # Net::DNS 0.53 } else { $res->igntc(1); # ignore truncated packets if Net-DNS version is # lower than 0.50 } # keep udp socket open, don't waste time for socket creation. # works with Net::DNS 0.53 $res->persistent_udp(1) if $PUDP == 1; our %RTYPES = ( 'A' => 1, 'TXT' => 16 ); # see RFC 1035 our $s; if($res) { my $ns = (($res->nameserver)[0]); if(!($s = IO::Socket::INET->new( PeerAddr => $ns, PeerPort => '53', Proto => 'udp' ) )) { mylog(warning=>"could not open RBL Lookup Socket to $ns: $@ $!"); $USE_NET_DNS = 1; } } # ---------------------------------------------------------- # main # ---------------------------------------------------------- # # Receive a bunch of attributes, evaluate the policy, send the result. # our $accepted = "UNDEF"; our $blocked = "UNDEF"; our $my_REJECTMSG = $REJECTMSG; our %bl_err; our $skip_rel; # cd to a coredump dir, if it exists chdir "$LOCKPATH/cores/master"; my %attr; if(!($DAEMONIZE)) { while () { my $string = $_; my $action = parse_input($string); if($action) { print STDOUT $action; %attr = (); } } } else { ############################################################################## # # DAEMON # ############################################################################## if($STOP && (!(-f $PIDFILE))) { print STDERR "No pidfile, expected it at $PIDFILE!\n"; exit 1; } if( -f $PIDFILE) { open(PF, $PIDFILE) || die $!; my $oldpid = ; close(PF); if($STOP && $oldpid) { if(my $ret = kill(-$sig_list{'TERM'}, $oldpid)) { print "terminating "; mylog(info=>"Daemon terminated."); } else { kill(-$sig_list{'KILL'}, $oldpid); print "killed\n"; mylog(info=>"Abnormal exit. Daemon killed forcingly."); } unlink $PIDFILE; if(!$RESTART) { print " ... done\n"; exit; } } my $i; while($oldpid && kill(0, $oldpid)) { if(!$RESTART) { mylog(warning=>"Process already running"); print STDERR "Process already running\n"; exit -1; } autoflush STDOUT 1; print "."; if($i++ > 5) { $STAYALIVE = 1; mylog(warning=>"Couldn't remove $PIDFILE, a process with pid $oldpid exists! Use \"restart\" to force.\n"); die "Couldn't remove $PIDFILE, a process with pid $oldpid exists! Use \"restart\" to force.\n"; } sleep 1; } print " done\n"; } create_lockpath("daemon"); if($BIND_ADDRESS && $BIND_ADDRESS !~ /^[ \t]*all[ \t]*$/i) { $tcp_socket = IO::Socket::INET->new( Proto => 'tcp', LocalHost => $BIND_ADDRESS, LocalPort => $TCP_PORT, Listen => $SOMAXCONN, Reuse => 1, Blocking => 0) or die "master: bind $TCP_PORT: $@ $!"; } else { $tcp_socket = IO::Socket::INET->new( Proto => 'tcp', LocalPort => $TCP_PORT, Listen => $SOMAXCONN, Reuse => 1, Blocking => 0) or die "master: bind $TCP_PORT: $@ $!"; } # XXX: do we really need that? I used it for a chance of closing # sockets when spawning caches and the like. fcntl($tcp_socket, F_SETFD, FD_CLOEXEC); open(PF, ">".$PIDFILE) or die $!; # drop privileges if(!($CMD_DEBUG)) { my $uname = getpwnam($USER) or die "User $USER doesn't exist!"; my $gname = getgrnam($GROUP) or die "Group $GROUP doesn't exist!"; my $runame = getpwuid($<) or die $!; my $rgname = getgrgid($() or die $!; # XXX: You'll get nightmares if you change stuff here! *voodoospell* $! = ''; # this first variant uses different approaches on plattforms. # freebsd/linux uses setresgid + setgroups, other bsd, Mac OS X use # obviously setregid + setgroups ($(,$)) = ($gname, "$gname $gname"); if($!) { $! = ''; # last try. Implementation variant not clear on all plattforms $( = $gname; die "($<)($>): set GID to $gname: $!" if $!; $) = "$gname $gname"; die "($<)($>): set EGID to $gname: $!" if $!; } ($<, $>) = ($uname, $uname); if($!) { $! = ''; # this turns on taint mode, too. see man perlsec! $< = $uname; die "set UID to $uname: $!" if $!; $> = $uname; die "set EUID to $uname: $!" if $!; } # create directories for chdir in order # to find core dumps an such if(!(-d "$LOCKPATH/cores/")) { mkdir "$LOCKPATH/cores/" or die "master: error while creating $LOCKPATH/cores/: $!"; } if(!(-d "$LOCKPATH/cores/master")) { mkdir "$LOCKPATH/cores/master" or die "master: error while creating $LOCKPATH/cores/master: $!"; } if(!(-d "$LOCKPATH/cores/cache")) { mkdir "$LOCKPATH/cores/cache" or die "master: error while creating $LOCKPATH/cores/cache: $!"; } chdir "$LOCKPATH/cores/master" or die "master: chdir $LOCKPATH/cores/master: $!"; if(!($FOREGROUND)) { defined(my $pid = fork) or die "Can't fork: $!"; exit if $pid; # daemonized setsid or die "Can't start a new session: $!"; open STDIN, '/dev/null' or die "Can't read /dev/null: $!"; open STDERR, '>/dev/null' or die "Can't write to /dev/null: $!"; open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!"; } mylog(info=>"policyd-weight $VERSION started and daemonized. " . "conf:$conf; " . "GID:$( EGID:$) UID:$< EUID:$>; " . "taint mode: " . ${^TAINT} ); } print PF $$ or die "err $!\n"; close PF or die "err $!\n"; my %childs; # maintenance hash for cleaning up children my %avail; # hash to know which client is available my %pipes; # hash to maintain pid -> pipe associations cache_query("start"); # pre-launch cache our $select_to; my $readable_handles = new IO::Select(); my $new_tcp_readable; $tcp_socket->autoflush(1); $readable_handles->add($tcp_socket); my $waitedpid; my $parentpid = $$; sub REAPER { my $waitedpid; while($waitedpid = waitpid(-1, WNOHANG)) { last if $waitedpid == -1; mylog(info=>"master: child $waitedpid exited"); delete($childs{$waitedpid}); delete($avail{$waitedpid}); delete($pipes{$waitedpid}); if(!(keys(%avail) > 0)) { $readable_handles->add($tcp_socket); } } $SIG{CHLD} = \&REAPER; } $SIG{CHLD} = \&REAPER; $SIG{'TERM'} = sub { foreach(keys(%childs)) { kill($sig_list{TERM}, $_); } unlink $PIDFILE; exit 0; }; use vars qw/$child/; use vars qw/$parent/; my $sigset; my $old_sigset; while(1) { # process SIGCHLD signals if($old_sigset) { unless (defined sigprocmask(SIG_UNBLOCK, $old_sigset)) { mylog(warning=>"master: Could not unblock SIGCHLD"); } } # wait for data on all sockets ($new_tcp_readable) = IO::Select->select($readable_handles, undef, undef, undef); # block SIGCHLD signals, avoid raceconditions and coredumps $sigset = POSIX::SigSet->new(SIGCHLD); $old_sigset = POSIX::SigSet->new; unless (defined sigprocmask(SIG_BLOCK, $sigset, $old_sigset)) { mylog(warning=>"master: Could not block SIGCHLD"); } my $max_proc_msg; # process socket data foreach my $sock (@$new_tcp_readable) { if($sock == $tcp_socket) { # let children handle it if they are available if (keys(%avail) > 0) { $readable_handles->remove($tcp_socket); next; } # don't spawn new children if MAX_PROC reached if(keys %childs >= $MAX_PROC) { if( (!($max_proc_msg)) ) { mylog(warning=>"master: MAX_PROC ($MAX_PROC) reached"); } $max_proc_msg = 1; $readable_handles->remove($tcp_socket); next; } # open a socketpair for control communication with the # soon to be spawned child ($child, $parent) = IO::Socket->socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC) or mylog(warning=>"master: socketpair: $@ $!"); $child->autoflush(1); $parent->autoflush(1); # check for configuration changes before we spawn a new child conf_check("master"); # attempt to fork a new child defined(my $pid = fork) or die "cannot fork: $!"; # parent stuff if ($pid) { $pipes{$pid} = $child; $readable_handles->add($pipes{$pid}); $readable_handles->add($tcp_socket); $parent->close; $childs{$pid} = 1; $avail{$pid} = 1; next; } ############################################################################## # # DAEMON CHILDREN # ############################################################################## $0 = "policyd-weight (child)"; $SIG{'TERM'} = sub { eval { local $SIG{ALRM} = sub { die "ETIMEOUT" }; alarm $IPC_TIMEOUT; print $parent ("$$ 0\n"); $parent->recv(my $ans, 1024); alarm 0; }; exit; }; our $die_r; $SIG{__DIE__} = sub { die @_ if index($_[0], 'ETIMEOUT') == 0; die @_ if @_ eq $die_r; $die_r = @_; mylog(warning=>"child: err: @_" ); eval { local $SIG{ALRM} = sub { die "ETIMEOUT" }; alarm $IPC_TIMEOUT; print $parent ("$$ 0\n"); $parent->recv(my $ans, 1024); alarm 0; }; }; $SIG{INT} = sub { eval { local $SIG{ALRM} = sub { die "ETIMEOUT" }; alarm $IPC_TIMEOUT; print $parent ("$$ 0\n"); $parent->recv(my $ans, 1024); alarm 0; }; exit; }; $SIG{'HUP'} = sub { conf_check('child'); }; $SIG{'PIPE'} = sub { mylog(warning=>"Got SIG@_. Child $$ terminated."); die; }; $SIG{'SYS'} = sub { mylog(warning=>"Got SIG@_. Child $$ terminated."); die }; $SIG{'USR1'} = sub { mylog(warning=>"Got SIG@_. Child $$ terminated."); die }; $SIG{'USR2'} = sub { mylog(warning=>"Got SIG@_. Child $$ terminated."); die }; if($SIG{'POLL'}) { $SIG{'POLL'} = sub { mylog(warning=>"Got SIG@_. Child $$ terminated."); die }; } if($SIG{'UNUSED'}) { $SIG{'UNUSED'} = sub { mylog(info=>"Got SIG@_. Child $$ terminated."); die }; } # core dumpers $SIG{'SEGV'} = sub { $SIG{'ABRT'} = ''; delete($SIG{'ABRT'}); mylog(warning=>"Got @_:".longmess(). ". Child $$ terminated"); die }; $SIG{'ILL'} = sub { $SIG{"ABRT"} = ''; mylog(warning=>"Got @_:".longmess(). ". Child $$ terminated."); die }; $SIG{'ABRT'} = sub { mylog(warning=>"Got @_:".longmess(). ". Child $$ terminated."); die }; $SIG{'FPE'} = sub { $SIG{"ABRT"} = ''; mylog(warning=>"Got @_:".longmess(). ". Child $$ terminated."); die }; $SIG{'BUS'} = sub { $SIG{"ABRT"} = ''; mylog(warning=>"Got @_:".longmess(). ". Child $$ terminated."); die }; mylog(info=>'child: spawned'); if($res) { if($s && $s->connected) { $s->close; # don't use inherited DNS sockets } my $ns = (($res->nameserver)[0]); if(!($s = IO::Socket::INET->new( PeerAddr => $ns, PeerPort => '53', Proto => 'udp')) ) { mylog(warning=> "child: could not open RBL Lookup Socket to $ns: $@ $!"); $USE_NET_DNS = 1; } } my $readable_handles = new IO::Select(); $readable_handles->add($parent); $readable_handles->add($tcp_socket); close $child; my $tout = $CHILDIDLE; my $maintenance = 0; my $sig_set; my $old_sigset; while(1) { if($maintenance >= $MAINTENANCE_LEVEL) { $maintenance = 0; conf_check("child"); } if($old_sigset) { unless (defined sigprocmask(SIG_UNBLOCK, $old_sigset)) { mylog(warning=>'child: Could not unblock SIGHUP'); } } my $time_s = time; ($new_tcp_readable) = IO::Select->select($readable_handles, undef, undef, $tout); my $time_e = time; # block SIGHUPs $sigset = POSIX::SigSet->new(SIGCHLD); $old_sigset = POSIX::SigSet->new; unless (defined sigprocmask(SIG_BLOCK, $sigset, $old_sigset)) { mylog(warning=>'child: Could not block SIGHUP'); } $select_to = 1; my $ans; foreach my $sock (@$new_tcp_readable) { $select_to = 0; my $ans; # define for the "for"-scope if($sock == $tcp_socket) { my $new_sock = $tcp_socket->accept(); if(!($new_sock) || (!($new_sock->connected))) { $tout = $CHILDIDLE - ($time_e - $time_s); if( ($tout <= 0) || ($tout > $CHILDIDLE)) { $tout = $CHILDIDLE; } next; } else { print $parent ("$$ 0\n"); $parent->recv($ans, 1024); $tout = $CHILDIDLE; $new_sock->autoflush(1); # set nonblocking IO, required by linux # BSD did fine without fcntl($new_sock, F_SETFD, O_NONBLOCK) || die $!; $readable_handles->add($new_sock); } print $parent ("$$ 0\n"); $parent->recv($ans, 1024); } else { print $parent ("$$ 0\n"); $parent->recv($ans, 1024); my $action; my $buf; my $ans; my $busy; $sock->timeout(1); while(<$sock>) { $buf = $_; if($buf) { $busy = 1; $action = parse_input($buf); } if($action eq 'EPARSE') { $action = ''; $buf = ''; last; } if($action) { $sock->send($action); if($TRY_BALANCE) { $readable_handles->remove($sock); $sock->close(); } %attr = (); print $parent ("$$ 1\n"); $parent->recv($ans, 1024); ++$maintenance; last; } } next if ($buf && (!($action))); if(!($buf)) { $readable_handles->remove($sock); $sock->shutdown(2); close $sock; print $parent ("$$ 1\n"); $parent->recv($ans, 1024); } } } if($select_to) { # child was idle too much, exit if no connection # to a smtp print $parent ("$$ 0\n"); $parent->recv($ans, 1024); my $connected; for($readable_handles->handles) { next if $_ == $tcp_socket or $_ == $parent; $connected = 1; } if((!($connected))) { #ask dad if we can die print $parent ("$$ d\n"); $parent->recv($ans, 1024); if(($ans) && ($ans eq "y\n")) { mylog(info=>"child: exiting: idle for $CHILDIDLE sec."); exit; } } $readable_handles->add($tcp_socket); print $parent ("$$ 1\n"); $parent->recv($ans, 1024); $tout = $CHILDIDLE; } } } ####################################################################### # # PARENT again # else { # piped control-communication with our children my $buf = <$sock>; if(!($buf)) { $readable_handles->remove($sock); $sock->close; next } my ($cpid, $stat) = split(' ', $buf); # a kid ask to go suicide if($stat eq 'd') { if(keys (%childs) > $MIN_PROC) { # tell kid to commit suicide print $sock ("y\n"); delete $childs{$cpid}; delete $avail{$cpid}; $readable_handles->add($tcp_socket); } else { print $sock ("n\n"); } next; } # a kid tells us whether it's busy or free if($stat == 1) { $avail{$cpid} = 1; } else { delete $avail{$cpid}; } if(keys(%avail) > 0) { $readable_handles->remove($tcp_socket); } elsif(keys(%childs) < $MAX_PROC) { $readable_handles->add($tcp_socket); } print $sock ("1\n"); next; } } } } sub parse_input { $_ = shift; $_ =~ tr/\r\n//d; if (/=/) { my ($k, $v) = split (/=/, lc($_), 2); $attr{$k} = $v; return; } elsif (length) { mylog(warning=>sprintf("ignoring garbage: %.100s", $_)); return; } if ($VERBOSE == 1) { for (sort keys %attr) { mylog(debug=> "Attribute: $_=".$attr{$_}); } } if(!($DAEMONIZE)) { fatal_exit ("unrecognized request type: '$attr{request}'") unless $attr{request} eq 'smtpd_access_policy'; } else { if(!($attr{request} eq 'smtpd_access_policy')) { mylog(warning=>"unrecognized request type: '$attr{request}'"); return('EPARSE'); } } my $response; my $action; $action = $DEFAULT_RESPONSE; no strict 'refs'; my $delay_time = time; $response = weighted_check->(attr=>\%attr); if ($response) { $action = $response; } else { mylog(warning=>'weighted_check returned a zero value!'); } # return only a restriction class if the user requested it with # specifying a response message with "rc:foo" if(index($action, 'rc:') != -1) { $action =~ s/^[ \t]*rc:[ \t]*(.*?)[,; .]+.*/$1/i; } my $trace_info; if($DEBUG) { $trace_info = ' '; } $trace_info .= ' ' . ' ' . ' ' . '' ; mylog(info=>"decided action=$action; $trace_info; delay: ". (time - $delay_time).'s'); return("action=$action\n\n"); } sub address_stripped { # my $foo = localpart_lhs('foo+bar@baz.com'); # returns 'foo@baz.com' my $string = shift; for ($string) { s/[+-].*\@/\@/; } return $string; } ############################################################################### ############################################################################### ## subroutines ################################################################ #------------------------------------------------------------------------------ # Plugin: weighted_check #------------------------------------------------------------------------------ sub weighted_check { local %_ = @_; my %attr = %{ $_{attr} }; my $ip = $attr{client_address}; $ip = Net::IP::ip_expand_address($ip,6) if Net::IP::ip_is_ipv6($ip); my $cl_hostname = $attr{client_name}; my $cansw; my $client_name = $attr{client_name} || ''; my $helo = $attr{helo_name} || ''; my $from = address_stripped($attr{sender}) || ''; my $rcpt = $attr{recipient} || ''; my $instance = $attr{instance} . $ip . $from; my $trace_info; if($DEBUG) { $trace_info = ' '; } $trace_info .= " "; my $from_domain; if($attr{sender} =~ /.*@(.*)/) { $from_domain = $1; } if($from eq '') { return('DUNNO NULL (<>) Sender'); } my $orig_from = $from; if($attr{recipient} && $attr{recipient} =~ /^(postmaster|abuse)\@/) { return('DUNNO mail for '.$attr{recipient}); } if(($instance) && ($instance eq $accepted)) { return ('DUNNO multirecipient-mail - already accepted by previous query'); } elsif(($instance) && ($instance eq $blocked)) { return ($my_REJECTMSG.' (multirecipient mail)' ); } ## cache check if( ($CACHESIZE > 0) || ($POSCACHESIZE > 0) ) { $cansw = cache_query('ask', $ip, '0', $orig_from, $from_domain); } if($cansw && index($cansw, 'rate') != 0) { $blocked = $instance; $my_REJECTMSG = $cansw; return($my_REJECTMSG); } elsif($cansw && index($cansw, 'rate:hard:') == 0) { $accepted = $instance; return("$RETANSW $POSCACHEMSG; $cansw"); } ## startup checks and preparing ############################################### my ($revip, $subip16, $subip); if (Net::IP::ip_is_ipv4($ip)) { my ($ipp1, $ipp2, $ipp3, $ipp4) = split(/\./, $ip); $revip = $ipp4.'.'.$ipp3.'.'.$ipp2.'.'.$ipp1; $subip16 = $ipp1.'.'.$ipp2.'.'; $subip = $subip16.$ipp3.'.'; } else { $ip = Net::IP::ip_expand_address($ip,6); $revip = Net::IP::ip_reverse($ip); $revip =~s/\.ip6.arpa\.$//; $subip16 = substr($ip,0,15); $subip = substr($ip,0,20); } my $rate = 0; my $total_dnsbl_score; # this var holds only positive scores! my $helo_ok = 0; my $mx_ok = 0; my $helo_untrusted_ok = 0; my $client_in_from = 0; my $RET = ''; my $dont_cache = 0; my $do_client_from_check = 0; my $client_seems_dialup = 0; my $in_dyn_bl = 0; my $helo_seems_dialup = 0; my $rhsbl_penalty = 0; my $bogus_mx_penalty = 0; my $maxdnserr = $MAXDNSERR; my $RELAYMSG = ''; my $found; my $rtime = time; # timestamp of policy request ## DNSBL check ################################################################ my $i; my $dnsbl_hits = 0; $skip_rel = $BL_SKIP_RELEASE + $BL_ERROR_SKIP; for($i=0;$i < @dnsbl_score; $i += 4) { $found = 0; my $answ = 0; if( (!($bl_err{$dnsbl_score[$i]})) || $bl_err{$dnsbl_score[$i]} <= $BL_ERROR_SKIP ) { $answ = rbl_lookup($revip.'.'.$dnsbl_score[$i]); } else { $RET .= ' '.$dnsbl_score[$i+3].'=SKIP('.$dnsbl_score[$i+2].')'; $rate += $dnsbl_score[$i+2]; if(++$bl_err{$dnsbl_score[$i]} >= $skip_rel) { $bl_err{$dnsbl_score[$i]} = 0; } next; } if(!($answ)) { # increase err counter for that rbl ++$bl_err{$dnsbl_score[$i]}; if($maxdnserr-- <= 1) { $accepted = $instance; return "$RETANSW $MAXDNSERRMSG in ".$dnsbl_score[$i].' lookups'; } $RET .= ' '.$dnsbl_score[$i+3].'=ERR('.$dnsbl_score[$i+2].')'; $rate += $dnsbl_score[$i+2]; next; } $bl_err{$dnsbl_score[$i]} = 0; if($answ > 0) { $RET .= ' IN_'.$dnsbl_score[$i+3].'=' . $dnsbl_score[$i+1]; $found = 1; $rate += $dnsbl_score[$i+1]; $total_dnsbl_score += $dnsbl_score[$i+1]; if(index(lc($dnsbl_score[$i+3]), 'dyn') != -1) { $client_seems_dialup = 1; $in_dyn_bl = 1; } } if($found == 0) { if($LOG_BAD_RBL_ONLY == 1) { if($dnsbl_score[$i+2] != 0) # if an RBL entry manipulates # the overall score, log it though. { $RET .= ' NOT_IN_'.$dnsbl_score[$i+3].'=' . $dnsbl_score[$i+2]; } } else { $RET .= ' NOT_IN_'.$dnsbl_score[$i+3].'='.$dnsbl_score[$i+2]; } $rate += $dnsbl_score[$i+2]; } else { # increase DNSBL hitcounter only if the DNSBL is a RBL and no # DNS whitelist if($dnsbl_score[$i+1] > 0) { ++$dnsbl_hits; } else { next; } # check for DNSBL Hit/Score limit exceeding if( ($dnsbl_hits > $MAXDNSBLHITS ) || ($total_dnsbl_score > $MAXDNSBLSCORE) ) { if($CACHESIZE > 0 && $MAXDNSBLMSG !~ /^\s*(4|DEFER|rc\:)/i) { cache_query('nadd', $ip, $total_dnsbl_score); } $blocked = $instance; mylog(info=>"weighted check: $RET; $trace_info; rate: $rate"); return($MAXDNSBLMSG."; check http://www.robtex.com/rbl/$ip.html"); } } } if($dnsbl_checks_only == 1) { return("$RETANSW $RET (only DNSBL check requested)"); } my $re_count; for(@dnsbl_checks_only_regexps) { my $re = $_; $re_count++; next if not $re; if($cl_hostname && $cl_hostname =~ /$re/) { return("$RETANSW $RET (only DNSBL check requested (regex-nr: $re_count))"); } } ## postive cache check if($cansw && ($POSCACHESIZE > 0) && ($dnsbl_hits < 1)) { $accepted = $instance; return("$RETANSW $POSCACHEMSG; $cansw"); } ## HELO/FROM DNS checks ####################################################### $found = 0; my $is_mx = 0; my $ip_eq_from = 0; my $addresses = ''; my $mx_names = ''; my $recs_found = 0; my $MATCH_TYPE; my $from_addresses = ''; my $dnserr = 0; my $bogus_mx = 0; my $bad_mx = 0; my $bad_mx_scored = 0; my $do_reverse_check = 0; my $squared_helo = squared_helo(\$helo, \$ip); if($squared_helo == 1) { $helo_ok = 1; } my $tmp_domain = $from_domain; $tmp_domain =~ s/[\[\]]//g; $tmp_domain = '['.$from_domain.']'; my $tmpip = squared_helo(\$tmp_domain, \$ip); if($tmpip == 1) { $from_addresses .= " $from_domain"; $found = 1; $helo_ok = 1; } $addresses .= " $helo"; my @helo_parts = split(/\./,$helo); $from =~ /.*@(.*)/; my $tmp_from = $1; my @parts_check = ($tmp_from, $helo); # don't change order for(my $tmpcnt=0; $tmpcnt < @parts_check; $tmpcnt++) { if($tmpcnt == 1) { $MATCH_TYPE = 'HELO'; } else { $MATCH_TYPE = 'FROM'; } my @parts = split(/\./,$parts_check[$tmpcnt]); for(;@parts >=2;shift(@parts)) { my $testhelo = join('.',@parts); next if $testhelo =~ /\[|\]/; my $query = $res->send($testhelo, 'MX'); if(dns_error(\$query, \$res)) { if($maxdnserr-- <= 1) { $accepted = $instance; return("$RETANSW $MAXDNSERRMSG in $MATCH_TYPE MX lookups for $testhelo"); } next; } # removed "if($query && $query->answer)" (which was introduced in # 0.1.14.4 due to dns_error() implementation) in 0.1.14.5 because # A lookups were not performed if MX returned NXDOMAIN # XXX: this is to be reviewed and sanitized if($query) { $recs_found = 1; # means, we've got some dns response foreach my $rr ($query->answer) { if($rr->type eq 'MX') { for my $query_type ('A','AAAA') { my $mxres = $res->send($rr->exchange , $query_type); if(dns_error(\$mxres, \$res)) { if($maxdnserr-- <= 1) { $accepted = $instance; return("$RETANSW $MAXDNSERRMSG in $MATCH_TYPE MX -> A lookups"); } next; } foreach my $mxvar ($mxres->answer) { next if ($mxvar->type ne 'A' && $mxvar->type ne 'AAAA'); my $ip_address = $mxvar->address; $ip_address = Net::IP::ip_expand_address($mxvar->address,6) if Net::IP::ip_is_ipv6($mxvar->address); # store sender MX hostname entries for comparission # with HELO argument if ($MATCH_TYPE eq 'FROM') { $mx_names .= '.'.$rr->exchange . " "; } if($tmpcnt == 0) { $from_addresses .= ' '.$ip_address; } $addresses .= ' '.$ip_address; if ($ip eq $ip_address) { $RET .= ' CL_IP_EQ_'.$MATCH_TYPE.'_MX=' . $helo_from_mx_eq_ip_score[1]; $found = 1; $is_mx = 1 if $MATCH_TYPE eq 'FROM'; $helo_ok = 1; $mx_ok = 1; $rate += $helo_from_mx_eq_ip_score[1]; last; } undef $ip_address; } } #Ipv4/IPv6 } last if $found; } # penalize dnsbl-weighted for empty/bogus MX records # XXX: probably need to separate hostnames from domainnames if( $MATCH_TYPE eq 'FROM' && (!($bad_mx)) && ( $from_addresses !~ /\d+/ || $from_addresses =~ /( 127\.| 192\.168\.| 10\.| 172\.(?:1[6-9]|2\d|3[01])\.)/ ) ) { $bad_mx = 1; } if(!($found)) { for my $query_type ('A','AAAA') { my $query = $res->send($testhelo,$query_type); if(dns_error(\$query, \$res)) { if($maxdnserr-- <= 1) { $accepted = $instance; return("$RETANSW $MAXDNSERRMSG in $MATCH_TYPE A lookup for $testhelo"); } next; } foreach my $addr ($query->answer) { if($addr->type eq 'PTR') { if($helo == $ip) { $RET .= ' CL_IP_EQ_HELO_NUMERIC='. $helo_score[1]; $rate += $helo_score[1]; $found = 1; $helo_untrusted_ok = 1; } } if(($addr->type ne 'A' && $addr->type ne 'AAAA')){ next; } my $ip_address = $addr->address; $ip_address= Net::IP::ip_expand_address($addr->address,6) if Net::IP::ip_is_ipv6($addr->address); if($tmpcnt == 0) { $from_addresses .= ' '.$ip_address; } $addresses .= ' '.$ip_address; if ($ip eq $ip_address) { $found = 1; $helo_ok = 1; $RET .= ' CL_IP_EQ_'.$MATCH_TYPE.'_IP=' . $helo_score[1]; $rate += $helo_score[1]; $bad_mx = 0; if($tmpcnt == 0) { $ip_eq_from = 1; } last; } undef $ip_address; } } #IPv4/IPv6 } if($bad_mx && (!($bad_mx_scored))) { my $score = $bogus_mx_score[0] * $total_dnsbl_score; if($score) { $RET .= ' BAD_MX='.$score; $rate += $score; $bad_mx_scored = 1; } } # check if sender domain has bogus or empty # A/MX records. if( ($MATCH_TYPE eq 'FROM') && (!($bogus_mx)) && ( $from_addresses !~ /\d+/ || $from_addresses =~ /( 127\.| 192\.168\.| 10\.| 172\.(?:1[6-9]|2\d|3[01])\.)/ ) ) { my $score = $bogus_mx_score[0] + $total_dnsbl_score; $RET .= ' BOGUS_MX='.$score; $rate += $score; $bogus_mx = 1; $bogus_mx_penalty = $score; } last if $found; } last if $found; } last if $found; } if((!($found)) && $recs_found) # helo seems forged { if(index($addresses,' '.$subip) != -1) { $RET .= ' HELO_IP_IN_CL_SUBNET='.$helo_ip_in_client_subnet[1]; $rate += $helo_ip_in_client_subnet[1]; $helo_ok = 1; $found = 1; } elsif(index($addresses,' '.$subip16) != -1) { $RET .= ' HELO_IP_IN_CL16_SUBNET=' . $helo_ip_in_cl16_subnet[1]; $rate += $helo_ip_in_cl16_subnet[1]; $helo_untrusted_ok = 1; $do_reverse_check = 1; $found = 1; } if($found != 1 && $helo_ok != 1 && $squared_helo != 1) { my $score = $helo_score[0] + $total_dnsbl_score; $RET .= ' CL_IP_NE_HELO='.$score; $helo_ok = 2; $rate += $score; } } elsif($found != 1) # probably DNS error { my $score = ($helo_score[0]-0.1); $RET .= ' NO_MX_A_RECS_FOUND='.$score; $rate += $score; $helo_ok = 2; } ## HELO numeric check ######################################################### my $glob_numeric_score; # check /1.2.3.4/ and /[1.2.3.4]/ if($helo =~ /^[\d|\[][\d\.]+[\d|\]]$/) { $glob_numeric_score = myrnd ( $helo_numeric_score[0] + ($helo_numeric_score[0] * $total_dnsbl_score) ); $RET .= ' HELO_NUMERIC='.$glob_numeric_score; $rate += $glob_numeric_score; } ## FROM Domain vs HELO regex check ############################################ if(!($is_mx)) { $from =~ s/.*@//; # delete localpart my $tmphelo = $helo; my $tmp_helo_domain; # handle sender "(host.)sub.domain.co.uk" # keep: "domain" if ($from =~ s/\.[a-z]{2}\.[a-z]{2}$//i) { $from =~ s/.*\.// } # handle sender "(host.)sub.domain1.com.br" # keep: "domain" elsif($from =~ s/\.(com|org|net)\.[a-z]{2}$//i) { $from =~ s/.*\.// } # handle sender "(host.)sub.domain.com" # handle "(host.)sub.domain.de" # keep: "domain" elsif($from =~ s/\.[a-z]{2,5}$//i) { $from =~ s/.*\.// } # handle helo "(host.)sub.domain.co.uk" if ($tmphelo =~ s/\.[a-z]{2}\.[a-z]{2}$//i) { } # handle helo "(host.)sub.domain1.com.br" elsif($tmphelo =~ s/\.(com|org|net)\.[a-z]{2}$//i) { } # handle helo "(host.)sub.domain.com" # handle helo "(host.)sub.domain.de" # keep: "domain" elsif($tmphelo =~ s/\.[a-z]{2,5}$//i) { } # get helo domain for checking against sender MX entries $tmp_helo_domain = $tmphelo; $tmp_helo_domain =~ s/.*\.//; # set "." (dot) delimiter for comparisions $from = '.' . $from .'.'; $tmphelo = '.' . $tmphelo .'.'; $tmp_helo_domain = '.' . $tmp_helo_domain .'.'; $RET .= ' (check from: ' . $from . ' - helo: ' . $tmphelo . ' - helo-domain: ' . $tmp_helo_domain .') '; # check trusted helos if($helo_ok == 1) { if( (index($tmphelo,$from) != -1) || (index($from,$tmphelo) != -1) || (index($mx_names,$tmp_helo_domain) != -1) || (index($from_addresses,$ip) != -1) ) { $RET .= ' FROM/MX_MATCHES_HELO(DOMAIN)=' . $from_match_regex_verified_helo[1]; $rate += $from_match_regex_verified_helo[1]; } else { my $score = myrnd( ( $from_match_regex_verified_helo[0] + ($total_dnsbl_score/4) + ($bogus_mx_penalty * $bogus_mx_penalty) + $glob_numeric_score ) ); $RET .= ' FROM/MX_MATCHES_NOT_HELO(DOMAIN)='.$score; $rate += $score; $do_client_from_check = 1; } } elsif(index($client_name,$from) != -1 && $squared_helo != 1) { $RET .= ' CL_HOSTNAME_MATCHES_FROM(DOMAIN)=' . $helo_ip_in_client_subnet[1]; $rate += $helo_ip_in_client_subnet[1]; $helo_ok = 1; $do_reverse_check = 0; $do_client_from_check = 0; $helo_untrusted_ok = 0; } elsif($helo_untrusted_ok == 1 && $squared_helo != 1) { # check untrusted helos if( (index($tmphelo,$from) != -1) || (index($from,$tmphelo) != -1) || (index($mx_names,$tmp_helo_domain) != -1) || (index($client_name,$from) != -1) ) { $RET .= ' FROM/MX_MATCHES_UNVR_HELO(DOMAIN)_OR_CL_NAME(DOMAIN)=' . $from_match_regex_unverified_helo[1]; $rate += $from_match_regex_unverified_helo[1]; } else { my $score = ( $from_match_regex_unverified_helo[0] + $total_dnsbl_score + ($bogus_mx_penalty * $bogus_mx_penalty) ); $RET .= ' FROM/MX_MATCHES_NOT_UNVR_HELO(DOMAIN)_NOR_CL_NAME(DOMAIN)='.$score; $rate += $score; $do_client_from_check = 1; } } # check totaly failed helos elsif(index($tmphelo,$from) != -1 || index($from,$tmphelo) != -1) { $RET .= ' MAIL_SEEMS_FORGED='.$from_match_regex_failed_helo[0]; $rate += $from_match_regex_failed_helo[0]; } elsif(index($tmphelo,$from) == -1 || index($from,$tmphelo) == -1) { my $score = ( $from_match_regex_failed_helo[0] + 0.5 + $total_dnsbl_score ); $RET .= ' FROM_NOT_FAILED_HELO(DOMAIN)='.$score; $rate += $score; } } ## Reverse IP == dynhost check ############################################### my $ip_res = $res->send("$ip"); my @reverse_ips; if($ip_res && $ip_res->answer) { foreach my $tmprr ($ip_res->answer) { if($tmprr->type eq 'PTR') { my $tmpptr = $tmprr->ptrdname; $tmpptr =~ s/\.$//; push(@reverse_ips, lc($tmpptr)); } } } if((!($client_seems_dialup)) && ($mx_ok != 1)) { foreach my $revhost (@reverse_ips) { if( $revhost =~ /(mx|smtp|mail|dedicated|(\b|[^n])stat).*?\..*?\./i ) { last } if ( $revhost =~ /(\.dip|cable|ppp|dial|dsl|dyn|client|rev.*?(ip|home)).*?\..*?\./i || $helo =~ /[a-z\.\-\_]+\d{1,3}[-._]\d{1,3}[-._]\d{1,3}[-._]\d{1,3}/i ) { $client_seems_dialup = 1; $total_dnsbl_score += $client_seems_dialup_score[0]; $rate += $client_seems_dialup_score[0]; $RET .= ' CL_SEEMS_DIALUP=' . $client_seems_dialup_score[0]; last; } } } ## Reverse IP == HELO check ################################################### $found = 0; my $rev_processed = 0; if(($helo_ok != 1 && $helo_untrusted_ok != 1) || $do_reverse_check) { foreach my $revhost (@reverse_ips) { $rev_processed = 1; $revhost =~ s/\.*$//; if ( $revhost eq $helo ) { $found = 1; $RET .= ' REV_IP_EQ_HELO='.$client_ip_eq_helo_score[1]; $rate += $client_ip_eq_helo_score[1]; last; } my $partsfound = 0; my $tmprevhost = reverse($revhost); my $tmphelo = reverse($helo); $tmphelo =~ s/.*?\.([^.]+).*/$1/; if( ($tmprevhost =~ /\.\Q$tmphelo\E$/i ) || ($tmprevhost =~ /\.\Q$tmphelo\E\./i) ) { $partsfound = 1; } if( $partsfound != 1 ) { my $tmphelo = reverse($helo); $tmprevhost =~ s/.*?\.([^.]+).*/$1/; if( ($tmphelo =~ /\.\Q$tmprevhost\E$/i ) || ($tmphelo =~ /\.\Q$tmprevhost\E\./i) ) { $partsfound = 1; } } if($partsfound == 1) { $found = 1; $RET .= ' REV_IP_EQ_HELO_DOMAIN='.$client_ip_eq_helo_score[1]; $rate += $client_ip_eq_helo_score[1]; last; } } if($rev_processed != 1 && $recs_found != 1) { $RET .= ' NO_DNS_RECORDS=0.5'; $rate += 0.5; $dnserr = 1; } if($found != 1 && $squared_helo != 1) { $RET .= ' RESOLVED_IP_IS_NOT_HELO='.$client_ip_eq_helo_score[0]; $rate += $client_ip_eq_helo_score[0]; } else { if( ! ($cl_hostname && $cl_hostname ne "unknown") ) { $helo_untrusted_ok = 1; } else { $helo_untrusted_ok = 0; } } } ## HELO dialup check ########################################################## my $DYN_DNS_MSG = ''; if( ( ($enforce_dyndns_score[0] != 0) || ($client_seems_dialup != 1) ) && (!($mx_ok)) && (!($ip_eq_from)) && $helo !~ /(mx|smtp|mail|dedicated|(\b|[^n])stat).*?\..*?\./i && ( ( $helo =~ /(\.dip|cable|ppp|dial|dsl|dyn|client|rev.*?(ip|home)).*?\..*?\./i ) || ( $helo =~ /[a-z\.\-\_]+\d{1,3}[-._]\d{1,3}[-._]\d{1,3}[-._]\d{1,3}/i # that's an ugly regex! watch this! ) ) ) { $helo_seems_dialup = 1; $DYN_DNS_MSG = "; Please use DynDNS"; if($helo_ok == 1) { my $score = $helo_seems_dialup[0] + $enforce_dyndns_score[0]; $RET .= ' HELO_SEEMS_DIALUP='.$score; $rate += $score; } else { my $score = $failed_helo_seems_dialup[0] + $enforce_dyndns_score[0]; $RET .= ' NOK_HELO_SEEMS_DIALUP='.$score; $rate += $score; } } ## From has nobody/anonymous user ############################################# my $anon= 0; if($orig_from =~ /(nobody|anonymous)\@/) { my $score = $from_anon[0] + $total_dnsbl_score + $glob_numeric_score; $RET .= ' FROM_NBDY_ANON='.$score; $rate += $score; $anon = 1; } ## client == MX/A FROM domain ################################################# if( ($mx_ok != 1) && ( ($do_client_from_check) && ($dnsbl_hits > 0) ) && ( $squared_helo != 1) ) { if( index($from_addresses, $ip) == -1 ) { my $score = $helo_from_mx_eq_ip_score[0] + $total_dnsbl_score; $RELAYMSG = '; please relay via your ISP ('.$from_domain.')'; $RET .= ' CLIENT_NOT_MX/A_FROM_DOMAIN='.$score; $rate += $score; if( index($from_addresses, $subip) == -1 ) { $RET .= ' CLIENT/24_NOT_MX/A_FROM_DOMAIN='.$score; $rate += $score; } } } ## From domain multiparted check ############################################## if( (!($helo_ok || $mx_ok)) && ($rate < $REJECTLEVEL) && ($orig_from =~ /\@.*?\..*?\./) ) { my $score = $from_multiparted[0] + $total_dnsbl_score; $RET .= ' FROM_MULTIPARTED='.$score; $rate += $score; } ## Random sender check ######################################################## if( ($rate < $REJECTLEVEL) && ( ($orig_from =~ /[bcdfgjklmnpqrtvwxz]{5,}.*\@/i) || ($orig_from =~ /[aeiou]{4,}.*\@/i) ) ) { my $score = ( $total_dnsbl_score + ($total_dnsbl_score * $random_sender_score[0]) + $random_sender_score[0] ); $RET .= ' RANDOM_SENDER=' . $score; $rate += $score; $rhsbl_penalty = $rhsbl_penalty_score[0] * $random_sender_score[0]; } ## rhsbl check ################################################################ my $in_rhsbl; my $RHSBLMSG = ''; if($rate < $REJECTLEVEL) { $orig_from =~ /@(.*)/; my $query = $1; if( ($do_client_from_check == 1) || ( ($helo_untrusted_ok == 1) && ($client_in_from != 1) ) || ($bogus_mx == 1) ) { $rhsbl_penalty += $rhsbl_penalty_score[0]; } for($i=0;$i < @rhsbl_score; $i += 4) { my $answer = rbl_lookup($query.'.'.$rhsbl_score[$i], 'A'); if(!($answer)) { if($maxdnserr-- <= 1) { $accepted = $instance; return ("$RETANSW $MAXDNSERRMSG in " . $rhsbl_score[$i].' lookups'); } next; } if($answer > 0) { my $score = myrnd( ($rhsbl_score[$i+1] + $rhsbl_penalty ) + ($total_dnsbl_score/2) ); $RET .= ' IN_'.$rhsbl_score[$i+3].'=' . $score; $rate = myrnd($rate + $score); $RHSBLMSG .= '; in '.$rhsbl_score[$i]; } } } ############################################################################### # parse and store results, do some cleanup, return results # sanitize rate, perl gives inaccurate results in computings like # -4.6 + 4.3 $rate = myrnd($rate); if(($DEBUG) || ($CMD_DEBUG == 1)) { $addresses =~ s/ $//; $RET .= ' '; } mylog(info=>"weighted check: $RET; $trace_info; rate: $rate"); if(($dnserr == 1) && ($dnsbl_hits < 2)) # applies if not too { # much dnsbl listed my $my_DNSERRMSG = $DNSERRMSG . ' Your HELO: '.$helo.', IP: '.$ip; return($my_DNSERRMSG); } if($rate >= $REJECTLEVEL) { $blocked = $instance; $my_REJECTMSG = $REJECTMSG; $dont_cache = 0; if($rate < $DEFER_LEVEL) { my @defer_arr = split(' ', $DEFER_STRING); foreach(@defer_arr) { if(index($RET, ' '.$_) != -1) { $dont_cache = 1; $my_REJECTMSG =~ s/^.*? /$DEFER_ACTION /; last; } } } if($my_REJECTMSG =~ /^(4|DEFER|rc\:)/i) { $dont_cache = 1; } if(($CACHESIZE > 0) && ($maxdnserr > 0) && (!($dont_cache))) { # add only the IP to SPAM cache if the client is dnsbl listed, # a dynamic client or has no ok helo # This should help in case of some dictionary attacks if(($dnsbl_hits >= 1 || $client_seems_dialup || $helo_ok != 1)) { cache_query('nadd', $ip, $rate); } else { cache_query('nadd', $ip, $rate, $orig_from, $from_domain); } } if(($helo_ok != 1) && ($helo_untrusted_ok != 1)) { my $EREJECTMSG = $my_REJECTMSG . '; MTA helo: '.$helo.', MTA hostname: ' . $client_name.'['.$ip.'] (helo/hostname mismatch)'; return($EREJECTMSG.$RHSBLMSG.$RELAYMSG.$DYN_DNS_MSG); } return($my_REJECTMSG.$RHSBLMSG.$RELAYMSG.$DYN_DNS_MSG); } else { if(($POSCACHESIZE > 0) && ($dnsbl_hits < 1)) { cache_query('padd', $ip, $rate, $orig_from, $from_domain); } $accepted = $instance; return("$RETANSW $RET; rate: $rate"); } } # # cache_query (QUERY, IP, SENDER, [RATE], DOMAIN) # # Function for querying the cache daemon # # QUERY : "nadd" - negative (SPAM) add # : "padd" - positive (HAM) add # : "ask" - is cached as SPAM or HAM? # : "kill" - terminated cache # : "start" - pre-start cache # IP : Client IP # SENDER : Sender Address # RATE : store rate in "Xadd" queries # DOMAIN : Sender domain # # Returns: CACHEREJECTMSG when SPAM listed # : "rate: $rate" when HAM listed # : undef in all other cases sub cache_query { my $query = shift(@_) || ''; my $ip = shift(@_) || ''; my $rate = shift(@_) || ''; my $sender = shift(@_) || ''; my $domain = shift(@_) || ''; $! = ''; $@ = (); if( (!($csock)) || ($csock && (!($csock->connected))) ) { $csock = IO::Socket::UNIX->new($SPATH); if( (!($csock = IO::Socket::UNIX->new($SPATH))) ) { if($query ne 'start') { mylog(warning=>"cache_query: \$csock couln't be created: $@, calling spawn_cache()"); } else { mylog(info=>'cache_query: start: calling spawn_cache()'); } spawn_cache(); return(undef); } if( $query eq 'start') { $csock->close(); # dont inherit this socket; return(undef); } } if($csock && ($csock->connected)) { my $buf; my $alrm = 0; $SIG{'ALRM'} = sub { # ignore alarms; $alrm = 1; }; $csock->autoflush(1); mylog(info=>"cache_query: $query $ip $rate $sender $domain") if $DEBUG; print $csock "$CVERSION $query $ip $rate $sender $domain\n"; my $sline; my $match = $query.$ip.$sender.' '; $csock->timeout($IPC_TIMEOUT); while($csock->connected) { eval { local $SIG{'ALRM'} = sub { mylog(warning=>'cache_query: timeout'); die "ETIMEOUT"; }; alarm $IPC_TIMEOUT; $csock->recv($buf, 4069); alarm 0; }; if($@ || (!($buf))) { return(undef) }; if($STATS) { $buf =~ s/^.*?(blocked|pass)/$1/; print $buf; return(undef) if $buf =~ /\nEOF\n/; next; } if($buf !~ /\n$/) { $sline .= $buf; next; } else { $sline .= $buf; } $sline =~ tr/\r\n//d; mylog(info=>"cache_query: \"$sline\" vs \"$match\"") if $DEBUG; if(index($sline, 'unknown cache request') >= 0) { print $csock "kill\n"; close($csock); $csock = ""; return(undef); } # return a proper line in case we had query timeouts # works like "next if not $sline =~ s/.*\Q$match\E//;" but faster my $index = rindex($sline, $match); next if $index < 0; return(substr($sline, $index + length($match))); } return(undef); # just in case ... } else { mylog(info=>'could not connect to cache (maybe just starting up)'); return(undef); } } ############################################################################# # # CACHE PROCESS # ############################################################################# sub spawn_cache { my $rname = getpwuid($<); if(($rname ne $USER) && (!($CMD_DEBUG))) { mylog(warning=>"cache: running as wrong user: ".$rname."; please edit master.cf, set user=$USER and/or add $USER to your user and group accounts; cache not spawned."); return(undef); } if(!( $< = getpwnam($USER))) { mylog(warning=>"cache: couldn't change UID to user $USER: $!"); die $!; } if(!( $( = getpwnam($USER) )) { mylog(warning=>"cache: couldn't change GID to user $GROUP: $!"); } create_lockpath('cache'); # avoid races at startups mkdir $LOCKPATH.'/cache_lock' or return undef; # check if a cache-socket file exist, and # whether we can connect to it. if( -S $SPATH) { my $test_sock = IO::Socket::UNIX->new($SPATH); if ($test_sock && $test_sock->connected) { mylog(warning=>"cache-init: error: socket exists and is connectable"); return undef; } close($test_sock); } # no cache seems to exist, go create one unlink $SPATH; use POSIX qw(setsid); defined(my $pid = fork) or die "cache: fork: $!"; if($pid) { return(undef); } setsid or die "cache: setsid: $!"; $SIG{__DIE__} = sub { die @_ if index($_[0], 'ETIMEOUT') == 0; mylog(warning=>"cache: err: @_"); unlink $SPATH; rmdir $LOCKPATH.'/cache_lock'; }; # change directory to $LOCKPATH in order to get some # coredumps just in case. chdir "$LOCKPATH/cores/cache" or die "cache: chdir $LOCKPATH/cores/cache: $!"; mylog(info=>'cache spawned'); $0 = 'policyd-weight (cache)'; if($CMD_DEBUG != 1) { close(STDIN); close(STDOUT); close(STDERR); open (STDIN, '/dev/null'); open (STDOUT, '>/dev/null'); open (STDERR, '>/dev/null'); } $s = '' if $s; # close socks, we don't need them anymore. $res = '' if $res; $sock->close if $sock; $new_sock->close if $new_sock; $tcp_socket->close if $tcp_socket; $SIG{'TERM'} = sub { unlink $SPATH; rmdir $LOCKPATH.'/cache_lock'; mylog(info=>"cache: SIG@_, terminating"); exit 0; }; $SIG{'QUIT'} = sub { unlink $SPATH; rmdir $LOCKPATH.'/cache_lock'; mylog(info=>"cache: SIG@_, terminating"); exit 0; }; $SIG{'INT'} = sub { unlink $SPATH; rmdir $LOCKPATH.'/cache_lock'; mylog(warning=>"cache: SIG@_, terminating"); exit}; # commented because an interrupt of 'policyd-weight -s' may # cause a SIGPIPE # changed in: 0.1.14 beta-12 #$SIG{'PIPE'} = sub { unlink $SPATH; # rmdir $LOCKPATH.'/cache_lock'; # mylog(warning=>"cache: SIG@_, terminating"); exit}; $SIG{'PIPE'} = 'IGNORE'; $SIG{'SYS'} = sub { unlink $PIDFILE; rmdir $LOCKPATH.'/cache_lock'; mylog(warning=>"cache: SIG@_, terminating"); exit}; $SIG{'USR1'} = sub { unlink $PIDFILE; rmdir $LOCKPATH.'/cache_lock'; mylog(warning=>"cache: SIG@_, terminating"); exit}; $SIG{'USR2'} = sub { unlink $PIDFILE; rmdir $LOCKPATH.'/cache_lock'; mylog(warning=>"cache: SIG@_, terminating"); exit}; if($SIG{'POLL'}) { $SIG{'POLL'} = sub { unlink $PIDFILE; rmdir $LOCKPATH.'/cache_lock'; mylog(warning=>"cache: SIG@_, terminating"); exit}; } if($SIG{'UNUSED'}) { $SIG{'UNUSED'} = sub { unlink $PIDFILE; rmdir $LOCKPATH.'/cache_lock'; mylog(warning=>"cache: SIG@_, terminating"); exit}; } # core dumpers $SIG{'SEGV'} = sub { $SIG{"ABRT"} = ''; # cluck; unlink $SPATH; rmdir $LOCKPATH.'/cache_lock'; mylog(warning=>"cache: SIG@_:".longmess(). " terminating"); CORE::dump(); exit}; $SIG{'ILL'} = sub { $SIG{"ABRT"} = ''; unlink $SPATH; rmdir $LOCKPATH.'/cache_lock'; mylog(warning=>"cache: SIG@_,".longmess(). " terminating"); CORE::dump(); exit}; $SIG{'ABRT'} = sub { unlink $SPATH; rmdir $LOCKPATH.'/cache_lock'; mylog(warning=>"cache: SIG@_,".longmess(). " terminating"); CORE::dump(); exit}; $SIG{'FPE'} = sub { $SIG{"ABRT"} = ''; unlink $SPATH; rmdir $LOCKPATH.'/cache_lock'; mylog(warning=>"cache: SIG@_,".longmess(). " terminating"); CORE::dump(); exit}; $SIG{'BUS'} = sub { $SIG{"ABRT"} = ''; unlink $SPATH; rmdir $LOCKPATH.'/cache_lock'; mylog(warning=>"cache: SIG@_,".longmess(). " terminating"); CORE::dump(); exit}; use strict; my $readable_handles = new IO::Select(); umask(0007); # alow only owner and group to read/write from/to socket our $lsock = IO::Socket::UNIX->new( Listen => $SOMAXCONN, Local => $SPATH) or die "warning: cache: $@ $!"; rmdir $LOCKPATH.'/cache_lock'; chown($<, $(, $SPATH); # set correct socket owner and group $readable_handles->add($lsock); $| = 1; my $new_readable; my $i; my $KILL; our $poscache_cnt = 0; our $cache_cnt = 0; our $maintenance = 0; our $FORCE_MAINT; my $old_mtime; if($conf ne 'default settings') { $old_mtime = (stat($conf))[9]; } ptime_conv(); while(1) { autoflush $lsock 1; $FORCE_MAINT = 1; ($new_readable) = IO::Select->select($readable_handles, undef, undef, $MAXIDLECACHE); foreach my $sock (@$new_readable) { $FORCE_MAINT = 0; if($sock == $lsock) { my $new_sock = $sock->accept(); $new_sock->autoflush(1); $readable_handles->add($new_sock); } else { $sock->autoflush(1); my $buf = <$sock>; if(($buf) && ($buf =~ /\n.*?\n/)) { mylog(info=>'cache: multiline request. Doh!'); } $buf =~ tr/\r\n//d if $buf; if($buf) { my $time = time; my $ret = '0'; # this var will hold the returned # result for the client if not told # within the routines my($cv, $query, $ip, $rate, $sender, $domain) = split(/ /, lc($buf)); if($CVERSION != $cv && (!($KILL))) { mylog(info=>'cache: new cache version, terminating ASAP') if (!($KILL)); $KILL = 1; $query = ''; } if($query eq 'ask') { # check whether IP or IP-Sender are in SPAM cache foreach my $ckey ($ip, $ip.'-'.$sender) { if($cache{$ckey}) { my $tdiff = $time - $cache{$ckey}[2]; if( ($cache{$ckey}[1] <= 0) && ($tdiff > $NTIME) ) { # NTTL reached and client retried it # after NTIME seconds $ret = '0'; delete($cache{$ckey}); --$cache_cnt; } else { if($tdiff > $NTIME) { $cache{$ckey}[1] -= 1; } $ret = $CACHEREJECTMSG. ' - retrying too fast. penalty: '. $NTIME.' seconds x '. $cache{$ckey}[1].' retries.'; $cache{$ckey}[2] = $time; last; } } } if(!($ret)) { # ask the HAM cache my $ckey = $ip.'-'.$domain; if($poscache{$ckey}) { $ret = "rate: "; # check entry time if($time - $poscache{$ckey}[3] > $my_TEMP_PTIME) { if( ($poscache{$ckey}[1] > 0) && ($time - $poscache{$ckey}[4] < $my_PTIME) ) { $ret = "rate:hard: "; $poscache{$ckey}[1] -= 1; } else { $poscache{$ckey}[1] = $PTTL; $poscache{$ckey}[4] = $time; } } $ret .= $poscache{$ckey}[0]; $poscache{$ckey}[2] = $time; } } } elsif($query eq 'padd') { my $ckey = $ip.'-'.$domain; ++$poscache_cnt unless $poscache{$ckey}; $poscache{$ckey}[0] = $rate; $poscache{$ckey}[1] = $PTTL; $poscache{$ckey}[2] = $time; # last seen $poscache{$ckey}[3] = $time; # TEMP_PTIME $poscache{$ckey}[4] = $time; # PTIME ++$maintenance; } elsif($query eq 'nadd') { my $ckey = $ip; if($domain) { $ckey = $ip.'-'.$sender; } ++$cache_cnt unless $cache{$ckey}; $cache{$ckey}[0] = $rate; $cache{$ckey}[1] = $NTTL; $cache{$ckey}[2] = $time; ++$maintenance; } elsif($query =~ /^stat/) { while ( my ($key, $val) = each(%cache) ) { $ret .= "blocked: $key ".join(" ",@$val)."\n"; } while ( my ($key, $val) = each(%poscache) ) { $ret .= "pass: $key ".join(" ",@$val)."\n"; } $ret .= "EOF"; } elsif($query eq 'reload') { $FORCE_MAINT = 1; } elsif($query eq 'kill') { $KILL = 1; } else { $ret = "unknown cache request: $buf\nEOF"; } print $sock $query.$ip.$sender.' '.$ret."\n"; } else { $readable_handles->remove($sock); close ($sock); } } } ## kill the cache if(($KILL) || (($FORCE_MAINT) && ($CMD_DEBUG))) { my $dbmsg = ''; $dbmsg = 'debug ' if $CMD_DEBUG; unlink ($SPATH); if($lsock) { close ($lsock) }; mylog (info=>$dbmsg.'cache killed'); exit(0); } if( ($maintenance >= $MAINTENANCE_LEVEL) || ($FORCE_MAINT == 1) ) { $maintenance = 0; conf_check('cache'); } ## clean up cache if($poscache_cnt > $POSCACHEMAXSIZE) { my $purgecnt = 0; my $startt = time; for(sort { $poscache{$a}[2] <=> $poscache{$b}[2] } keys %poscache) { if($poscache_cnt > $POSCACHESIZE) { delete($poscache{$_}); ++$purgecnt; --$poscache_cnt; } else { last; } } if($purgecnt > 0) { mylog(info=>"cache: purged $purgecnt from HAM cache, time: ".(time - $startt).'s'); } } if($cache_cnt > $CACHEMAXSIZE) { my $purgecnt = 0; my $startt = time; for(sort { $cache{$a}[2] <=> $cache{$b}[2] } keys %cache) { if($cache_cnt > $CACHESIZE) { delete($cache{$_}); ++$purgecnt; --$cache_cnt; } else { last; } } if($purgecnt > 0) { mylog(info=>"cache: purged $purgecnt from SPAM cache, time: ".(time - $startt).'s'); } } } } # # mylog(FACILITY, STRING) # # prints FACILITY, STRING on STDOUT when in command-line debug (-d) mode # otherwise passes it to syslog() # sub mylog { my $fac = shift(@_); my $string = join(' ', @_); if($CMD_DEBUG) { my $now = scalar(localtime); $now =~ /(\d\d:\d\d:\d\d)/; print("$1 $fac: $string"); print "\n"; } else { my $log_trap; if($fac ne 'info') { $string = $fac.': '.$string; } $@ = ''; eval { local $SIG{'__DIE__'}; syslog($fac, "%s", $string); }; while($@) { if($log_trap++ >= 5) { emerge_log($fac, $string); last; } select(undef, undef, undef, 0.2); # sleep 0.2 sec $@ = ''; eval { local $SIG{'__DIE__'}; syslog($fac, "%s", $string); }; } } } # emerge_log is a routine which shouldn't die() # it logs entries in case of syslog absence # a die() in emerge_log could mean a log-loop # this routine is not resource optimized sub emerge_log { local $SIG{__DIE__}; open(ELOG, ">>$LOCKPATH/polw-emergency.log"); print ELOG localtime().' '. $syslog_ident. '['.$$."]: $syslog_facility: $_[1]\n"; close ELOG; } # rbl_lookup RBL_QUERY [TYPE] # returns: 1: found, -1: not found, 0: error, -2: sock err # remember to give IP octets in reversed order. # EG: IP: 121.122.123.124, Host: mail.example.com, Rbl: bl.rbl.com # RBL_QUERY : "124.123.122.121.bl.rbl.com" # RHSBL_QUERY: "mail.example.com.bl.rbl.com" # TYPE : additonal and usually not needed, default is TXT # In case of weird errors it tries to use Net::DNS # You may force the permanent usage of Net::DNS by global setting USE_NET_DNS sub rbl_lookup { my @bu = @_; if($bu[0] =~ /[^.]{64}/) { return (1) }; # see RFC 1035 sect. 2.3.4 while(length($bu[0]) > 255) # see RFC 1035 sect. 3.1 { $bu[0] =~ s/.*?\.//; } if(($USE_NET_DNS == 1) || ($] < 5.008000)) { my $answ = $res->send(@bu); if (!($answ)) { return (0) } # dns error elsif(($answ->answer) > 0 ) { return (1) } # found else { return (-1) } # not found } my $query = shift(@bu); my $rtype = shift(@bu); my $oid = 1 + int(rand(65535)); $rtype = 'A' unless ($rtype && $RTYPES{$rtype}); # ID RD QDCOUNT my $p = pack ("n*", $oid, 0x100, 1, 0, 0, 0) . # concatenate the query and pack it into length preceded labels pack ('(C/A*)*', split /\./, $query ). pack ('@ (n*)*', $RTYPES{$rtype}, 1); # ^QTYPE ^QCLASS see: RFC 1035 $SIG{ALRM} = sub { mylog(warning=>"rbl_lookup: SIGALRM trapped?! Report."); return }; my $buf; my $errcnt = 0; my $dropped = 0; while($s) { alarm 0; # reset all eventually alarms if($dropped==0) { mylog(info=>"rbl_lookup: sending: $query, $oid") if $DEBUG; eval { local $SIG{ALRM} = sub { die "ETIMEOUT" }; alarm $DNS_RETRY_IVAL; if($s->send($p) < length($p)) { mylog(warning=>"rbl_lookup: sent bytes != packet size"); ++$errcnt; # timeout or error on sending } alarm 0; }; if($@) { ++$errcnt; mylog(warning=>"rbl_lookup: timeout sending: $query") if $DEBUG; next; } } $dropped = 0; my $buf; eval { local $SIG{ALRM} = sub { die "ETIMEOUT" }; alarm $DNS_RETRY_IVAL; $s->recv($buf, 2048); alarm 0; }; if((!($buf)) && ($errcnt < $DNS_RETRIES)) { ++$errcnt; next; } elsif((!($buf)) && ($errcnt >= $DNS_RETRIES)) { return(0); # too many timeouts or errors } my ($id, $bf, $qc, $anc, $nsc, $arc, $qb) = unpack('n n n n n n a*', $buf); my ($dn, $offset) = dn_expand(\$qb, 0); if(($id && $anc) && ($id == $oid) && ($query eq $dn)) { mylog(info=>"rbl_lookup: $query vs $dn, $oid vs $id, anc == $anc") if $DEBUG; return(1); # found } elsif($id && (!($anc)) && ($id == $oid) && ($query eq $dn)) { mylog(info=>"rbl_lookup: $query vs $dn, $oid vs $id, anc == 0") if $DEBUG; return(-1); # not found } elsif(($id && $dn) && (($query ne $dn) || ($id != $oid))) { mylog(info=>"rbl_lookup: dropped: out:$query vs in:$dn, out:$oid vs in:$id") if $DEBUG; $dropped = 1; return(0) if $errcnt >= $DNS_RETRIES; next; # wrong packet received, drop } mylog(warning=>"rbl_lookup: unknown error: out:$query, in:$dn, out-id:$oid, in-id:$id"); return(0) if $errcnt >= $DNS_RETRIES; ++$errcnt; # unknown error } mylog(warning=>'RBL Socket died, using Net-DNS now.'); $USE_NET_DNS = 1; return(rbl_lookup(@bu)); # return Net::DNS result } sub conf_check { my $who = shift; if($conf ne 'default settings') { my @conf_stat = stat($conf); if( $conf_stat[9] != $old_mtime ) { if(sprintf("%04o",$conf_stat[2]) !~ /(7|6|3|2)$/) { my $conf_str; if(open(CONF, $conf)) { read(CONF,$conf_str,-s CONF); close(CONF); #XXX taint $conf_str as $< enables taint mode ($conf_str) = $conf_str =~ m/(.*)/s; eval $conf_str; if($@) { mylog(warning=>"$who: syntax error in file $conf: ".$@); } else { $old_mtime = $conf_stat[9]; ptime_conv(); mylog(info=>"$who: $conf reloaded"); } } else { mylog(warning=>"$who: could not open $conf: $!"); } } else { mylog(warning=>"$who: conf-err: $conf is world-writeable! Config not reloaded!"); } } } } sub create_lockpath { my $who = shift(@_); if(!( -d $LOCKPATH)) { mkdir $LOCKPATH or die "$who: error while creating $LOCKPATH: $!"; } # # check LOCKPATH, SPATH and cache_lock for being part of symlinks # check_symlnk( $who, $LOCKPATH, $SPATH, "$LOCKPATH/cache_lock" ); my $tuid = $USER; if($USER =~ /[^0-9]/) { if( !(defined( $tuid = getpwnam($USER) ) ) ) { mylog(warning=>"User $USER doesn't exist, create it, or set \$USER"); } } if( !(chown ($tuid, -1, $LOCKPATH)) ) { mylog(warning=> "$who: Couldn't chown $LOCKPATH to $USER ($tuid): $! - UID/EUID: $"); } if( !(chmod (0700, $LOCKPATH)) ) { mylog(warning=> "$who: Couldn't set permissions on $LOCKPATH for $USER ($tuid): $! - UID/EUID: $"); } } # # usage: check_symlnk($caller, @files) # # caller: context specifier (eg: "cache: function:") # files: list of files to check sub check_symlnk { my $who = shift; for ( @_ ) { my $file = File::Spec->canonpath($_); my @stat = lstat( $file ); if(! -e _) { next; } # first, file must not be a symlink if ( -l _ ) { fatal_exit("$who: $file is a symbolic link. Symbolic links are not expected and not allowed within policyd-weight. Exiting!"); } # second, file must be owned by uid root or $USER and # gid root/wheel or $USER if(!( ( $stat[4] == getpwnam($USER) || $stat[4] == "0" ) && ( $stat[5] == getgrnam($GROUP) || $stat[5] == "0" ) ) ) { fatal_exit("$who: $file is owned by UID $stat[4], GID $stat[5]. Exiting!"); } # third, the file/dir must not be world writeable if(sprintf("%04o",$stat[2]) =~ /(7|6|3|2)$/) { fatal_exit("$who: $file is world writeable. Exiting!"); } } } # function for sanitizing floating point output sub myrnd { my $n = index($_[0], "."); if($n > 0) { $n-- if index($_[0], "-") >= 0; return(sprintf("%.".($n+3)."g", $_[0])); } return($_[0]); } sub ptime_conv { # convert PTIME and TEMP_PTIME to seconds my %time_conv; $time_conv{'s'} = 1; $time_conv{'m'} = 60; $time_conv{'h'} = 3600; $time_conv{'d'} = 86400; my $time_unit; if($PTIME =~ /.*?(\d+)([smhd]{0,1}).*/) { if(!($2)) { $time_unit = 's' } else { $time_unit = $2 } $my_PTIME = $1 * $time_conv{$time_unit}; } else { mylog(warning=>"cache: \$PTIME in wrong format. Using default."); $my_PTIME = 10800; # 3 hours } if($TEMP_PTIME =~ /.*?(\d+)([smhd]{0,1}).*/) { if(!($2)) { $time_unit = 's' } else { $time_unit = $2 } $my_TEMP_PTIME = $1 * $time_conv{$time_unit}; } else { mylog(warning=>"cache: \$TEMP_PTIME in wrong format. Using default."); $my_TEMP_PTIME = 259200; # 3 days } mylog(info=>"cache: PTIME: $my_PTIME, TEMP_PTIME: $my_TEMP_PTIME") if $DEBUG or $VERBOSE; } # # Usage: dns_error(\$query_object, \$res_object) # # Returns undef in case of NOERROR or NXDOMAIN # Returns 1 in all other cases # # This function expects references to objects of Net::DNS as arguments sub dns_error { my ($myquery, $myres) = @_; return 1 if not $$myquery; return 1 if not $$myres; return undef if $$myres->errorstring eq 'NOERROR' or $$myres->errorstring eq 'NXDOMAIN'; mylog(debug=>"dns_error: errorstring: ".$$myres->errorstring) if $CMD_DEBUG; return 1; } # # returns 1 if the helo is in [n.n.n.n] notation, # valid, and matches the client ip # sub squared_helo { my $helo = shift; my $ip = shift; if($$helo !~ /^\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]$/ ) { return } my $tmp_helo_ip = $1; my $tmpip = inet_aton( $tmp_helo_ip ); length($tmpip) or return; $tmpip = inet_ntoa($tmpip); if($tmpip eq $$ip) { return 1 } return 0; } policyd-weight-0.1.15.2/policyd-weight.conf.sample0000644000000000000000000002651011675645366020472 0ustar rootroot# ---------------------------------------------------------------- # policyd-weight configuration (defaults) Version 0.1.15 beta-2 # ---------------------------------------------------------------- $DEBUG = 0; # 1 or 0 - don't comment $REJECTMSG = "550 Mail appeared to be SPAM or forged. Ask your Mail/DNS-Administrator to correct HELO and DNS MX settings or to get removed from DNSBLs"; $REJECTLEVEL = 1; # Mails with scores which exceed this # REJECTLEVEL will be rejected $DEFER_STRING = 'IN_SPAMCOP= BOGUS_MX='; # A space separated case-sensitive list of # strings on which if found in the $RET # logging-string policyd-weight changes # its action to $DEFER_ACTION in case # of rejects. # USE WITH CAUTION! # DEFAULT: "IN_SPAMCOP= BOGUS_MX=" $DEFER_ACTION = '450'; # Possible values: DEFER_IF_PERMIT, # DEFER_IF_REJECT, # 4xx response codes. See also access(5) # DEFAULT: 450 $DEFER_LEVEL = 5; # DEFER mail only up to this level # scores greater than DEFER_LEVEL will be # rejected # DEFAULT: 5 $DNSERRMSG = '450 No DNS entries for your MTA, HELO and Domain. Contact YOUR administrator'; $dnsbl_checks_only = 0; # 1: ON, 0: OFF (default) # If ON request that ALL clients are only # checked against RBLs @dnsbl_checks_only_regexps = ( # qr/[^.]*(exch|smtp|mx|mail).*\..*\../, # qr/yahoo.com$/ ); # specify a comma-separated list of regexps # for client hostnames which shall only # be RBL checked. This does not work for # postfix' "unknown" clients. # The usage of this should not be the norm # and is a tool for people which like to # shoot in their own foot. # DEFAULT: empty $LOG_BAD_RBL_ONLY = 1; # 1: ON (default), 0: OFF # When set to ON it logs only RBLs which # affect scoring (positive or negative) ## DNSBL settings @dnsbl_score = ( # HOST, HIT SCORE, MISS SCORE, LOG NAME 'pbl.spamhaus.org', 3.25, 0, 'DYN_PBL_SPAMHAUS', 'sbl-xbl.spamhaus.org', 4.35, -1.5, 'SBL_XBL_SPAMHAUS', 'bl.spamcop.net', 3.75, -1.5, 'SPAMCOP', 'dnsbl.njabl.org', 4.25, -1.5, 'BL_NJABL', 'ix.dnsbl.manitu.net', 4.35, 0, 'IX_MANITU' #'rbl.ipv6-world.net', 4.25, 0, 'IPv6_RBL' #don't use, kept for testing failures! ); $MAXDNSBLHITS = 2; # If Client IP is listed in MORE # DNSBLS than this var, it gets # REJECTed immediately $MAXDNSBLSCORE = 8; # alternatively, if the score of # DNSBLs is ABOVE this # level, reject immediately $MAXDNSBLMSG = '550 Your MTA is listed in too many DNSBLs'; ## RHSBL settings @rhsbl_score = ( 'multi.surbl.org', 4, 0, 'SURBL', 'rhsbl.ahbl.org', 4, 0, 'AHBL', 'dsn.rfc-ignorant.org', 3.5, 0, 'DSN_RFCI', 'postmaster.rfc-ignorant.org', 0.1, 0, 'PM_RFCI', 'abuse.rfc-ignorant.org', 0.1, 0, 'ABUSE_RFCI' ); $BL_ERROR_SKIP = 2; # skip a RBL if this RBL had this many continuous # errors $BL_SKIP_RELEASE = 10; # skip a RBL for that many times ## cache stuff $LOCKPATH = '/tmp/.policyd-weight/'; # must be a directory (add # trailing slash) $SPATH = $LOCKPATH.'/polw.sock'; # socket path for the cache # daemon. $MAXIDLECACHE = 60; # how many seconds the cache may be idle # before starting maintenance routines # NOTE: standard maintenance jobs happen # regardless of this setting. $MAINTENANCE_LEVEL = 5; # after this number of requests do following # maintenance jobs: # checking for config changes # negative (i.e. SPAM) result cache settings ################################## $CACHESIZE = 2000; # set to 0 to disable caching for spam results. # To this level the cache will be cleaned. $CACHEMAXSIZE = 4000; # at this number of entries cleanup takes place $CACHEREJECTMSG = '550 temporarily blocked because of previous errors'; $NTTL = 1; # after NTTL retries the cache entry is deleted $NTIME = 30; # client MUST NOT retry within this seconds in order # to decrease TTL counter # positve (i.,e. HAM) result cache settings ################################### $POSCACHESIZE = 1000; # set to 0 to disable caching of HAM. To this number # of entries the cache will be cleaned $POSCACHEMAXSIZE = 2000; # at this number of entries cleanup takes place $POSCACHEMSG = 'using cached result'; $PTTL = 60; # after PTTL requests the HAM entry must # succeed one time the RBL checks again $PTIME = '3h'; # after $PTIME in HAM Cache the client # must pass one time the RBL checks again. # Values must be nonfractal. Accepted # time-units: s, m, h, d $TEMP_PTIME = '1d'; # The client must pass this time the RBL # checks in order to be listed as hard-HAM # After this time the client will pass # immediately for PTTL within PTIME ## DNS settings $DNS_RETRIES = 2; # Retries for ONE DNS-Lookup $DNS_RETRY_IVAL = 2; # Retry-interval for ONE DNS-Lookup $MAXDNSERR = 3; # max error count for unresponded queries # in a complete policy query $MAXDNSERRMSG = 'passed - too many local DNS-errors'; $PUDP = 0; # persistent udp connection for DNS queries. # broken in Net::DNS version 0.51. Works with # Net::DNS 0.53; DEFAULT: off $USE_NET_DNS = 0; # Force the usage of Net::DNS for RBL lookups. # Normally policyd-weight tries to use a faster # RBL lookup routine instead of Net::DNS $NS = ''; # A list of space separated NS IPs # This overrides resolv.conf settings # Example: $NS = '1.2.3.4 1.2.3.5'; # DEFAULT: empty $IPC_TIMEOUT = 2; # timeout for receiving from cache instance $TRY_BALANCE = 0; # If set to 1 policyd-weight closes connections # to smtpd clients in order to avoid too many # established connections to one policyd-weight # child # scores for checks, WARNING: they may manipulate eachother # or be factors for other scores. # HIT score, MISS Score @client_ip_eq_helo_score = (1.5, -1.25 ); @helo_score = (1.5, -2 ); @helo_from_mx_eq_ip_score = (1.5, -3.1 ); @helo_numeric_score = (2.5, 0 ); @from_match_regex_verified_helo = (1, -2 ); @from_match_regex_unverified_helo = (1.6, -1.5 ); @from_match_regex_failed_helo = (2.5, 0 ); @helo_seems_dialup = (1.5, 0 ); @failed_helo_seems_dialup = (2, 0 ); @helo_ip_in_client_subnet = (0, -1.2 ); @helo_ip_in_cl16_subnet = (0, -0.41 ); @client_seems_dialup_score = (3.75, 0 ); @from_multiparted = (1.09, 0 ); @from_anon = (1.17, 0 ); @bogus_mx_score = (2.1, 0 ); @random_sender_score = (0.25, 0 ); @rhsbl_penalty_score = (3.1, 0 ); @enforce_dyndns_score = (3, 0 ); $VERBOSE = 0; $ADD_X_HEADER = 1; # Switch on or off an additional # X-policyd-weight: header # DEFAULT: on $DEFAULT_RESPONSE = 'DUNNO default'; # Fallback response in case # the weighted check didn't # return any response (should never # appear). # # Syslogging options for verbose mode and for fatal errors. # NOTE: comment out the $syslog_socktype line if syslogging does not # work on your system. # $syslog_socktype = 'unix'; # inet, unix, stream, console $syslog_facility = "mail"; $syslog_options = "pid"; $syslog_priority = "info"; $syslog_ident = "postfix/policyd-weight"; # # Process Options # $USER = "polw"; # User must be a username, no UID $GROUP = ""; # specify GROUP if necessary # DEFAULT: empty, will be initialized as # $USER $MAX_PROC = 50; # Upper limit if child processes $MIN_PROC = 3; # keep that minimum processes alive $TCP_PORT = 12525; # The TCP port on which policyd-weight # listens for policy requests from postfix $BIND_ADDRESS = '127.0.0.1'; # IP-Address on which policyd-weight will # listen for requests. # You may only list ONE IP here, if you want # to listen on all IPs you need to say 'all' # here. Default is '127.0.0.1'. # You need to restart policyd-weight if you # change this. $SOMAXCONN = 1024; # Maximum of client connections # policyd-weight accepts # Default: 1024 $CHILDIDLE = 240; # how many seconds a child may be idle before # it dies. $PIDFILE = "/var/run/policyd-weight.pid"; policyd-weight-0.1.15.2/documentation.txt0000644000000000000000000001644511346523636017021 0ustar rootroot0.1.14 May 10, 2007 policyd-weight documentation 1.0 ............ What is policyd-weight 1.1 ........ What is policyd-weight not 1.2 ..... Who should use policyd-weight 1.3 ...................... Requirements 2.0 .................. How does it work 2.1 .................. How to set it up 2.2 ... How to read/understand the logs 3.0 ............................ Thanks First things first. This documentation IS INCOMPLETE and does not always reflect the current state of the project which is still beta. To get an additional picture I suggest to also read the changelog (changes.txt). Documentation will be updated as soon as I have a satisfactory stable release in terms of: optimal usage of possible techniques, no more techniques left, optimal resource usage, full reliability. I invite everyone to help on the documentation part, as this is the most difficult and laborious - and my English is only good enough to get a drink at the local pub or to ask for the right bus station; you'll know what I mean sooner or later :-) -- rob -- 1.0 What is policyd-weight policyd-weight is a policy server for Postfix written in Perl to score - DNSBLs/RHSBLs - HELO argument - MAIL FROM: argument - Client IP address - DNS client/HELO/FROM entries (A/16 A/24 A/32, PTR/FQDN and Parent Domains MX/16 MX/24 MX/32 for their correctness respectively whether they match. Most MTAs have checks for these things built-in, but unfortunately, those checks are often too restrictive, one hit will cause important mails to get rejected. Thus most companies are forced to have a rather non- restrictive and even insecure MTA setup so they don't lose important mails. policyd-weight is intented to be used right after the RCPT TO command. This way neither the mail headers nor the mail body must be received. This behaviour is different from other filters that must parse (and receive) the complete mail. With the policyd-weight approach we can reject obviously faked mails and MTAs that are listed in too many DNSBLs or are poorly configured. To avoid using extra bandwidth for DNS queries policyd-weight caches the most frequent client/sender combinations. Also, if DNS lookups are necessary it does this intentionally serialized to keep lookups to a minimum. NOTE: It takes some time for new SPAM mailers on the Internet to get listed in DNSBLs, if they behave well and don't forge everything SPAM may appear as normal mail. Filters such as SpamAssassin or amavisd will parse the mail and can report it to DNSBLs if set up this way (consult your SPAM/virus scanner's manual). 1.1 What is policyd-weight not policyd-weight is NOT a SPAM or Virus Filter - as it doesn't parse the contents of the mail. Also policyd-weight is not able to reject Mails bounced or forwarded by correct MTAs. Example: you have an account at yahoo.com, and a have set it to forward mail to your company account. yahoo.com sends with correct MTAs, and thus SPAM received from your yahoo.com account will pass this filter. 1.2 Who should use policyd-weight For now: for Postfix users that receive or relay mail via SMTP and for people that - receive lots of e-mails caught by SpamAssassin or amavisd (I'm talking about +300/day). - want to reduce bandwidth-usage caused by bogus mails (forged SPAM/virus mails) - want to reduce CPU usage caused by scanning bogus mails. - don't want to lose legitimate mails due to overly restrictive header checks - want to reduce bounce mails from internal servers or filters 1.3 Requirements Postfix version 2.1 or higher (tested with 2.1.5 and 2.3.1) Perl 5.8 version 5.8 recommended, 5.6 might work, too Perl modules Fcntl (standard in Perl 5.8.8) Sys::Syslog Net::DNS Mail must be accepted directly from the Internet (aka first in line) A fast caching DNS server in your network is highly recommended! 2.0 How does it work Well, slightly different from beta to beta. -> to be continued after a stable release 2.1 How to set it up - copy policyd-weight to the proper location for your OS, i.e. /usr/local/bin/policyd-weight - set correct permissions: chown root /usr/local/bin/policyd-weight chgrp wheel /usr/local/bin/policyd-weight chmod 0755 /usr/local/bin/policyd-weight - create a Unix system account for user and group "polw", the user does not need a shell or a home directory - create an rc init script or manage otherwise so that "/usr/local/bin/policyd-weight start" gets executed before Postfix at boot-time - remove unnecessary reject_rbl_client and reject_rhsbl_client checks from Postfix' main.cf - edit: [main.cf]: smtpd_recipient_restrictions = permit_mynetworks, ... reject_unauth_destination, check_policy_service inet:127.0.0.1:12525 ... Important, keep your old SASL permits (permit_sasl_authenticated), they must come before check_policy_service If you are using Postfix servers on different hosts you can let other Postfix instances ask the server on which policyd-weight runs by using in their main.cf: smtpd_recipient_restrictions = ... reject_unauth_destination check_policy_service inet:$POLICY_SERVER_IP:12525 where $POLICY_SERVER_IP needs to be replaced with the IP of the server which runs policyd-weight. Also you need to set $BIND_ADDRESS = 'all'; in /policyd-weight.conf Make sure that only your own Postfix servers are allowed to connect to that port by adjusting your firewall rules. policyd-weight has NO ACL mechanism for that due to performance and anti-DoS reasons. For adjusting scores or other policyd-weight parameters you can create /etc/policyd-weight.conf and insert the changed parameter/value there. To see the available configuration options execute "policyd-weight defaults". It is good practice to only add changed parameters to the config file and omit the defaults, so the file can more easily be maintained. "policyd-weight --help" gives a short help on how to use the command-line switches. 2.2 How to read/understand the logs To see mails rejected by policyd-weight: grep "policyd-weight.*action=" /var/log/maillog | grep -v DUNNO To see mails accepted by policyd-weight: grep "policyd-weight.*action=" /var/log/maillog | grep DUNNO ...to be continued. 3.0 Thanks Ralf Hildebrandt, it was him who set me on fire, also for his tests. Bob Tito, for testing and feeding me with results Philipp Koller for his patches and help on Solaris, documentation and website. All Spammers that provided me with food and enlargement pills. To the mailing-list users which reported bugs and odd behavior. policyd-weight-0.1.15.2/Makefile0000644000000000000000000000004011675645625015037 0ustar rootrootall: clean: distclean: install: