1#!/usr/local/bin/perl -T -w 2 3############################ 4package postfwd3::basic; 5 6use warnings; 7use strict; 8use IO::Socket qw(SOCK_STREAM); 9use Sys::Syslog qw(:DEFAULT setlogsock); 10# export 11use Exporter qw(import); 12our @EXPORT = qw( 13 %postfwd_settings %postfwd_patterns 14 &uniq &init_log &log_info &log_note 15 &log_warn &log_err &log_crit 16); 17our @EXPORT_OK = qw( 18 %postfwd_commands 19 &wantsdebug &hash_to_list 20 &hash_to_str &str_to_hash 21 &check_inet &check_unix &ts 22 &load_hash &save_hash 23 $TIMEHIRES $STORABLE $NETADDR $DIGESTMD5 24); 25our($TIMEHIRES); our($STORABLE); our($NETADDR); our($NETCIDR); our($DIGESTMD5); 26BEGIN { 27 # use Time::HiRes if available 28 eval { require Time::HiRes }; 29 $TIMEHIRES = ($@) ? 0 : (Time::HiRes->VERSION || 'available'); 30 # use Storable if available 31 eval { require Storable }; 32 $STORABLE = ($@) ? 0 : (Storable->VERSION || 'available'); 33 eval { require NetAddr::IP }; 34 $NETADDR = ($@) ? 0 : (NetAddr::IP->VERSION || 'available'); 35 eval { require Net::CIDR::Lite }; 36 $NETCIDR = ($@) ? 0 : (Net::CIDR::Lite->VERSION || 'available'); 37 eval { require Digest::MD5 }; 38 $DIGESTMD5 = ($@) ? 0 : (Digest::MD5->VERSION || 'available'); 39 # use Storable if available 40 Storable->import( qw(nstore retrieve) ) if $STORABLE; 41 # This check prevents an error on "CentOS release 6" 42 eval { require Net::Server::Multiplex }; 43 die "\nERROR: Required perl module 'Net::Server::Multiplex' not found!\n\n" if ($@); 44}; 45 46 47# basics 48our $NAME = "postfwd3"; 49our $VERSION = "2.03"; 50our $DEFAULT = 'DUNNO'; 51 52# change this, to match your POD requirements 53# we need pod2text for the -m switch (manual) 54$ENV{PATH} = "/bin:/usr/bin:/usr/local/bin"; 55$ENV{ENV} = ""; 56our($cmd_manual) = "pod2text"; 57our($cmd_pager) = "more"; 58 59my $sepreq = '///'; 60my $seplst = ':::'; 61my $seplim = '~~~'; 62my $nounixsock = ($^O eq 'solaris'); 63my $terminating = 0; 64 65# program settings 66our %postfwd_settings = ( 67 base => { 68 user => 'nobody', 69 group => 'nobody', 70 log_level => 2, 71 log_file => 'Sys::Syslog', 72 syslog_ident => "$NAME", 73 umask => "0177", 74 no_client_stdout => 1, 75 }, 76 master => { 77 pid_file => "/var/tmp/$NAME-master.pid", 78 watchdog => 60, 79 failures => 7, 80 respawn => 4, 81 daemons => [ 'cache', 'server' ], 82 }, 83 cache => { 84 commandline => " ".$NAME."::cache", 85 syslog_ident => "$NAME/cache", 86 host => (($nounixsock) ? "127.0.0.1" : ""), 87 port => (($nounixsock) ? "10043" : "/var/tmp/$NAME-cache.socket"), 88 proto => (($nounixsock) ? "tcp" : "unix"), 89 check => (($nounixsock) ? \&check_inet : \&check_unix), 90 umask => "0177", 91 }, 92 server => { 93 commandline => " ".$NAME."::policy", 94 syslog_ident => "$NAME/policy", 95 host => '127.0.0.1', 96 port => '10045', 97 proto => "tcp", 98 check => \&check_inet, 99 umask => "0111", 100 # child control 101 #check_for_dead => 30, 102 #check_for_waiting => 10, 103 min_spare_servers => 5, 104 min_servers => 10, 105 max_spare_servers => 50, 106 max_servers => 100, 107 max_requests => 200, # lowered 108 child_communication => 1, # children report data to parent for summary in PreFork mode 109 leave_children_open_on_hup => 1, # children should finish their work 110 }, 111 syslog => { 112 nolog => 0, 113 noidlestats => 0, 114 norulestats => 0, 115 name => $NAME, 116 facility => 'mail', 117 options => 'pid', 118 # allow "umlaute" ;) 119 #unsafe_charset => qr/[^\x20-\x7E,\x80-\xFE]/, 120 unsafe_charset => qr/[^\x20-\x7E]/, 121 unsafe_version => ( (not(defined $Sys::Syslog::VERSION) or $Sys::Syslog::VERSION lt '0.15') 122 or (not(defined $Net::Server::VERSION) or $Net::Server::VERSION lt '0.94') ), 123 perfmon => 0, 124 stdout => 0, 125 stdin => 0, 126 }, 127 timeout => { 128 rule => 40, 129 cache => 3, 130 server => 3, 131 config => 4, 132 }, 133 request => { 134 ttl => 600, 135 cleanup => 600, 136 no_sender => 0, 137 rdomain_only => 0, 138 no_size => 0, 139 nolog => 0, 140 noparent => 0, 141 autocacheid => 0, 142 usemd5 => 1, 143 }, 144 dns => { 145 disable => 0, 146 nolog => 0, 147 noparent => 1, 148 anylog => 0, 149 async_txt => 0, 150 timeout => 14, 151 max_timeout => 10, 152 max_interval => 1200, 153 ttl => 3600, 154 cleanup => 600, 155 mask => '^127\.', 156 max_ns_lookups => 100, 157 max_mx_lookups => 100, 158 ipv6_dnsbl => 0, 159 }, 160 rate => { 161 cleanup => 600, 162 noparent => 0, 163 store => undef, 164 }, 165 group => { 166 ttl => 3600, # default ttl=1h 167 maxitems => 999999, # '-1' for unlimited 168 cleanup => 600, 169 noparent => 0, 170 aggregate_addrs => 0, 171 store => undef, 172 }, 173 scores => { 174 "5.0" => "554 5.7.1 ".$NAME." score exceeded", 175 }, 176 debug => { 177 #all => 0, 178 #verbose => 0, 179 #cache => 0, 180 #rates => 0, 181 #config => 0, 182 #cache => 0, 183 #getcache => 0, 184 #setcache => 0, 185 #dns => 0, 186 #getdns => 0, 187 #setdns => 0, 188 }, 189 dumper => { 190 Indent => 2, 191 Purity => 1, 192 Quotekeys => 0, 193 Sortkeys => 1, 194 Terse => 1, 195 }, 196 name => $NAME, 197 version => $VERSION, 198 default => $DEFAULT, 199 # choose 'Multiplex' or 'PreFork' 200 personality => 'Multiplex', 201 autopersonality => 1, 202 daemon => 1, 203 chroot => undef, 204 manual => $cmd_manual, 205 pager => $cmd_pager, 206 sepreq => $sepreq, 207 seplst => $seplst, 208 seplim => $seplim, 209 summary => 600, 210 instant => 0, 211 verbose => 0, 212 test => 0, 213 keep_rates => 0, 214 keep_groups => 0, 215 aggregate_addrs => 0, 216 cidr_method => 'postfwd', 217 max_command_recursion => 64, 218 timeformat => ( ($TIMEHIRES) ? '%.2f' : '%d' ), 219); 220 221# daemon commands 222our %postfwd_commands = ( 223 ping => 'PING', 224 pong => 'PONG', 225 dumpstats => 'DS', 226 dumpcache => 'DC', 227 delcache => 'RC', 228 delrate => 'RR', 229 countcache => 'CN', 230 matchcache => 'MT', 231 setcacheitem => 'SC', 232 getcacheitem => 'GC', 233 getcacheval => 'GV', 234 setrateitem => 'SR', 235 groupadd => 'GA', 236 groupdel => 'GD', 237 groupget => 'GG', 238); 239 240# precompiled patterns 241our %postfwd_patterns = ( 242 ping => $postfwd_commands{ping}, 243 pong => $postfwd_commands{pong}, 244 keyval => qr/^([^=]+)=(.*)$/, 245 cntval => qr/^([^=]+)=(\d+)$/, 246 command => qr/^CMD\s*=/i, 247 dumpstats => qr/^CMD\s*=\s*$postfwd_commands{dumpstats}\s*;\s*$/i, 248 dumpcache => qr/^CMD\s*=\s*$postfwd_commands{dumpcache}\s*;\s*$/i, 249 delcache => qr/^CMD\s*=\s*$postfwd_commands{delcache}\s+(.*?)$/i, 250 delrate => qr/^CMD\s*=\s*$postfwd_commands{delrate}\s+(.*?)$/i, 251 countcache => qr/^CMD\s*=\s*$postfwd_commands{countcache}\s*;\s*TYPE\s*=\s*(.*?)\s*$/i, 252 matchcache => qr/^CMD\s*=\s*$postfwd_commands{matchcache}\s*;\s*TYPE\s*=\s*(.*?)\s*$/i, 253 setcacheitem => qr/^CMD\s*=\s*$postfwd_commands{setcacheitem}\s*;\s*TYPE\s*=\s*([^;]+)\s*;\s*ITEM\s*=\s*(.*?)\s*$sepreq\s*(.*?)\s*$/i, 254 getcacheitem => qr/^CMD\s*=\s*$postfwd_commands{getcacheitem}\s*;\s*TYPE\s*=\s*([^;]+)\s*;\s*ITEM\s*=\s*(.*?)\s*$/i, 255 getcacheval => qr/^CMD\s*=\s*$postfwd_commands{getcacheval}\s*;\s*TYPE\s*=\s*([^;]+)\s*;\s*ITEM\s*=\s*(.*?)\s*$sepreq\s*KEY\s*=\s*(.*?)\s*$/i, 256 setrateitem => qr/^CMD\s*=\s*$postfwd_commands{setrateitem}\s*;\s*TYPE\s*=\s*([^;]+)\s*;\s*ITEM\s*=\s*(.*?)\s*$sepreq\s*(.*?)\s*$/i, 257 groupadd => qr/^CMD\s*=\s*$postfwd_commands{groupadd}\s*;\s*TYPE\s*=\s*([^;]+)\s*;\s*ITEM\s*=\s*(.*?)\s*$seplim\s*(.*?)\s*$/i, 258 groupdel => qr/^CMD\s*=\s*$postfwd_commands{groupdel}\s*;\s*TYPE\s*=\s*([^;]+)\s*;\s*ITEM\s*=\s*(.*?)\s*$/i, 259 groupget => qr/^CMD\s*=\s*$postfwd_commands{groupget}\s*;\s*TYPE\s*=\s*(.*?)\s*$/i, 260); 261 262 263## SUBS 264 265# prints formatted timestamp 266sub ts { return sprintf ($postfwd_settings{timeformat}, $_[0]) }; 267 268# takes a list and returns a unified list, keeping given order 269sub uniq { 270 undef my %uniq; 271 return grep(!$uniq{$_}++, @_); 272}; 273 274# tests debug levels 275sub wantsdebug { 276 return unless defined $postfwd_settings{debug}; 277 foreach (@_) { return 1 if defined $postfwd_settings{debug}{$_} }; 278}; 279 280# hash -> scalar 281sub hash_to_str { 282 my $request = shift; my $result = ''; 283 map { $result .= $postfwd_settings{sepreq}."$_=".((ref $request->{$_} eq 'ARRAY') ? (join $postfwd_settings{seplst}, @{$request->{$_}}) : ($request->{$_} || '')) } (keys %{$request}); 284 return $result; 285}; 286 287# scalar -> hash 288sub str_to_hash { 289 my $request = shift; my %result = (); 290 foreach (split $postfwd_settings{sepreq}, $$request) { 291 next unless m/$postfwd_patterns{keyval}/; 292 my @items = split $postfwd_settings{seplst}, $2; 293 ($#items) ? @{$result{$1}} = @items : $result{$1} = $2; 294 }; return %result; 295}; 296 297# displays hash structure 298sub hash_to_list { 299 my ($pre, $request) = @_; my @output = (); 300 # get longest key 301 my $minkey = '-'.(length((sort {length($b) <=> length($a)} (keys %{$request}))[0] || '') + 1); 302 while ( my($s, $v) = each %{$request} ) { 303 my $r = ref $v; 304 if ($r eq 'HASH') { 305 push @output, (%{$v}) 306 ? hash_to_list ( sprintf ("%s -> %".$minkey."s", $pre, '%'.$s), \%{$v} ) 307 : sprintf ("%s -> %".$minkey."s -> %s", $pre, '%'.$s, 'undef'); 308 } elsif ($r eq 'ARRAY') { 309 push @output, sprintf ("%s -> %".$minkey."s -> %s", $pre, '@'.$s, ((@{$v}) ? "'".(join ",", @{$v})."'" : 'undef')); 310 } elsif ($r eq 'CODE') { 311 push @output, sprintf ("%s -> %".$minkey."s -> %s", $pre, '&'.$s, ((defined $v) ? "'".$v."'" : 'undef')); 312 } else { 313 push @output, sprintf ("%s -> %".$minkey."s -> %s", $pre, '$'.$s, ((defined $v) ? "'".$v."'" : 'undef')); 314 }; 315 }; 316 @output = sort { my ($c, $d) = ($a, $b); 317 $c =~ tr/$/1/; $c =~ tr/&/2/; $c =~ tr/@/3/; $c =~ tr/%/4/; 318 $d =~ tr/$/1/; $d =~ tr/&/2/; $d =~ tr/@/3/; $d =~ tr/%/4/; 319 return $c cmp $d; } @output; 320 return @output; 321}; 322 323# Sys::Syslog < 0.15 324sub mylogs_old { 325 my $prio = shift @_; 326 my $msg = shift @_; 327 eval { local $SIG{'__DIE__'}; syslog ($prio,$msg,@_) }; 328}; 329 330# Sys::Syslog >= 0.15 331sub mylogs_new { 332 my $prio = shift @_; 333 my $msg = shift @_; 334 syslog ($prio,$msg,@_); 335}; 336 337# Syslog to stdout 338sub mylogs_stdout { 339 my $prio = shift @_; 340 my $msg = shift @_; 341 $msg =~ s/\%/%%/g; $msg =~ /^(.*)$/; 342 printf STDERR "[".$postfwd_settings{name}."][$$][LOG $prio]: $1\n", @_; 343}; 344 345# send log message 346sub mylogs { 347 my $prio = shift @_; 348 my $msg = shift @_; 349 return if $postfwd_settings{syslog}{nolog}; 350 # escape unsafe characters 351 $msg =~ s/$postfwd_settings{syslog}{unsafe_charset}/?/g; 352 $msg =~ s/\%/%%/g; 353 &{$postfwd_settings{syslog}{logger}} ($prio,$msg,@_); 354}; 355 356# short versions 357sub log_info { mylogs ('info', @_) }; 358sub log_note { mylogs ('notice', @_) }; 359sub log_warn { mylogs ('warning', @_) }; 360sub log_err { mylogs ('err', @_) }; 361sub log_crit { mylogs ('crit', @_) }; 362 363# init logging 364sub init_log { 365 my($logname) = @_; 366 $postfwd_settings{syslog}{name} = $logname if $logname; 367 # Some module versions contain non-digits (example: Sys::Syslog::VERSION 0.33_01) 368 my $sver = $Sys::Syslog::VERSION; 369 my $nver = $Net::Server::VERSION; 370 $sver =~ s/[^0-9\.]//g if defined $sver; 371 $nver =~ s/[^0-9\.]//g if defined $nver; 372 $postfwd_settings{syslog}{unsafe_version} = ( (not((defined $sver) or (defined $nver))) or ($sver lt '0.15') or ($nver lt '0.94') ); 373 if ($postfwd_settings{syslog}{stdout}) { 374 $postfwd_settings{syslog}{logger} = \&mylogs_stdout; 375 $postfwd_settings{syslog}{socktype} = 'console'; 376 $postfwd_settings{syslog}{options} = 'cons,pid'; 377 $postfwd_settings{base}{log_file} = undef; 378 } else { 379 $postfwd_settings{syslog}{socktype} = ( $postfwd_settings{syslog}{socktype} || (($postfwd_settings{syslog}{unsafe_version}) ? (($nounixsock) ? 'inet' : 'unix') : 'native') ); 380 $postfwd_settings{syslog}{logger} = ($postfwd_settings{syslog}{unsafe_version}) ? \&mylogs_old : \&mylogs_new; 381 }; 382 setlogsock $postfwd_settings{syslog}{socktype}; 383 openlog $postfwd_settings{syslog}{name}, $postfwd_settings{syslog}{options}, $postfwd_settings{syslog}{facility}; 384 log_info ("set up syslogging Sys::Syslog".((defined $Sys::Syslog::VERSION) ? " version $Sys::Syslog::VERSION" : '').' ['.($postfwd_settings{syslog}{unsafe_version} || 0).']' ) if wantsdebug (qw[ all verbose ]); 385}; 386 387# check: INET 388sub check_inet { 389 my ($type,$send) = @_; 390 if ( my $socket = new IO::Socket::INET ( 391 PeerAddr => $postfwd_settings{$type}{host}, 392 PeerPort => $postfwd_settings{$type}{port}, 393 Proto => 'tcp', 394 Timeout => $postfwd_settings{timeout}{$type}, 395 Type => SOCK_STREAM ) ) { 396 $socket->print("$send\n"); 397 $send = $socket->getline(); 398 $socket->close(); 399 chomp($send) if $send; 400 } else { 401 warn("can not open socket to $postfwd_settings{$type}{host}:$postfwd_settings{$type}{port}: '$!' '$@'\n") unless $terminating; 402 undef $send; 403 }; 404 return $send || ''; 405}; 406 407# check: UNIX 408sub check_unix { 409 my ($type,$send) = @_; 410 if ( my $socket = new IO::Socket::UNIX ( 411 Peer => $postfwd_settings{$type}{port}, 412 Timeout => $postfwd_settings{timeout}{$type}, 413 Type => SOCK_STREAM ) ) { 414 $socket->print("$send\n"); 415 $send = $socket->getline(); 416 $socket->close(); 417 chomp($send) if $send; 418 } else { 419 warn("can not open socket to $postfwd_settings{$type}{host}:$postfwd_settings{$type}{port}: '$!' '$@'\n") unless $terminating; 420 undef $send; 421 }; 422 return $send || ''; 423}; 424 425# saves hash to disk 426sub save_hash { 427 my($ref,$nam,$fil) = @_; 428 return unless ($STORABLE and $fil and defined $ref and scalar keys %{$ref}); 429 eval { 430 local $SIG{__DIE__} = sub { warn("can not store $nam cache to $fil: '$!' '$@'") }; 431 nstore ($ref, $fil); 432 }; 433 log_info ("Saved ".(scalar %{$ref})." $nam items to $fil") unless $@; 434}; 435 436# loads hash from disk 437sub load_hash { 438 my($ref,$nam,$fil) = @_; 439 return unless ($STORABLE and $fil and (-f $fil)); 440 eval { 441 local $SIG{__DIE__} = sub { log_note ("can not load $nam cache from $fil: '$!' '$@'") }; 442 %{$ref} = %{retrieve($fil)}; 443 }; 444 log_info ("Fetched ".(scalar %{$ref})." $nam items from $fil") if ( not($@) and $ref ); 445}; 446 4471; # EOF postfwd3::basic 448 449 450############################ 451package postfwd3::cache; 452 453 454## MODULES 455use warnings; 456use strict; 457use base 'Net::Server::Multiplex'; 458import postfwd3::basic qw(:DEFAULT &wantsdebug &hash_to_list &hash_to_str &ts &load_hash &save_hash $TIMEHIRES $NETADDR $DIGESTMD5); 459use vars qw( %Cache %Groups %Cleanup %Count %Interval %Top $Reload_Conf $Summary $StartTime ); 460BEGIN { 461 # use Time::HiRes if available 462 Time::HiRes->import( qw(time) ) if $TIMEHIRES; 463}; 464 465 466## SUBS 467 468# prepare stats 469sub list_stats { 470 my @output = (); my $line = ''; my $now = time(); 471 my $uptime = $now - $StartTime; 472 return @output unless $uptime and (%Count or %Cache); 473 return @output if $postfwd_settings{syslog}{noidlestats} 474 and ((($Interval{request_set} || 0) + ($Interval{request_get} || 0)) <= 0) 475 and ((($Interval{rate_set} || 0) + ($Interval{rate_get} || 0)) <= 0) 476 and ((($Interval{dns_set} || 0) + ($Interval{dns_get} || 0)) <= 0); 477 push ( @output, sprintf ( 478 "[STATS] %s::cache %s: %d queries since %d days, %02d:%02d:%02d hours", 479 $postfwd_settings{name}, 480 $postfwd_settings{version}, 481 ($Count{cache_queries} || 0), 482 ($uptime / 60 / 60 / 24), 483 (($uptime / 60 / 60) % 24), 484 (($uptime / 60) % 60), 485 ($uptime % 60) 486 ) ); 487 my $lastreq = (($now - $Summary) > 0) ? (($Interval{request_set} || 0) + ($Interval{request_get} || 0)) / ($now - $Summary) * 60 : 0; 488 $Top{request} = $lastreq if ($lastreq > ($Top{request} || 0)); $Top{request} ||= 0; 489 my $lastdns = (($now - $Summary) > 0) ? (($Interval{dns_set} || 0) + ($Interval{dns_get} || 0)) / ($now - $Summary) * 60 : 0; 490 $Top{dns} = $lastdns if ($lastdns > ($Top{dns} || 0)); $Top{dns} ||= 0; 491 push ( @output, sprintf ( 492 "[STATS] Requests: %.1f/min last, %.1f/min overall, %.1f/min top", 493 $lastreq, 494 (($Count{request_set} || 0) + ($Count{request_get} || 0)) / $uptime * 60, 495 $Top{request} 496 ) ); 497 push ( @output, sprintf ( 498 "[STATS] Dnsstats: %.1f/min last, %.1f/min overall, %.1f/min top", 499 $lastdns, 500 (($Count{dns_set} || 0) + ($Count{dns_get} || 0)) / $uptime * 60, 501 $Top{dns} 502 ) ) unless ($postfwd_settings{dns}{disable} or $postfwd_settings{dns}{noparent}); 503 push ( @output, sprintf ( 504 "[STATS] Hitrates: %.1f%% requests, %.1f%% dns, %.1f%% rates", 505 ($Count{request_get}) ? ($Count{request_hits} || 0) / $Count{request_get} * 100 : 0, 506 ($Count{dns_get}) ? ($Count{dns_hits} || 0) / $Count{dns_get} * 100 : 0, 507 ($Count{rate_get}) ? ($Count{rate_hits} || 0) / $Count{rate_get} * 100 : 0 508 ) ); 509 push ( @output, "[STATS] Contents: ". 510 join ', ', map { $_ = "$_=".(scalar keys %{$Cache{$_}}) } (reverse sort keys %Cache) 511 ); 512 513 my $grpcnt = (scalar keys %Groups || 0); 514 if ( $grpcnt ) { 515 my $grpitems = 0; 516 map { $grpitems += (scalar keys %{$Groups{$_}} || 0) } (sort keys %Groups); 517 push ( @output, "[STATS] $grpcnt Groups with $grpitems item".(($grpitems > 1) ? 's' : '')." overall" ); 518 }; 519 520 if (wantsdebug (qw[ all stats devel parent_cache ])) { 521 push ( @output, "[STATS] Counters: ". 522 join ', ', map { $_ = "$_=".$Count{$_} } (reverse sort keys %Count) ); 523 push ( @output, "[STATS] Interval: ". 524 join ', ', map { $_ = "$_=".$Interval{$_} } (reverse sort keys %Interval) ); 525 }; 526 map { $Interval{$_} = 0 } (keys %Interval); 527 $Summary = $now; 528 return @output; 529}; 530 531# return cache contents 532sub dump_cache { 533 my @result = (); 534 foreach (sort keys %Cache) { 535 push @result, hash_to_list ('%'.$_."_cache", $Cache{$_}) if %{$Cache{$_}}; 536 }; 537 push @result, hash_to_list ('%Group_cache', \%Groups) if %Groups; 538 return @result; 539}; 540 541# get a whole cache item 542sub get_cache { 543 my ($self,$now,$type,$item) = @_; 544 my @answer = (); 545 return '<undef>' unless ( defined $Cache{$type}{$item}{'until'} and ($now <= $Cache{$type}{$item}{'until'}[0])); 546 $Count{$type."_hits"}++; 547 map { push @answer, "$_=".(join $postfwd_settings{seplst}, @{$Cache{$type}{$item}{$_}}) } (keys %{$Cache{$type}{$item}}); 548 return (join $postfwd_settings{sepreq}, @answer); 549}; 550 551# set item to cache 552sub set_cache { 553 my ($self,$type,$item,$vals) = @_; 554 my @answer = (); 555 undef $Cache{$type}{$item}; 556 foreach my $arg (split ($postfwd_settings{sepreq}, $vals)) { 557 map { push @{$Cache{$type}{$item}{$1}}, $_; 558 push @answer, "$type->$item->$1=$_"; 559 @{$Cache{$type}{$item}{$1}} = uniq(@{$Cache{$type}{$item}{$1}}); 560 } (split $postfwd_settings{seplst}, $2) if ($arg =~ m/$postfwd_patterns{keyval}/); 561 }; 562 @answer = '<undef>' unless @answer; 563 return (join '; ', @answer); 564}; 565 566# add item to group 567sub add_group { 568 my ($self,$now,$groupname,$groupitem,$groupttl) = @_; 569 my $answer = 'no'; 570 unless ( (defined $Groups{$groupname}{$groupitem} and $Groups{$groupname}{$groupitem} > $now) 571 or (scalar keys %{$Groups{$groupname}} >= $postfwd_settings{group}{maxitems}) 572 or ($postfwd_settings{group}{maxitems} < 0) 573 ) { 574 $Groups{$groupname}{$groupitem} = $groupttl; 575 $Count{"group_hits"}++; 576 $answer = 'ok'; 577 }; 578 return $answer; 579}; 580 581# remove item from group 582sub del_group { 583 my ($self,$now,$groupname,$groupitem) = @_; 584 my $answer = 'no'; 585 if (defined $Groups{$groupname}{$groupitem}) { 586 delete $Groups{$groupname}{$groupitem}; 587 $Count{"group_hits"}++; 588 $answer = 'ok'; 589 }; 590 return $answer; 591}; 592 593# get items from group 594sub get_group { 595 my ($self,$now,$groupname) = @_; 596 my @answer = (); 597 if (defined $Groups{$groupname} and scalar keys %{$Groups{$groupname}}) { 598 map { push @answer, $_ if $Groups{$groupname}{$_} > $now } (keys %{$Groups{$groupname}}); 599 $Count{"group_hits"}++; 600 }; 601 return (join $postfwd_settings{sepreq}, @answer); 602}; 603 604# set rate to cache 605sub set_rate { 606 my ($self,$now,$type,$item,$vals) = @_; 607 my $rindex = ''; my %entry = (); my $rcount = undef; 608 ($item, $rindex) = split $postfwd_settings{seplim}, $item; 609 push @{$Cache{$type}{$item}{'list'}}, $rindex; 610 @{$Cache{$type}{$item}{'list'}} = uniq(@{$Cache{$type}{$item}{'list'}}); 611 foreach my $arg (split ($postfwd_settings{sepreq}, $vals)) { 612 map { 613 #push @{$entry{$1}}, $_; 614 #@{$entry{$1}} = uniq(@{$entry{$1}}); 615 $entry{$1} = $_; 616 } (split $postfwd_settings{seplst}, $2) if ($arg =~ m/$postfwd_patterns{keyval}/); 617 }; 618 unless (defined $Cache{$type}{$item}{$rindex} and defined $Cache{$type}{$item}{$rindex}{'until'}) { 619 %{$Cache{$type}{$item}{$rindex}} = %entry; 620 #} elsif ($now > $Cache{$type}{$item}{$rindex}{'until'}[0]) { 621 } elsif ($now > $Cache{$type}{$item}{$rindex}{'until'}) { 622 %{$Cache{$type}{$item}{$rindex}} = %entry; 623 } else { 624 #$rcount = $Cache{$type}{$item}{$rindex}{'count'}[0] + ($entry{'count'}[0] || 0); 625 #$Cache{$type}{$item}{$rindex}{'count'}[0] = $rcount; 626 $rcount = $Cache{$type}{$item}{$rindex}{'count'} + ($entry{'count'} || 0); 627 $Cache{$type}{$item}{$rindex}{'count'} = $rcount; 628 }; 629 #return $rcount || $Cache{$type}{$item}{$rindex}{'count'}[0] || '<undef>'; 630 return $rcount || $Cache{$type}{$item}{$rindex}{'count'} || '<undef>'; 631}; 632 633# clean up cache 634sub cleanup_cache { 635 my($type,$now) = @_; 636 my $start = $Cleanup{$type} = time(); 637 log_info ("[CLEANUP] checking $type cache...") if wantsdebug (qw[ all cleanup parentcleanup ]); 638 return unless defined $Cache{$type} and my $count = scalar keys %{$Cache{$type}}; 639 CLEANUP: foreach my $checkitem (keys %{$Cache{$type}}) { 640 next CLEANUP unless (defined $Cache{$type}{$checkitem}); 641 unless ( defined $Cache{$type}{$checkitem}{'list'} ) { 642 # remove incomplete objects 643 unless ( defined $Cache{$type}{$checkitem}{'until'} and defined $Cache{$type}{$checkitem}{ttl} ) { 644 if ( wantsdebug (qw[ all cleanup parentcleanup devel ]) ) { 645 log_info ("[CLEANUP] deleting incomplete $type cache item '$checkitem'"); 646 map { log_info ("[CLEANUP] $_") } ( hash_to_list($Cache{$type}{$checkitem}) ); 647 }; 648 delete $Cache{$type}{$checkitem}; 649 # remove timed out objects 650 } elsif ( $now > $Cache{$type}{$checkitem}{'until'}[0] ) { 651 log_info ("[CLEANUP] removing $type cache item '$checkitem' after ttl ".$Cache{$type}{$checkitem}{ttl}[0]."s") 652 if wantsdebug (qw[ all cleanup parentcleanup ]); 653 delete $Cache{$type}{$checkitem}; 654 }; 655 } else { 656 my @i = (); 657 foreach my $crate (@{$Cache{$type}{$checkitem}{'list'}}) { 658 unless ( defined $Cache{$type}{$checkitem}{$crate}{'until'} and defined $Cache{$type}{$checkitem}{$crate}{ttl} ) { 659 if ( wantsdebug (qw[ all cleanup parentcleanup devel ]) ) { 660 log_info ("[CLEANUP] deleting incomplete $type cache item '$checkitem'->'$crate'"); 661 map { log_info ("[CLEANUP] $_") } ( hash_to_list($Cache{$type}{$checkitem}{$crate}) ); 662 }; 663 delete $Cache{$type}{$checkitem}{$crate}; 664 #} elsif ( $now > $Cache{$type}{$checkitem}{$crate}{'until'}[0] ) { 665 } elsif ( $now > $Cache{$type}{$checkitem}{$crate}{'until'} ) { 666 #log_info ("[CLEANUP] removing $type cache item '$checkitem'->'$crate' after ttl ".$Cache{$type}{$checkitem}{$crate}{ttl}[0]."s") 667 log_info ("[CLEANUP] removing $type cache item '$checkitem'->'$crate' after ttl ".$Cache{$type}{$checkitem}{$crate}{ttl}."s") 668 if wantsdebug (qw[ all cleanup parentcleanup ]); 669 delete $Cache{$type}{$checkitem}{$crate}; 670 } else { 671 push @i, $crate; 672 }; 673 }; 674 unless ($i[0]) { 675 log_info ("[CLEANUP] removing $type cache complete item '$checkitem'") 676 if wantsdebug (qw[ all cleanup parentcleanup ]); 677 delete $Cache{$type}{$checkitem}; 678 } else { 679 log_info ("[CLEANUP] new $type cache limits for item '$checkitem': ".(join ', ', @i)) 680 if wantsdebug (qw[ all cleanup parentcleanup ]); 681 @{$Cache{$type}{$checkitem}{'list'}} = @i; 682 }; 683 }; 684 }; 685 my $end = time(); 686 log_info ("[CLEANUP] cleaning $type cache needed ".ts($end - $start)." seconds for " 687 .($count - scalar keys %{$Cache{$type}})." out of ".$count 688 ." cached items after cleanup time ".$postfwd_settings{$type}{cleanup}."s") 689 if ( wantsdebug (qw[ all verbose cleanup parentcleanup ]) or (($end - $start) >= 1) ); 690}; 691 692# clean up group cache 693sub cleanup_groups { 694 my($now) = $_[0]; return unless $now; 695 my $start = $Cleanup{group} = time(); 696 map { log_info("[CLEANUP] PRE: Group: '$_' -> ".(scalar keys %{$Groups{$_}})." items") } (sort keys %Groups) if wantsdebug (qw[ all cleanup parentcleanup groups ]); 697 foreach my $groupname (keys %Groups) { 698 foreach my $groupitem (keys %{$Groups{$groupname}}) { 699 unless ( $Groups{$groupname}{$groupitem} ) { 700 log_info ("[CLEANUP] deleting incomplete group-cache item '$groupitem' from group '$groupname'") 701 if wantsdebug (qw[ all cleanup parentcleanup devel groups ]); 702 delete $Groups{$groupname}{$groupitem}; 703 } elsif ($now > $Groups{$groupname}{$groupitem}) { 704 log_info ("[CLEANUP] removing expired group-cache item '$groupitem' from group '$groupname'") 705 if wantsdebug (qw[ all cleanup parentcleanup groups ]); 706 delete $Groups{$groupname}{$groupitem}; 707 }; 708 }; 709 unless ( %{$Groups{$groupname}} ) { 710 log_info ("[CLEANUP] removing empty group '$groupname'") 711 if wantsdebug (qw[ all cleanup parentcleanup groups ]); 712 delete $Groups{$groupname}; 713 }; 714 }; 715 map { log_info("[CLEANUP] POST: Group: '$_' -> ".(scalar keys %{$Groups{$_}})." items") } (sort keys %Groups) if wantsdebug (qw[ all cleanup parentcleanup groups ]); 716 my $end = time(); 717 log_info ("[CLEANUP] cleaning group cache needed ".ts($end - $start)." seconds") 718 if ( wantsdebug (qw[ all verbose cleanup parentcleanup ]) or (($end - $start) >= 1) ); 719}; 720 721# saves rate limits to disk 722sub save_rates { 723 cleanup_cache ('rate', time()); 724 save_hash ($Cache{rate}, 'rate', $postfwd_settings{rate}{store}); 725}; 726 727# loads rate limits from disk 728sub load_rates { 729 my %lr = (); 730 load_hash (\%lr, 'rate', $postfwd_settings{rate}{store}); 731 if (scalar keys %lr) { 732 %{$Cache{rate}} = %lr; 733 cleanup_cache ('rate', time()); 734 }; 735}; 736 737# saves group cache to disk 738sub save_groups { 739 cleanup_groups (time()); 740 save_hash (\%Groups, 'group', $postfwd_settings{group}{store}); 741}; 742 743# loads group cache from disk 744sub load_groups { 745 load_hash (\%Groups, 'group', $postfwd_settings{group}{store}); 746 cleanup_groups (time()) if scalar keys %Groups; 747}; 748 749sub set_personality {}; 750 751 752## Net::Server::Multiplex methods 753 754# ignore syslog failures 755sub handle_syslog_error {}; 756 757# set $Reload_Conf marker on HUP signal 758sub sig_hup { 759 log_note ("catched HUP signal - clearing request cache on next request"); 760 $Reload_Conf = 1; 761}; 762 763# cache start 764sub pre_loop_hook() { 765 my $self = shift; 766 # change cache name 767 $0 = $self->{server}->{commandline} = " ".$postfwd_settings{name}.'::cache'; 768 $postfwd_settings{name} .= "/cache"; 769 load_rates(); 770 load_groups(); 771 $StartTime = $Summary = $Cleanup{request} = $Cleanup{rate} = $Cleanup{dns} = $Cleanup{group} = time(); 772 log_info ("ready for input"); 773}; 774 775# cache end 776sub post_child_cleanup_hook { 777 save_rates(); 778 save_groups(); 779}; 780 781# cache process request 782sub mux_input { 783 my ($self, $mux, $client, $mydata) = @_; 784 my $action = '<undef>'; 785 my $now = time(); 786 while ( $$mydata =~ s/^([^\r\n]*)\r?\n// ) { 787 # check request line 788 next unless defined $1; 789 my $request = $1; 790 log_info ("request: '$request'") if wantsdebug (qw[ all ]); 791 if ($Reload_Conf) { 792 undef $Reload_Conf; my $s = ''; delete $Cache{request}; 793 unless ($postfwd_settings{keep_rates}) { delete $Cache{rate}; $s = ' and rate' }; 794 unless ($postfwd_settings{keep_groups}) { %Groups = (); $s = ' and group' }; 795 log_info ("request".(($s) ? "$s" : '')." cache cleared") if wantsdebug (qw[ all verbose ]); 796 }; 797 if ($request eq $postfwd_patterns{ping}) { 798 $action = $postfwd_patterns{pong}; 799 } elsif ($request =~ m/$postfwd_patterns{groupget}/) { 800 my ($type) = ($1); 801 log_info ("[GETGROUP] request: '$request'") if wantsdebug (qw[ all cache setcache groups ]); 802 cleanup_groups ($now) if (($now - $Cleanup{group}) > ($postfwd_settings{group}{cleanup} || 300)); 803 $Count{cache_queries}++; $Interval{cache_queries}++; 804 $Count{"group_get"}++; $Interval{"group_get"}++; 805 $action = $self->get_group($now,$type); 806 log_info ("[GETGROUP] answer: '$action'") if wantsdebug (qw[ all cache getcache groups ]); 807 } elsif ($request =~ m/$postfwd_patterns{getcacheitem}/) { 808 my ($type, $item) = ($1, $2); 809 log_info ("[GETCACHEITEM] request: '$request'") if wantsdebug (qw[ all cache getcache ]); 810 cleanup_cache ($type,$now) if (($now - $Cleanup{$type}) > ($postfwd_settings{$type}{cleanup} || 300)); 811 $Count{cache_queries}++; $Interval{cache_queries}++; 812 $Count{$type."_get"}++; $Interval{$type."_get"}++; 813 $action = $self->get_cache($now,$type,$item); 814 log_info ("[GETCACHEITEM] answer: '$action'") if wantsdebug (qw[ all cache getcache ]); 815 } elsif ($request =~ m/$postfwd_patterns{setcacheitem}/) { 816 my ($type, $item, $vals) = ($1, $2, $3); 817 log_info ("[SETCACHEITEM] request: '$request'") if wantsdebug (qw[ all cache setcache ]); 818 $Count{cache_queries}++; $Interval{cache_queries}++; 819 $Count{$type."_set"}++; $Interval{$type."_set"}++; 820 $action = $self->set_cache($type,$item,$vals); 821 log_info ("[SETCACHEITEM] answer: '$action'") if wantsdebug (qw[ all cache setcache ]); 822 } elsif ($request =~ m/$postfwd_patterns{setrateitem}/) { 823 my ($type, $item, $vals) = ($1, $2, $3); 824 log_info ("[SETRATEITEM] request: '$request'") if wantsdebug (qw[ all cache setcache ]); 825 $Count{cache_queries}++; $Interval{cache_queries}++; 826 $Count{$type."_set"}++; $Interval{$type."_set"}++; 827 $action = $self->set_rate($now,$type,$item,$vals); 828 log_info ("[SETRATEITEM] answer: '$action'") if wantsdebug (qw[ all cache setcache ]); 829 } elsif ($request =~ m/$postfwd_patterns{groupadd}/) { 830 my ($type, $item, $vals) = ($1, $2, $3); 831 log_info ("[ADDGROUP] request: '$request'") if wantsdebug (qw[ all cache setcache groups ]); 832 $Count{cache_queries}++; $Interval{cache_queries}++; 833 $Count{"group_add"}++; $Interval{"group_add"}++; 834 $action = $self->add_group($now,$type,$item,$vals); 835 log_info ("[ADDGROUP] answer: '$action'") if wantsdebug (qw[ all cache setcache groups ]); 836 } elsif ($request =~ m/$postfwd_patterns{groupdel}/) { 837 my ($type, $item) = ($1, $2); 838 log_info ("[DELGROUP] request: '$request'") if wantsdebug (qw[ all cache setcache groups ]); 839 $Count{cache_queries}++; $Interval{cache_queries}++; 840 $Count{"group_del"}++; $Interval{"group_del"}++; 841 $action = $self->del_group($now,$type,$item); 842 log_info ("[DELGROUP] answer: '$action'") if wantsdebug (qw[ all cache setcache groups ]); 843 } elsif ($request =~ m/$postfwd_patterns{dumpstats}/) { 844 $action = join $postfwd_settings{sepreq}.$postfwd_settings{seplst}, list_stats(); 845 } elsif ($request =~ m/$postfwd_patterns{dumpcache}/) { 846 $action = join $postfwd_settings{sepreq}.$postfwd_settings{seplst}, dump_cache(); 847 } elsif ($request =~ m/$postfwd_patterns{delcache}/) { 848 my $del = $1; $del =~ s/^[%]?//; 849 if (defined $Cache{'request'}{$del}) { 850 delete $Cache{'request'}{$del}; 851 log_info ("[DELCACHEITEM] request cache item '$del' removed"); 852 $action = "request cache item '$del' removed"; 853 } else { 854 log_info ("[DELCACHEITEM] request cache removal of '$del' failed: item not found"); 855 $action = "request cache removal of '$del' failed: item not found"; 856 }; 857 } elsif ($request =~ m/$postfwd_patterns{delrate}/) { 858 my $del = $1; $del =~ s/^[%]?//; 859 if (defined $Cache{'rate'}{$del}) { 860 delete $Cache{'rate'}{$del}; 861 log_info ("[DELRATEITEM] rate cache item '$del' removed"); 862 $action = "rate cache item '$del' removed"; 863 } else { 864 log_info ("[DELRATEITEM] rate cache removal of '$del' failed: item not found"); 865 $action = "rate cache removal of '$del' failed: item not found"; 866 }; 867 } else { 868 log_note ("warning: ignoring unknown command '".substr($request,0,512)."'"); 869 }; 870 print $client "$action\n"; 871 log_info ("answer: '$action'") if wantsdebug (qw[ all ]); 872 }; 873}; 874 8751; # EOF postfwd3::cache 876 877 878############################ 879package postfwd3::server; 880 881use warnings; 882use strict; 883use IO::Socket qw(SOCK_STREAM); 884use Net::DNS; 885use Net::Server::PreFork; 886use Net::Server::Multiplex; 887 888import postfwd3::basic qw(:DEFAULT %postfwd_commands &check_inet &check_unix &wantsdebug &hash_to_str &str_to_hash &hash_to_list &ts &load_hash &save_hash $TIMEHIRES $NETADDR $DIGESTMD5); 889# export these functions for '-C' switch 890use Exporter qw(import); 891our @EXPORT_OK = qw( 892 &read_config &show_config &process_input &get_plugins 893); 894our @ISA = (); 895BEGIN { 896 # use Time::HiRes if available 897 Time::HiRes->import( qw(time) ) if $TIMEHIRES; 898 # use NetAddr::IP if available 899 NetAddr::IP->import( qw(Compact) ) if $NETADDR; 900 # use Digest::MD5 if available 901 Digest::MD5->import( qw(md5_hex md5_base64) ) if $DIGESTMD5; 902}; 903 904 905 906# these items have to be compared as... 907# scoring 908my $COMP_SCORES = "score"; 909my $COMP_NS_NAME = "sender_ns_names"; 910my $COMP_NS_ADDR = "sender_ns_addrs"; 911my $COMP_MX_NAME = "sender_mx_names"; 912my $COMP_MX_ADDR = "sender_mx_addrs"; 913my $COMP_HELO_ADDR = "helo_address"; 914# networks in CIDR notation (a.b.c.d/nn) 915my $COMP_NETWORK_CIDRS = "(client_address|sender_(ns|mx)_addrs|helo_address)"; 916# RBL checks 917my $COMP_DNSBL_TEXT = "dnsbltext"; 918my $COMP_RBL_CNT = "rblcount"; 919my $COMP_RHSBL_CNT = "rhsblcount"; 920my $COMP_RBL_KEY = "rbl"; 921my $COMP_RHSBL_KEY = "rhsbl"; 922my $COMP_RHSBL_KEY_CLIENT = "rhsbl_client"; 923my $COMP_RHSBL_KEY_SENDER = "rhsbl_sender"; 924my $COMP_RHSBL_KEY_RCLIENT = "rhsbl_reverse_client"; 925my $COMP_RHSBL_KEY_HELO = "rhsbl_helo"; 926my %DNSBLITEMS = ( 927 rbl => { 928 cnt => "rblcount", 929 }, 930 rhsbl => { 931 cnt => "rhsblcount", 932 }, 933 rhsbl_client => { 934 cnt => "rhsblcount", 935 }, 936 rhsbl_sender => { 937 cnt => "rhsblcount", 938 }, 939 rhsbl_reverse_client => { 940 cnt => "rhsblcount", 941 }, 942 rhsbl_helo => { 943 cnt => "rhsblcount", 944 }, 945); 946# dns key value matching 947my %DNS_REPNAMES = ( 948 "NS" => "nsdname", 949 "MX" => "exchange", 950 "A" => "address", 951 "TXT" => "char_str_list", 952 "CNAME" => "cname", 953); 954 955# file items 956our($COMP_CONF_FILE) = 'cfile|file'; 957our($COMP_CONF_TABLE) = 'ctable|table'; 958our($COMP_LIVE_FILE) = 'lfile'; 959our($COMP_LIVE_TABLE) = 'ltable'; 960our($COMP_TABLES) = qr/^($COMP_CONF_TABLE|$COMP_LIVE_TABLE)$/i; 961our($COMP_CONF_FILE_TABLE) = qr/^($COMP_CONF_FILE|$COMP_CONF_TABLE):(.+)$/i; 962our($COMP_LIVE_FILE_TABLE) = qr/^($COMP_LIVE_FILE|$COMP_LIVE_TABLE):(.+)$/i; 963our($COMP_ADDRESS_ITEM) = qr/^[;=<>~]+([0-9a-fA-F_:\/\.\[\]\-]+)$/; 964# date checks 965my $COMP_DATE = "date"; 966my $COMP_TIME = "time"; 967my $COMP_DAYS = "days"; 968my $COMP_MONTHS = "months"; 969# always true 970my $COMP_ACTION = "action"; 971my $COMP_ACTION_MATCH = qr/^(\w[\-\w]+)\s*\(\s*(.*?)\s*\)$/; 972my $COMP_ID = "id"; 973my $COMP_CACHE = "cache"; 974# rule hits 975my $COMP_HITS = "request_hits"; 976# item match counter 977my $COMP_MATCHES = "matches"; 978# separator 979my $COMP_SEPARATOR = "[=\~\<\>]=|[\<\>]|[=\!][=\~\<\>]|="; 980# macros 981my $COMP_ACL = "[\&][\&]"; 982# negation 983my $COMP_NEG = "[\!][\!]"; 984my $COMP_DENEG = qr/^$COMP_NEG\s*\(?\s*(.+?)\s*\)?$/; 985# variables 986my $COMP_VAR = "[\$][\$]"; 987my $COMP_DEVAR1 = qr/(.*)$COMP_VAR\s*(\w+)(.*)/; 988my $COMP_DEVAR2 = qr/(.*)$COMP_VAR\s*\((\w+)\)(.*)/; 989# groups 990my $COMP_GROUP = "[\%][\%]"; 991# date calculations 992my $COMP_DATECALC = "($COMP_DATE|$COMP_TIME|$COMP_DAYS|$COMP_MONTHS)"; 993# these items allow whitespace-or-comma-separated values 994my $COMP_CSV = "($COMP_NETWORK_CIDRS|$COMP_RBL_KEY|$COMP_RHSBL_KEY|$COMP_RHSBL_KEY_CLIENT|$COMP_RHSBL_KEY_HELO|$COMP_RHSBL_KEY_SENDER|$COMP_RHSBL_KEY_RCLIENT|$COMP_DATECALC)"; 995# dont treat these as lists 996my $COMP_SINGLE = "($COMP_ID|$COMP_CACHE|$COMP_SCORES|$COMP_RBL_CNT|$COMP_RHSBL_CNT)"; 997 998# date tools 999my %months = ( 1000 "Jan" => 0, "jan" => 0, "JAN" => 0, 1001 "Feb" => 1, "feb" => 1, "FEB" => 1, 1002 "Mar" => 2, "mar" => 2, "MAR" => 2, 1003 "Apr" => 3, "apr" => 3, "APR" => 3, 1004 "May" => 4, "may" => 4, "MAY" => 4, 1005 "Jun" => 5, "jun" => 5, "JUN" => 5, 1006 "Jul" => 6, "jul" => 6, "JUL" => 6, 1007 "Aug" => 7, "aug" => 7, "AUG" => 7, 1008 "Sep" => 8, "sep" => 8, "SEP" => 8, 1009 "Oct" => 9, "oct" => 9, "OCT" => 9, 1010 "Nov" => 10, "nov" => 10, "NOV" => 10, 1011 "Dec" => 11, "dec" => 11, "DEC" => 11, 1012); 1013my %weekdays = ( 1014 "Sun" => 0, "sun" => 0, "SUN" => 0, 1015 "Mon" => 1, "mon" => 1, "MON" => 1, 1016 "Tue" => 2, "tue" => 2, "TUE" => 2, 1017 "Wed" => 3, "wed" => 3, "WED" => 3, 1018 "Thu" => 4, "thu" => 4, "THU" => 4, 1019 "Fri" => 5, "fri" => 5, "FRI" => 5, 1020 "Sat" => 6, "sat" => 6, "SAT" => 6, 1021); 1022 1023use vars qw( 1024 @Rules @DNSBL_Text @Rate_Items 1025 %Rule_by_ID %Matches %ACLs %Timeouts %Hits %Count 1026 %postfwd_items %postfwd_compare %postfwd_actions 1027 %postfwd_items_plugin %postfwd_compare_plugin %postfwd_actions_plugin 1028 %Request_Cache %AutoCacheID %Config_Cache %DNS_Cache %Rate_Cache %Group_Cache 1029 $Cleanup_Requests $Cleanup_RBLs $Cleanup_Rates $Cleanup_Timeouts $Cleanup_Groups 1030 %Cache %Cleanup $StartTime $Summary 1031); 1032 1033 1034## SUBS 1035 1036# cache query 1037sub cache_query { return ( &{$postfwd_settings{cache}{check}}('cache',@_) || '<undef>' ) }; 1038 1039# get ip and mask 1040sub cidr_parse { 1041 return unless defined $_[0]; 1042 return unless $_[0] =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/; 1043 return unless ($1 < 256 and $2 < 256 and $3 < 256 and $4 < 256 and $5 <= 32 and $5 >= 0); 1044 my $net = ($1<<24)+($2<<16)+($3<<8)+$4; 1045 my $mask = ~((1<<(32-$5))-1); 1046 return ($net & $mask, $mask); 1047}; 1048 1049# compare address to network 1050sub cidr_match { 1051 my ($net, $mask, $addr) = @_; 1052 return unless defined $net and defined $addr; 1053 $addr = ($1<<24)+($2<<16)+($3<<8)+$4 if ($addr =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/); 1054 return ($addr & $mask) == $net; 1055}; 1056 1057# sets an action for a score 1058sub modify_score { 1059 my($myscore,$myaction) = @_; 1060 log_info ( ((defined $postfwd_settings{scores}{$myscore}) ? "redefined" : "setting new") 1061 ." score $myscore with action=\"$myaction\"") if wantsdebug (qw[ all thisrequest verbose ]); 1062 $postfwd_settings{scores}{$myscore} = $myaction; 1063}; 1064 1065# returns content of !!() negation 1066sub deneg_item { 1067 my($val) = (defined $_[0]) ? $_[0] : ''; 1068 return ( ($val =~ /$COMP_DENEG/) ? $1 : '' ); 1069}; 1070 1071# resolves $$() variables 1072sub devar_item { 1073 my($cmp,$val,$myitem,$request) = @_; 1074 return '' unless $val and $myitem; 1075 my($pre,$post,$var,$myresult) = ''; 1076 while ( ($val =~ /$COMP_DEVAR1/g) or ($val =~ /$COMP_DEVAR2/g) ) { 1077 ($pre,$var,$post) = ($1,$2,$3); 1078 if ($var eq $COMP_DNSBL_TEXT) { 1079 $myresult=$val=$pre.(join "; ", uniq(@DNSBL_Text)).$post; 1080 } elsif (defined $request->{$var}) { 1081 $myresult=$val=$pre.$request->{$var}.$post; 1082 }; 1083 log_info ("substitute : \"$myitem\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest devar ]); 1084 }; 1085 return $myresult; 1086}; 1087 1088# resolves %%() groups 1089sub degroup_item { 1090 my($now, $groupname) = @_; 1091 my(@myresult) = (); 1092 $groupname =~ s/^$COMP_GROUP//; 1093 return () unless $groupname; 1094 if ($postfwd_settings{group}{noparent}) { 1095 if (defined $Group_Cache{$groupname}) { 1096 foreach my $item (keys %{$Group_Cache{$groupname}}) { 1097 push @myresult, $item if $Group_Cache{$groupname}{$item} >= $now; 1098 }; 1099 }; 1100 } else { 1101 my $cmd = "CMD=".$postfwd_commands{groupget}.";TYPE=$groupname"; 1102 my $res = cache_query ($cmd); 1103 log_info ("get parent group '".$cmd."' -> '".($res || '<undef>')."'") if wantsdebug (qw[ all thisrequest groups ]); 1104 unless ($res eq '<undef>') { 1105 push @myresult, split $postfwd_settings{sepreq}, $res; 1106 @myresult = uniq(@myresult); 1107 }; 1108 }; 1109 log_info ("group : \"$groupname\" \"".((@myresult) ? (join ',', @myresult) : '<empty>')."\"") if wantsdebug (qw[ all thisrequest groups ]); 1110 return @myresult; 1111}; 1112 1113# clean up RBL cache 1114sub cleanup_dns_cache { 1115 my($now) = $_[0]; return unless $now; 1116 foreach my $checkitem (keys %DNS_Cache) { 1117 # remove inclomplete objects (dns timeouts) 1118 unless ( defined($DNS_Cache{$checkitem}{'until'}) and defined($DNS_Cache{$checkitem}{ttl}) ) { 1119 log_info ("[CLEANUP] deleting incomplete dns-cache item '$checkitem'") 1120 if wantsdebug (qw[ all cleanup childcleanup devel ]); 1121 delete $DNS_Cache{$checkitem}; 1122 # remove timed out objects 1123 } elsif ( $now > $DNS_Cache{$checkitem}{'until'} ) { 1124 log_info ("[CLEANUP] removing dns-cache item '$checkitem' after ttl ".$DNS_Cache{$checkitem}{ttl}."s") 1125 if wantsdebug (qw[ all cleanup childcleanup ]); 1126 delete $DNS_Cache{$checkitem}; 1127 }; 1128 }; 1129}; 1130 1131# clean up request cache 1132sub cleanup_request_cache { 1133 my($now) = $_[0]; return unless $now; 1134 foreach my $checkitem (keys %Request_Cache) { 1135 unless ( defined($Request_Cache{$checkitem}{'until'}) and defined($Request_Cache{$checkitem}{ttl}) ) { 1136 log_info ("[CLEANUP] deleting incomplete request-cache item '$checkitem'") 1137 if wantsdebug (qw[ all cleanup childcleanup devel ]); 1138 delete $Request_Cache{$checkitem}; 1139 } elsif ( $now > $Request_Cache{$checkitem}{'until'} ) { 1140 log_info ("[CLEANUP] removing request-cache item '$checkitem' after ttl ".$Request_Cache{$checkitem}{ttl}."s") 1141 if wantsdebug (qw[ all cleanup childcleanup ]); 1142 delete $Request_Cache{$checkitem}; 1143 }; 1144 }; 1145}; 1146 1147# clean up group cache 1148sub cleanup_group_cache { 1149 my($now) = $_[0]; return unless $now; 1150 foreach my $groupname (keys %Group_Cache) { 1151 foreach my $groupitem (keys %{$Group_Cache{$groupname}}) { 1152 unless ( $Group_Cache{$groupname}{$groupitem} ) { 1153 log_info ("[CLEANUP] deleting incomplete group-cache item '$groupitem' from group '$groupname'") 1154 if wantsdebug (qw[ all cleanup childcleanup devel groups ]); 1155 delete $Group_Cache{$groupname}{$groupitem}; 1156 } elsif ($now > $Group_Cache{$groupname}{$groupitem}) { 1157 log_info ("[CLEANUP] removing expired group-cache item '$groupitem' from group '$groupname'") 1158 if wantsdebug (qw[ all cleanup childcleanup groups ]); 1159 delete $Group_Cache{$groupname}{$groupitem}; 1160 }; 1161 }; 1162 unless ( scalar keys %{$Group_Cache{$groupname}} ) { 1163 log_info ("[CLEANUP] removing empty group '$groupname'") 1164 if wantsdebug (qw[ all cleanup childcleanup groups ]); 1165 delete $Group_Cache{$groupname}; 1166 }; 1167 }; 1168}; 1169 1170# clean up rate cache 1171sub cleanup_rate_cache { 1172 my($now) = $_[0]; return unless $now; 1173 foreach my $checkitem (keys %Rate_Cache) { 1174 unless (defined $Rate_Cache{$checkitem}{'list'}) { 1175 log_info ("[CLEANUP] deleting incomplete rate-cache item '$checkitem'") 1176 if wantsdebug (qw[ all cleanup childcleanup devel ]); 1177 delete $Rate_Cache{$checkitem}; 1178 } else { 1179 my @i = (); 1180 foreach my $crate (@{$Rate_Cache{$checkitem}{'list'}}) { 1181 if ( not(defined $Rate_Cache{$checkitem}{$crate}{'until'}) or not(defined $Rate_Cache{$checkitem}{$crate}{'ttl'}) ) { 1182 log_info ("[CLEANUP] deleting incomplete rate-cache item '$checkitem'->'$crate'") 1183 if wantsdebug (qw[ all cleanup childcleanup devel ]); 1184 delete $Rate_Cache{$checkitem}{$crate}; 1185 } elsif ( $now > $Rate_Cache{$checkitem}{$crate}{'until'} ) { 1186 log_info ("[CLEANUP] removing rate-cache item '$checkitem'->'$crate' after ttl ".$Rate_Cache{$checkitem}{$crate}{ttl}."s") 1187 if wantsdebug (qw[ all cleanup childcleanup ]); 1188 delete $Rate_Cache{$checkitem}{$crate}; 1189 } else { 1190 push @i, $crate; 1191 }; 1192 }; 1193 unless ($i[0]) { 1194 log_info ("[CLEANUP] removing complete rate-cache item '$checkitem'") 1195 if wantsdebug (qw[ all cleanup childcleanup ]); 1196 delete $Rate_Cache{$checkitem}; 1197 } else { 1198 log_info ("[CLEANUP] new limits for rate-cache item '$checkitem': ".(join ', ', @i)) 1199 if wantsdebug (qw[ all cleanup childcleanup ]); 1200 @{$Rate_Cache{$checkitem}{'list'}} = @i; 1201 }; 1202 }; 1203 }; 1204}; 1205 1206# saves rate limits to disk 1207sub save_rates { 1208 cleanup_rate_cache (time()); 1209 save_hash (\%Rate_Cache, 'rate', $postfwd_settings{rate}{store}); 1210}; 1211 1212# loads rate limits from disk 1213sub load_rates { 1214 load_hash (\%Rate_Cache, 'rate', $postfwd_settings{rate}{store}); 1215 cleanup_rate_cache (time()) if scalar keys %Rate_Cache; 1216}; 1217 1218# saves group cache to disk 1219sub save_groups { 1220 cleanup_group_cache (time()); 1221 save_hash (\%Group_Cache, 'group', $postfwd_settings{group}{store}); 1222}; 1223 1224# loads group cache from disk 1225sub load_groups { 1226 load_hash (\%Group_Cache, 'group', $postfwd_settings{group}{store}); 1227 cleanup_group_cache (time()) if scalar keys %Group_Cache; 1228}; 1229 1230# return cache contents 1231sub dump_cache { 1232 my @result = (); 1233 my %cache = (); 1234 $cache{Request_Cache} = \%Request_Cache; 1235 $cache{Rate_Cache} = \%Rate_Cache; 1236 $cache{DNS_Cache} = \%DNS_Cache; 1237 $cache{Group_Cache} = \%Group_Cache; 1238 foreach (keys %cache) { 1239 push @result, hash_to_list ('%'.$_, $cache{$_}) if %{$cache{$_}}; 1240 }; return @result; 1241}; 1242 1243# aggregate ip address items with NetAddr::IP->Compact() 1244sub aggregate_addr { 1245 my($file,$num,$index,$item,@addrs) = @_; 1246 my @keep = my @comp = (); 1247 foreach my $addr (@addrs) { 1248 # disable aggregation for dynamic elements (lfile, negation, ...) 1249 if ( $addr =~ m@$COMP_ADDRESS_ITEM@ ) { 1250 ($1) ? push @comp, $1 : push @keep, $addr; 1251 } else { 1252 push @keep, $addr; 1253 }; 1254 }; 1255 if ( @comp ) { 1256 @comp = Compact( map { $_ = NetAddr::IP->new($_) } @comp ); 1257 # the compare operator can be safely overwritten for cidr 1258 # as all of them lead to the same action 1259 map { $_ = "=;$_" } @comp; 1260 push @keep, @comp; 1261 log_note ("notice: Rule $index ($file line $num): aggregated ".$item."-networks : ".(scalar @addrs)." -> ".(scalar @keep)) if wantsdebug (qw[ all thisrequest config aggr cidr ]); 1262 }; 1263 return @keep; 1264}; 1265 1266# preparses configuration line for ACL syntax 1267sub acl_parser { 1268 my($file,$num,$myline) = @_; 1269 if ( $myline =~ /^\s*($COMP_ACL[\-\w]+)\s*{\s*(.*?)\s*;?\s*}[\s;]*$/ ) { 1270 $ACLs{$1} = $2; $myline = ""; 1271 } else { 1272 while ( $myline =~ /($COMP_ACL[\-\w]+)/) { 1273 my($acl) = $1; 1274 if ( $acl and defined $ACLs{$acl} ) { 1275 $myline =~ s/\s*$acl\s*/$ACLs{$acl}/g; 1276 } else { 1277 log_warn ("file $file, ignoring line $num: undefined macro '$acl'"); 1278 return ""; 1279 }; 1280 }; 1281 }; 1282 return $myline; 1283}; 1284 1285# prepares pcre item 1286sub prepare_pcre { 1287 my($item) = shift; undef my $neg; 1288 # temporarily remove negation 1289 $item = $neg if ($neg = deneg_item($item)); 1290 # allow // regex 1291 $item =~ s/^\/?(.*?)\/?$/$1/; 1292 # re-enable negation 1293 $item = "!!($item)" if $neg; 1294 return $item; 1295}; 1296 1297# prepares file item 1298sub prepare_file { 1299 my($forced_reload,$type,$cmp,$file) = @_; my(@result) = (); undef my $fh; 1300 my($is_table) = ($type =~ /^$COMP_TABLES$/); 1301 unless (-e $file) { 1302 log_warn ("error: $type:$file not found - will be ignored"); 1303 return @result; 1304 }; 1305 if ( not($forced_reload) and (defined $Config_Cache{$file}{lastread}) and ($Config_Cache{$file}{lastread} > (stat $file)[9]) ) { 1306 log_info ("$type:$file unchanged - using cached content (mtime: " 1307 .(stat $file)[9].", cache: $Config_Cache{$file}{lastread})") 1308 if wantsdebug (qw[ all thisrequest config ]); 1309 return @{$Config_Cache{$file}{content}}; 1310 }; 1311 unless (open ($fh, '<', $file)) { 1312 log_warn ("error: could not open $type:$file - $! - will be ignored"); 1313 return @result; 1314 }; 1315 log_info ("reading $type:$file") if wantsdebug (qw[ all thisrequest config ]); 1316 while (<$fh>) { 1317 chomp; 1318 s/#.*//g; 1319 next if /^\s*$/; 1320 s/\s+[^\s]+$// if $is_table; 1321 s/^\s+//; s/\s+$//; 1322 push @result, prepare_item($forced_reload, $cmp, $_); 1323 }; close ($fh); 1324 # update Config_Cache 1325 $Config_Cache{$file}{lastread} = time(); 1326 @{$Config_Cache{$file}{content}} = @result; 1327 log_info ("read ".($#result + 1)." items from $type:$file") if wantsdebug (qw[ all thisrequest config ]); 1328 return @result; 1329}; 1330 1331# prepares ruleset item 1332sub prepare_item { 1333 my($forced_reload,$cmp,$item) = @_; my(@result) = (); undef my $type; 1334 if ($item =~ /$COMP_CONF_FILE_TABLE/) { 1335 return prepare_file ($forced_reload, $1, $cmp, $2); 1336 } elsif ($cmp eq '=~' or $cmp eq '!~') { 1337 return $cmp.";".prepare_pcre($item); 1338 } else { 1339 return $cmp.";".$item; 1340 }; 1341}; 1342 1343# compatibility for old "rate"-syntax 1344sub check_for_old_syntax { 1345 my($myindex,$myfile,$mynum,$mykey,$myvalue) = @_; 1346 if ($mykey =~ /^action$/) { 1347 if ($myvalue =~ /^(\w[\-\w]+)\s*\(\s*(.*?)\s*\)$/) { 1348 my($mycmd,$myarg) = ($1, $2); 1349 if ($mycmd =~ /^(rate|size|rcpt)(5321)?$/i) { 1350 if ($myarg =~ /^\$\$(.*)$/) { 1351 $myarg = $1; 1352 $myvalue = "$mycmd($myarg)"; 1353 log_note ( "notice: Rule $myindex ($myfile line $mynum): " 1354 ."removing obsolete '\$\$' for $mycmd limit index. See man page for new syntax." ) if wantsdebug (qw[ all thisrequest config verbose ]); 1355 }; 1356 push @Rate_Items, (split '/', $myarg)[0]; 1357 }; 1358 }; 1359 }; 1360 return $myvalue; 1361}; 1362 1363# parses configuration line 1364sub parse_config_line { 1365 my($forced_reload, $myfile, $mynum, $myindex, $myline) = @_; 1366 my(%myrule) = (); 1367 my($mykey, $myvalue, $mycomp, $prevalert); 1368 eval { 1369 local $SIG{'__DIE__'}; 1370 local $SIG{'ALRM'} = sub { $myline =~ s/[ \t][ \t]*/ /g; log_warn ("timeout after ".$postfwd_settings{timeout}{config}."s at parsing Rule $myindex ($myfile line $mynum): \"$myline\""); %myrule = (); die }; 1371 $prevalert = alarm($postfwd_settings{timeout}{config}) if $postfwd_settings{timeout}{config}; 1372 if ( $myline = acl_parser ($myfile, $mynum, $myline) ) { 1373 unless ( $myline =~ /^\s*[^=\s]+\s*$COMP_SEPARATOR\s*([^;\s]+\s*)+(;\s*[^=\s]+\s*$COMP_SEPARATOR\s*([^;\s]+\s*)+)*[;\s]*$/ ) { 1374 log_warn ("ignoring invalid $myfile line ".$mynum.": \"".$myline."\""); 1375 } else { 1376 # separate items 1377 foreach (split ";", $myline) { 1378 # remove whitespaces around 1379 s/^\s*(.*?)\s*($COMP_SEPARATOR)\s*(.*?)\s*$/$1$2$3/; 1380 ( ($mycomp = $2) =~ /^([\<\>\~])=$/ ) and $mycomp = "=$1"; 1381 ($mykey, $myvalue) = split /$COMP_SEPARATOR/, $_, 2; 1382 if ($mykey =~ /^$COMP_SINGLE$/) { 1383 log_note ( "notice: Rule $myindex ($myfile line $mynum):" 1384 ." overriding $mykey=\"".$myrule{$mykey}."\"" 1385 ." with $mykey=\"$myvalue\"" 1386 ) if (defined $myrule{$mykey}); 1387 $myvalue = check_for_old_syntax($myindex,$myfile,$mynum,$mykey,$myvalue); 1388 $myrule{$mykey} = $myvalue; 1389 } elsif ($mykey =~ /^$COMP_CSV$/) { 1390 map { push @{$myrule{$mykey}}, prepare_item ($forced_reload, $mycomp, $_) } ( split /\s*,\s*/, $myvalue ); 1391 } elsif ($mykey =~ /^$COMP_ACTION$/) { 1392 push @{$myrule{$mykey}}, $myvalue; 1393 } else { 1394 push @{$myrule{$mykey}}, prepare_item ($forced_reload, $mycomp, $myvalue); 1395 }; 1396 }; 1397 unless (exists($myrule{$COMP_ACTION})) { 1398 log_warn ("Rule ".$myindex." ($myfile line ".$mynum."): contains no action and will be ignored"); 1399 return (%myrule = ()); 1400 }; 1401 unless (exists($myrule{$COMP_ID})) { 1402 $myrule{$COMP_ID} = "R-".$myindex; 1403 log_note ("notice: Rule $myindex ($myfile line $mynum): contains no rule identifier - will use \"$myrule{id}\"") if wantsdebug (qw[ all thisrequest config verbose ]); 1404 }; 1405 map { @{$myrule{$_}} = aggregate_addr($myfile,$mynum,$myindex,$_,@{$myrule{$_}}) if $_ =~ /$COMP_NETWORK_CIDRS/ } (keys %myrule) if ($NETADDR and $postfwd_settings{aggregate_addrs}); 1406 log_info ("loaded: Rule $myindex ($myfile line $mynum): id->\"$myrule{id}\" action->\"".(join ' || ', @{$myrule{action}})."\"") if wantsdebug (qw[ all thisrequest config verbose ]); 1407 }; 1408 }; 1409 alarm($prevalert) if $postfwd_settings{timeout}{config}; 1410 }; 1411 return %myrule; 1412}; 1413 1414# parses configuration file 1415sub read_config_file { 1416 my($forced_reload, $myindex, $myfile) = @_; 1417 my(%myrule, @myruleset, @lines) = (); 1418 my($mybuffer) = ""; undef my $fh; 1419 1420 unless (-e $myfile) { 1421 log_warn ("error: file ".$myfile." not found - file will be ignored"); 1422 } else { 1423 unless (open ($fh, '<', $myfile)) { 1424 log_warn ("error: could not open ".$myfile." - $! - file will be ignored"); 1425 } else { 1426 log_info ("reading file $myfile") if wantsdebug (qw[ all thisrequest config verbose ]); 1427 while (<$fh>) { 1428 chomp; 1429 s/(\"|#.*)//g; 1430 next if /^\s*$/; 1431 if ( /(.*)\\\s*$/ or /(.*\{)\s*$/ ) { $mybuffer = $mybuffer.$1; next; }; 1432 $mybuffer .= $_; 1433 if ( $lines[0] and $mybuffer =~ /^(\}|\s+\S)/ ) { 1434 my $last = pop(@lines); $last .= ';' unless $last =~ /;\s*$/; 1435 $mybuffer = $last.$mybuffer; 1436 }; 1437 push @lines, $mybuffer; 1438 $mybuffer = ""; 1439 }; 1440 map { 1441 log_info ("parsing line: '$_'") if wantsdebug (qw[ all thisrequest config ]); 1442 %myrule = parse_config_line ($forced_reload, $myfile, $., ($#myruleset+$myindex+1), $mybuffer.$_); 1443 push ( @myruleset, { %myrule } ) if (%myrule); 1444 $mybuffer = ""; 1445 } @lines; 1446 close ($fh); 1447 log_info ("loaded: Rules $myindex - ".($myindex + $#myruleset)." from file \"$myfile\"") if wantsdebug (qw[ all thisrequest config verbose ]); 1448 }; 1449 }; 1450 return @myruleset; 1451}; 1452 1453# reads all configuration items 1454sub read_config { 1455 my($forced_reload) = shift; 1456 my(%myrule, @myruleset) = (); 1457 my($mytype,$myitem); 1458 1459 # init, cleanup cache and config vars 1460 @Rules = (); %Rule_by_ID = %Request_Cache = (); @Rate_Items = (); 1461 %Rate_Cache = () unless $postfwd_settings{keep_rates}; 1462 %Group_Cache = () unless $postfwd_settings{keep_groups}; 1463 1464 # parse configurations 1465 for my $config (@{$postfwd_settings{Configs}}) { 1466 ($mytype,$myitem) = split $postfwd_settings{sepreq}, $config; 1467 if ($mytype eq "r" or $mytype eq "rule") { 1468 %myrule = parse_config_line ($forced_reload, 'RULE', 0, ($#Rules + 1), $myitem); 1469 push ( @Rules, { %myrule } ) if (%myrule); 1470 } elsif ($mytype eq "f" or $mytype eq "file") { 1471 if ( not($forced_reload) and defined $Config_Cache{$myitem}{lastread} and ($Config_Cache{$myitem}{lastread} > (stat $myitem)[9]) ) { 1472 log_info ("file \"$myitem\" unchanged - using cached ruleset (mtime: ".(stat $myitem)[9].", 1473 cache: $Config_Cache{$myitem}{lastread})" 1474 ) if wantsdebug (qw[ all thisrequest config verbose ]); 1475 push ( @Rules, @{$Config_Cache{$myitem}{ruleset}} ) if $Config_Cache{$myitem}{ruleset}; 1476 } else { 1477 @myruleset = read_config_file ($forced_reload, ($#Rules + 1), $myitem); 1478 if (@myruleset) { 1479 @Rules = ( @Rules, @myruleset ) if @myruleset; 1480 $Config_Cache{$myitem}{lastread} = time(); 1481 @{$Config_Cache{$myitem}{ruleset}} = @myruleset; 1482 }; 1483 }; 1484 }; 1485 }; 1486 if ($#Rules < 0) { 1487 log_warn("critical: no rules found - i feel useless (have you set -f or -r?)"); 1488 } else { 1489 # update Rule by ID hash 1490 map { $Rule_by_ID{$Rules[$_]{$COMP_ID}} = $_ } (0 .. $#Rules); 1491 if ($postfwd_settings{request}{autocacheid}) { 1492 # update AutoCacheID hash 1493 my @myautocacheid = (); 1494 map { $Rule_by_ID{$Rules[$_]{$COMP_ID}} = $_; push @myautocacheid, keys %{$Rules[$_]} } (0 .. $#Rules); 1495 map { $AutoCacheID{lc($_)} = 1 } (grep !/^($COMP_ID|$COMP_ACTION)$/, uniq(@myautocacheid)); 1496 if (%AutoCacheID) { 1497 log_info ("Setting AutoCacheID to '".(join ',', (sort keys %AutoCacheID))."'") if wantsdebug(qw[ all verbose thisrequest config cache ]); 1498 } else { 1499 log_info ("No items found in ruleset. Disabling AutoCacheID") if wantsdebug(qw[ all verbose thisrequest config cache ]); 1500 }; 1501 }; 1502 if ( @Rate_Items ) { 1503 @Rate_Items = uniq(@Rate_Items); 1504 log_info ("rate items: ".(join ', ', @Rate_Items)) if wantsdebug (qw[ all thisrequest verbose rates ]); 1505 # disable request cache with ratelimits 1506 $postfwd_settings{request}{ttl} = 0; 1507 log_note ("disabling request cache due to ratelimits in configuration"); 1508 }; 1509 }; 1510}; 1511 1512# displays configuration 1513sub show_config { 1514 if (wantsdebug (qw[ all verbose ])) { 1515 print STDOUT "=" x 75, "\n"; 1516 printf STDOUT "Rule count: %s\n", ($#Rules + 1); 1517 print STDOUT "=" x 75, "\n"; 1518 }; 1519 for my $index (0 .. $#Rules) { 1520 next unless exists $Rules[$index]; 1521 printf STDOUT "Rule %3d: id->\"%s\"; action->\"%s\"", $index, $Rules[$index]{$COMP_ID}, join(' || ', @{$Rules[$index]{$COMP_ACTION}}); 1522 my $line = (wantsdebug (qw[ all verbose ])) ? "\n\t " : ""; 1523 for my $mykey ( reverse sort keys %{$Rules[$index]} ) { 1524 unless (($mykey eq $COMP_ACTION) or ($mykey eq $COMP_ID)) { 1525 $line .= "; " unless wantsdebug (qw[ all verbose ]); 1526 $line .= ($mykey =~ /^$COMP_SINGLE$/) 1527 ? $mykey."->\"".$Rules[$index]{$mykey}."\"" 1528 : $mykey."->\"".(join ', ', @{$Rules[$index]{$mykey}})."\""; 1529 $line .= " ; " if wantsdebug (qw[ all verbose ]); 1530 }; 1531 }; 1532 $line =~ s/\s*\;\s*$// if wantsdebug (qw[ all verbose ]); 1533 printf STDOUT "%s\n", $line; 1534 print STDOUT "-" x 75, "\n" if wantsdebug (qw[ all verbose ]); 1535 }; 1536}; 1537 1538 1539## sub DNS 1540 1541# checks for rbl timeouts 1542sub rbl_timeout { 1543 my($myrbl) = shift; 1544 return ( ($postfwd_settings{dns}{max_timeout} > 0) and (defined $Timeouts{$myrbl}) and ($Timeouts{$myrbl} > $postfwd_settings{dns}{max_timeout}) ); 1545}; 1546 1547# reads DNS answers 1548sub rbl_read_dns { 1549 my($myresult) = shift; 1550 my($now) = time(); 1551 my($que,$ttl,$res,$typ) = undef; 1552 my(@addrs,@texts) = (); 1553 1554 if ( defined $myresult ) { 1555 # read question, for dns cache id 1556 foreach ($myresult->question) { 1557 $typ = ($_->qtype || ''); $que = ($_->qname || ''); 1558 map { log_info ("[GETDNS00] type=$typ, query=$que, $_") } (hash_to_list ('%packet', $myresult)) 1559 if wantsdebug (qw[ all thisrequest dns getdns getdnspacket ]); 1560 next unless ($typ and $que); 1561 log_info ("[GETDNS01] type=$typ, query=$que") if wantsdebug (qw[ all thisrequest dns getdns ]); 1562 unless ( (defined $DNS_Cache{$que}) 1563 and (($typ eq 'A') or ($typ eq 'TXT')) ) { 1564 log_note ("[DNSBL] ignoring unknown query '$que', type '$typ'"); 1565 next; 1566 }; 1567 1568 # parse answers 1569 foreach ($myresult->answer) { 1570 log_info ("[GETDNS02] type=$typ, query=$que, restype='".$_->type."'") if wantsdebug (qw[ all thisrequest dns getdns ]); 1571 if ($_->type eq 'A') { 1572 push @addrs, $_->address if $_->address; 1573 $ttl = $_->ttl; 1574 log_info ("[GETDNSA1] type=$typ, query=$que, ttl=$ttl, answer='".($_->address || '')."'") if wantsdebug (qw[ all thisrequest dns getdns ]); 1575 } elsif ($_->type eq 'TXT') { 1576 $res = (join(" ", $_->char_str_list()) || ''); 1577 # escape commas for set() action 1578 $res =~ s/,/ /g; 1579 push @texts, $res; 1580 $ttl = $_->ttl; 1581 log_info ("[GETDNST1] type=$typ, query=$que, ttl=$ttl, answer='$res'") if wantsdebug (qw[ all thisrequest dns getdns ]); 1582 } elsif (wantsdebug (qw[ all thisrequest dns getdns ])) { 1583 log_info ("[GETDNS??] received answer type=".$typ." for query $que"); 1584 }; 1585 }; 1586 1587 # save result in cache 1588 if ($typ eq 'A') { 1589 $ttl = ( $DNS_Cache{$que}{ttl} > ($ttl||=0) ) ? $DNS_Cache{$que}{ttl} : $ttl; 1590 @{$DNS_Cache{$que}{A}} = @addrs; 1591 $DNS_Cache{$que}{ttl} = $ttl; 1592 $DNS_Cache{$que}{delay} = ($now - $DNS_Cache{$que}{delay}); 1593 $DNS_Cache{$que}{'log'} = 1; 1594 $DNS_Cache{$que}{'until'} = $now + $DNS_Cache{$que}{ttl}; 1595 log_info ("[GETDNSA2] type=$typ, query=$que, cache='".(hash_to_str($DNS_Cache{$que}))."'") if wantsdebug (qw[ all thisrequest dns getdns ]); 1596 #} elsif ($typ eq 'TXT') { 1597 } else { 1598 $res = (join(" ", @texts) || ''); 1599 $ttl = ( $DNS_Cache{$que}{ttl} > ($ttl||=0) ) ? $DNS_Cache{$que}{ttl} : $ttl; 1600 $DNS_Cache{$que}{TXT} = $res; 1601 $DNS_Cache{$que}{ttl} = $ttl unless $DNS_Cache{$que}{ttl}; 1602 log_info ("[GETDNST2] type=$typ, query=$que, cache='".(hash_to_str($DNS_Cache{$que}))."'") if wantsdebug (qw[ all thisrequest dns getdns ]); 1603 }; 1604 }; 1605 return $que if (@addrs || $res); 1606 } else { 1607 log_note ("[DNSBL] dns timeout"); 1608 }; 1609}; 1610 1611# fires DNS queries 1612sub rbl_prepare_lookups { 1613 my($mytype, $myval, @myrbls) = @_; 1614 my($myresult) = undef; 1615 my($cmp,$rblitem,$myquery); 1616 my(@lookups) = (); 1617 1618 # skip these 1619 return @lookups if not(defined $myval) or ($myval eq '') or ($myval eq "unknown") or ($myval =~ /:/); 1620 1621 # removes duplicate lookups, but keeps the specified order 1622 @myrbls = uniq(@myrbls); 1623 1624 RBLQUERY: foreach (@myrbls) { 1625 1626 # separate rbl-name and answer 1627 ($cmp,$rblitem) = split ";", $_; 1628 next RBLQUERY unless $rblitem; 1629 my($myrbl, $myrblans, $myrbltime) = split /\//, $rblitem; 1630 next RBLQUERY unless $myrbl; 1631 next RBLQUERY if rbl_timeout($myrbl); 1632 $myrblans = $postfwd_settings{dns}{mask} unless $myrblans; 1633 $myrbltime = $postfwd_settings{dns}{ttl} unless $myrbltime; 1634 1635 # create query string 1636 $myquery = $myval.".".$myrbl; 1637 my $mypat = qr/$myrblans/; 1638 1639 # query our cache 1640 if ( exists($DNS_Cache{$myquery}) and exists($DNS_Cache{$myquery}{A}) ) { 1641 ANSWER1: foreach (@{$DNS_Cache{$myquery}{A}}) { last ANSWER1 if $myresult = ( $_ =~ /$mypat/ ) }; 1642 log_info ("[DNSBL] cached $mytype: $myrbl $myval ($myquery) - answer: \'".(join ", ", @{$DNS_Cache{$myquery}{A}})."\'") 1643 if ( wantsdebug (qw[ all thisrequest ]) or ($myresult and wantsdebug (qw[ verbose ])) ); 1644 1645 # query parent cache 1646 } elsif ( not($postfwd_settings{dns}{noparent}) 1647 and not((my $pans = cache_query ("CMD=".$postfwd_commands{getcacheitem}.";TYPE=dns;ITEM=$myquery")) eq '<undef>') ) { 1648 %{$DNS_Cache{$myquery}} = str_to_hash(\$pans); delete $DNS_Cache{$myquery}{'log'} if $DNS_Cache{$myquery}{'log'}; 1649 if ($DNS_Cache{$myquery}{A}) { 1650 ref $DNS_Cache{$myquery}{A} eq 'ARRAY' or $DNS_Cache{$myquery}{A} = [ $DNS_Cache{$myquery}{A} ]; 1651 ANSWER2: foreach (@{$DNS_Cache{$myquery}{A}}) { last ANSWER2 if $myresult = ( $_ =~ /$mypat/ ) }; 1652 log_info ("[DNSBL] parent cached $mytype: $myrbl $myval ($myquery) - answer: \'".(join ", ", @{$DNS_Cache{$myquery}{A}})."\'") 1653 if ( wantsdebug (qw[ all thisrequest ]) or ($myresult and wantsdebug (qw[ verbose ])) ); 1654 }; 1655 1656 # not found -> prepare dns query 1657 } else { 1658 $DNS_Cache{$myquery} = { 1659 type => $mytype, 1660 name => $myrbl, 1661 value => $myval, 1662 ttl => $myrbltime, 1663 delay => time(), 1664 }; 1665 log_info("[DNSBL] query $mytype: $myrbl $myval ($myquery)") if wantsdebug (qw[ all thisrequest ]); 1666 push @lookups, $myquery; 1667 }; 1668 }; 1669 # return necessary lookups 1670 return @lookups; 1671}; 1672 1673# checks RBL items 1674sub rbl_check { 1675 my($mytype,$myrbl,$myval) = @_; 1676 my($myanswer,$myrblans,$myrbltime,$myresult,$mystart,$myend); 1677 my($m1,$m2,$myrbltype,$m4,$myrbltxt,$myquery); 1678 my($now) = time(); 1679 1680 # skip these 1681 return $myresult if not(defined $myval) or ($myval eq '') or ($myval eq "unknown") or ($myval =~ /:/); 1682 1683 # separate rbl-name and answer 1684 ($myrbl, $myrblans, $myrbltime) = split '/', $myrbl; 1685 $myrblans = $postfwd_settings{dns}{mask} unless $myrblans; 1686 $myrbltime = $postfwd_settings{dns}{ttl} unless $myrbltime; 1687 1688 # create query string 1689 $myquery = $myval.".".$myrbl; 1690 1691 # query our cache 1692 return $myresult unless ( $myresult = (defined $DNS_Cache{$myquery} and not(defined $DNS_Cache{$myquery}{'timed'})) ); 1693 if (not($postfwd_settings{dns}{noparent}) and defined $DNS_Cache{$myquery}{'log'}) { 1694 my $pdns = "CMD=".$postfwd_commands{setcacheitem}.";TYPE=dns;ITEM=$myquery".hash_to_str($DNS_Cache{$myquery}); 1695 cache_query ($pdns); 1696 }; 1697 if ( $myresult = ($#{$DNS_Cache{$myquery}{A}} >= 0) ) { 1698 my $mypat = qr/$myrblans/; 1699 ANSWER: foreach (@{$DNS_Cache{$myquery}{A}}) { 1700 last ANSWER if ( $myresult = ( ($_) and ($_ =~ m/$mypat/)) ); 1701 }; 1702 push @DNSBL_Text, $DNS_Cache{$myquery}{type}.':'.$DNS_Cache{$myquery}{name}.':<'.($DNS_Cache{$myquery}{TXT} || '').'>' 1703 if $myresult and defined $DNS_Cache{$myquery}{type} and defined $DNS_Cache{$myquery}{name}; 1704 if ( wantsdebug (qw[ all thisrequest verbose ]) or $postfwd_settings{dns}{anylog} 1705 or ($myresult and not($postfwd_settings{dns}{nolog}) and defined $DNS_Cache{$myquery}{'log'}) ) { 1706 log_info ("[DNSBL] ".( ($mytype eq $COMP_RBL_KEY) ? join('.', reverse(split(/\./,$myval))) : $myval )." listed on " 1707 .lc(($DNS_Cache{$myquery}{type} || $mytype)).":$myrbl (answer: ".(join ", ", @{$DNS_Cache{$myquery}{A}}) 1708 .", time: ".ts($DNS_Cache{$myquery}{delay})."s, ttl: ".$DNS_Cache{$myquery}{ttl}."s, '".($DNS_Cache{$myquery}{TXT} || '')."')"); 1709 delete $DNS_Cache{$myquery}{'log'} if defined $DNS_Cache{$myquery}{'log'}; 1710 }; 1711 }; 1712 return $myresult; 1713}; 1714 1715# dns resolver wrapper 1716sub dns_query { 1717 my (@queries) = @_; undef my @result; 1718 eval { 1719 local $SIG{__DIE__} = sub { log_note ("[DNS] ERROR: \"$!\", DETAIL: \"@_\""); return if $^S; }; 1720 @result = dns_query_net_dns(@queries); 1721 }; 1722 return @result; 1723}; 1724 1725# resolves dns queries using Net::DNS 1726sub dns_query_net_dns { 1727 my (@queries) = @_; undef my @result; undef my $pans; 1728 my %ownsock = (); my @ownready = (); undef my $bgsock; 1729 my $ownsel = IO::Select->new(); 1730 my $dns = Net::DNS::Resolver->new( 1731 tcp_timeout => $postfwd_settings{dns}{timeout}, 1732 udp_timeout => $postfwd_settings{dns}{timeout}, 1733 persistent_tcp => 0, persistent_udp => 0, 1734 retrans => 0, retry => 1, dnsrch => 0, defnames => 0, 1735 ); 1736 my $now = time(); 1737 # prepare queries 1738 foreach (@queries) { 1739 my ($item, $type) = split ','; $type ||= 'A'; 1740 # query child cache 1741 if ( (defined $DNS_Cache{$item}{$type}) and (defined $DNS_Cache{$item}{'until'}) and ($DNS_Cache{$item}{'until'} >= $now) ) { 1742 $DNS_Cache{$item}{$type} = [ $DNS_Cache{$item}{$type} ] unless (ref $DNS_Cache{$item}{$type} eq 'ARRAY'); 1743 log_info ("[DNS] dnsccache: item=$item, type=$type -> ".(join ',', @{$DNS_Cache{$item}{$type}})." (ttl: ".($DNS_Cache{$item}{ttl} || 0).")") 1744 if ($postfwd_settings{dns}{anylog} or wantsdebug (qw[ all thisrequest dns getdns ])); 1745 push @result, @{$DNS_Cache{$item}{$type}}; 1746 # query parent cache 1747 } elsif ( not($postfwd_settings{dns}{noparent}) 1748 and not(($pans = cache_query ("CMD=".$postfwd_commands{getcacheitem}.";TYPE=dns;ITEM=$item")) eq '<undef>') 1749 and (%{$DNS_Cache{$item}} = str_to_hash(\$pans)) 1750 and (defined $DNS_Cache{$item}{$type}) and (defined $DNS_Cache{$item}{'until'}) and ($DNS_Cache{$item}{'until'} >= $now) ) { 1751 $DNS_Cache{$item}{$type} = [ $DNS_Cache{$item}{$type} ] unless (ref $DNS_Cache{$item}{$type} eq 'ARRAY'); 1752 log_info ("[DNS] dnspcache: item=$item, type=$type -> ".(join ',', @{$DNS_Cache{$item}{$type}})." (ttl: ".($DNS_Cache{$item}{ttl} || 0).")") 1753 if ($postfwd_settings{dns}{anylog} or wantsdebug (qw[ all thisrequest dns getdns ])); 1754 push @result, @{$DNS_Cache{$item}{$type}}; 1755 # send queries 1756 } else { 1757 log_info ("[DNS] dnsquery: item=$item, type=$type") 1758 if ($postfwd_settings{dns}{anylog} or wantsdebug (qw[ all thisrequest dns getdns ])); 1759 $DNS_Cache{$item}{delay} = $now; 1760 $bgsock = $dns->bgsend ($item, $type); 1761 $ownsel->add($bgsock); 1762 $ownsock{$bgsock} = $item.','.$type; 1763 }; 1764 }; 1765 # retrieve answers 1766 while ((scalar keys %ownsock) and (@ownready = $ownsel->can_read($postfwd_settings{dns}{timeout}))) { 1767 foreach my $sock (@ownready) { 1768 if (defined $ownsock{$sock}) { 1769 my $packet = $dns->bgread($sock); 1770 my ($item, $type) = split ',', $ownsock{$sock}; 1771 my $rname = $DNS_REPNAMES{$type}; 1772 my @rrs = (grep { $_->type eq $type } $packet->answer); 1773 $now = time(); my $ttl = 0; my @ans = (); 1774 if (@rrs) { 1775 # sort MX records by preference 1776 @rrs = sort { $a->preference <=> $b->preference } @rrs if ($type eq 'MX'); 1777 foreach my $rr (@rrs) { 1778 $ttl = $rr->ttl if ($rr->ttl > $ttl); 1779 log_info ("[DNS] dnsanswer: item=$item, type=$type -> $rname=".$rr->$rname." (ttl: $ttl)") 1780 if ($postfwd_settings{dns}{anylog} or wantsdebug (qw[ all thisrequest dns setdns ])); 1781 push @ans, $rr->$rname; 1782 }; 1783 push @result, @ans; 1784 }; 1785 # add to dns cache 1786 $ttl ||= $postfwd_settings{dns}{ttl}; 1787 @{$DNS_Cache{$item}{$type}} = @ans; 1788 $DNS_Cache{$item}{ttl} = $ttl; 1789 $DNS_Cache{$item}{'until'} = $now + $ttl; 1790 $DNS_Cache{$item}{delay} = ($DNS_Cache{delay}) ? $now - $DNS_Cache{delay} : 0; 1791 cache_query ( "CMD=".$postfwd_commands{setcacheitem}.";TYPE=dns;ITEM=$item".hash_to_str($DNS_Cache{$item}) ) 1792 unless ($postfwd_settings{dns}{noparent}); 1793 $DNS_Cache{$item}{'log'} = 1; 1794 log_info ("[DNS] dnsanswers: item=$item, type=$type -> $rname=".((@{$DNS_Cache{$item}{$type}}) ? join ',', @{$DNS_Cache{$item}{$type}} : '')." (delay: ".ts($DNS_Cache{$item}{delay}).", ttl: $ttl)") 1795 if ($postfwd_settings{dns}{anylog} or wantsdebug (qw[ all thisrequest verbose dns setdns ])); 1796 delete $ownsock{$sock}; 1797 } else { 1798 $ownsel->remove($sock); 1799 $sock = undef; 1800 }; 1801 }; 1802 }; 1803 # show timeouts 1804 map { log_note ("dnsquery: timeout for $_ after ".$postfwd_settings{dns}{timeout}." seconds") } (values %ownsock); 1805 return @result; 1806}; 1807 1808 1809## SUB plugins 1810 1811# 1812# these subroutines integrate additional attributes to 1813# a request before the ruleset is evaluated 1814# call: %result = postfwd_items{foo}(%request) 1815# save: $result{$_} 1816# 1817%postfwd_items = ( 1818 "__builtin__" => sub { 1819 my($request) = shift; 1820 # postfwd version 1821 $request->{version} = $postfwd_settings{name}." ".$postfwd_settings{version}; 1822 # postfwd server interface 1823 $request->{postfwd_interface} = $postfwd_settings{server}{interface}; 1824 # postfwd server port 1825 $request->{postfwd_port} = $postfwd_settings{server}{port}; 1826 # sender info 1827 $request->{sender} =~ /(.*)@([^@]*)$/; 1828 ( $request->{sender_localpart}, $request->{sender_domain} ) = ( $1, $2 ); 1829 # recipient info 1830 $request->{recipient} =~ /(.*)@([^@]*)$/; 1831 ( $request->{recipient_localpart}, $request->{recipient_domain} ) = ( $1, $2 ); 1832 # reverted ip address (for lookups) 1833 if ( $request->{client_address} =~ /\./ ) { 1834 # IPv4 1835 $request->{reverse_address} = join(".", reverse(split(/\./,$request->{client_address}))); 1836 } elsif ( $request->{client_address} =~ /:/ ) { 1837 # IPv6 1838 $request->{reverse_address} = join('.', (reverse split '', (join '', (map { $_ = sprintf "%04s", $_ } (split /:/, $request->{client_address}))))) 1839 if ($postfwd_settings{dns}{ipv6_dnsbl}); 1840 }; 1841 }, 1842 "sender_dns" => sub { 1843 my($request) = @_; 1844 map { $request->{$_} = $request->{sender_domain} } ($COMP_NS_NAME, $COMP_NS_ADDR, $COMP_MX_NAME, $COMP_MX_ADDR); 1845 $request->{$COMP_HELO_ADDR} = $request->{helo_name}; 1846 }, 1847); 1848# returns additional request information 1849# for all postfwd_items 1850sub postfwd_items { 1851 my($request) = shift; 1852 foreach (sort keys %postfwd_items) { 1853 log_info ("[PLUGIN] executing postfwd-item ".$_) 1854 if wantsdebug (qw[ all thisrequest ]); 1855 &{$postfwd_items{$_}}($request) if (defined $postfwd_items{$_}); 1856 }; 1857 map { $request->{$_} = '' unless (defined $request->{$_}); log_info ("[PLUGIN] ATTR: $_=$request->{$_}") if wantsdebug (qw[ all thisrequest ]) } (keys %{$request}); 1858}; 1859# 1860# compare item subroutines 1861# must take compare_item_foo ( $COMPARE_TYPE, $RULEITEM, $REQUESTITEM, $REQUEST ); 1862# 1863%postfwd_compare = ( 1864 # old version: cidr only for v4, v6 as regex 1865 "cidr_postfwd" => sub { 1866 my($cmp,$val,$myitem,$request) = @_; 1867 my($myresult) = ($val and $myitem); 1868 log_info ("type cidr_postfwd : \"$myitem\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest ]); 1869 if ($myresult) { 1870 # always true 1871 $myresult = ($val eq '0.0.0.0/0'); 1872 unless ($myresult) { 1873 # v4 addresses only 1874 $myresult = ($myitem =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/); 1875 if ($myresult) { 1876 $val .= '/32' unless ($val =~ /\/\d{1,2}$/); 1877 $myresult = cidr_match((cidr_parse($val)),$myitem); 1878 } else { 1879 log_info ("Non IPv4 address. Using type default") if wantsdebug (qw[ all thisrequest ]); 1880 return &{$postfwd_compare{default}}($cmp,$val,$myitem,$request); 1881 }; 1882 }; 1883 }; 1884 $myresult = not($myresult) if ($cmp eq '!='); 1885 return $myresult; 1886 }, 1887 # new version: based on NetAddr::IP 1888 "cidr_netaddr" => sub { 1889 my($cmp,$val,$myitem,$request) = @_; 1890 my($myresult) = ($val and $myitem); 1891 log_info ("type cidr_netaddr : \"$myitem\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest ]); 1892 if ($myresult) { 1893 $myresult = 0; 1894 # compare with NetAddr::IP->within(), unfortunately code dies on unknown value 1895 eval { local $SIG{__DIE__} = sub{ log_warn("NetAddr::IP error comparing '$val' to '$myitem'") }; $myresult = NetAddr::IP->new($myitem)->within(NetAddr::IP->new($val)) }; 1896 }; 1897 $myresult = not($myresult) if ($cmp eq '!='); 1898 return $myresult; 1899 }, 1900 # new version: based on Net::CIDR::Lite 1901 "cidr_netcidr" => sub { 1902 my($cmp,$val,$myitem,$request) = @_; 1903 my($myresult) = ($val and $myitem); 1904 log_info ("type cidr_netcidr : \"$myitem\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest ]); 1905 if ($myresult) { 1906 $myresult = 0; 1907 # compare with Net::CIDR::Lite->find(), unfortunately code dies on unknown value 1908 eval { local $SIG{__DIE__} = sub{ log_warn("Net::CIDR::Lite error comparing '$val' to '$myitem'") }; $myresult = Net::CIDR::Lite->new($val)->find($myitem) }; 1909 }; 1910 $myresult = not($myresult) if ($cmp eq '!='); 1911 return $myresult; 1912 }, 1913 "numeric" => sub { 1914 my($cmp,$val,$myitem,$request) = @_; 1915 my($myresult) = undef; 1916 log_info ("type numeric : \"$myitem\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest ]); 1917 $myitem ||= "0"; $val ||= "0"; 1918 if ($cmp eq '==') { 1919 $myresult = ($myitem == $val); 1920 } elsif ($cmp eq '=<') { 1921 $myresult = ($myitem <= $val); 1922 } elsif ($cmp eq '=>') { 1923 $myresult = ($myitem >= $val); 1924 } elsif ($cmp eq '<') { 1925 $myresult = ($myitem < $val); 1926 } elsif ($cmp eq '>') { 1927 $myresult = ($myitem > $val); 1928 } elsif ($cmp eq '!=') { 1929 $myresult = not($myitem == $val); 1930 } elsif ($cmp eq '!<') { 1931 $myresult = not($myitem <= $val); 1932 } elsif ($cmp eq '!>') { 1933 $myresult = not($myitem >= $val); 1934 } else { 1935 $myresult = ($myitem >= $val); 1936 }; 1937 return $myresult; 1938 }, 1939 $COMP_RBL_KEY => sub { 1940 my($cmp,$val,$myitem,$request) = @_; 1941 my($myresult) = not($postfwd_settings{dns}{disabled}); 1942 log_info ("type rbl : \"$myitem\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest ]); 1943 $myresult = ( rbl_check ($COMP_RBL_KEY, $val, $myitem) ) if $myresult; 1944 $myresult = not($myresult) if ($cmp eq '!='); 1945 return $myresult; 1946 }, 1947 $COMP_RHSBL_KEY => sub { 1948 my($cmp,$val,$myitem,$request) = @_; 1949 my($myresult) = not($postfwd_settings{dns}{disabled}); 1950 log_info ("type rhsbl : \"$myitem\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest ]); 1951 $myresult = ( rbl_check ($COMP_RHSBL_KEY, $val, $myitem) ) if $myresult; 1952 $myresult = not($myresult) if ($cmp eq '!='); 1953 return $myresult; 1954 }, 1955 $COMP_MONTHS => sub { 1956 my($cmp,$val,$myitem,$request) = @_; 1957 my($myresult) = undef; 1958 my($imon) = (split (',', $myitem))[4]; $imon ||= 0; 1959 my($rmin,$rmax) = split (/\s*-\s*/, $val); 1960 $rmin = ($rmin) ? (($rmin =~ /^\d$/) ? $rmin : $months{$rmin}) : $imon; 1961 $rmax = ($rmax) ? (($rmax =~ /^\d$/) ? $rmax : $months{$rmax}) : (($val =~ /-/) ? $imon : $rmin); 1962 log_info ("type months : \"$imon\" \"$cmp\" \"$rmin\"-\"$rmax\"") 1963 if wantsdebug (qw[ all thisrequest ]); 1964 $myresult = (($rmin <= $imon) and ($rmax >= $imon)); 1965 $myresult = not($myresult) if ($cmp eq '!='); 1966 return $myresult; 1967 }, 1968 $COMP_DAYS => sub { 1969 my($cmp,$val,$myitem,$request) = @_; 1970 my($myresult) = undef; 1971 my($iday) = (split (',', $myitem))[6]; $iday ||= 0; 1972 my($rmin,$rmax) = split (/\s*-\s*/, $val); 1973 $rmin = ($rmin) ? (($rmin =~ /^\d$/) ? $rmin : $weekdays{$rmin}) : $iday; 1974 $rmax = ($rmax) ? (($rmax =~ /^\d$/) ? $rmax : $weekdays{$rmax}) : (($val =~ /-/) ? $iday : $rmin); 1975 log_info ("type days : \"$iday\" \"$cmp\" \"$rmin\"-\"$rmax\"") 1976 if wantsdebug (qw[ all thisrequest ]); 1977 $myresult = (($rmin <= $iday) and ($rmax >= $iday)); 1978 $myresult = not($myresult) if ($cmp eq '!='); 1979 return $myresult; 1980 }, 1981 $COMP_DATE => sub { 1982 my($cmp,$val,$myitem,$request) = @_; 1983 my($myresult) = undef; 1984 my($isec,$imin,$ihour,$iday,$imon,$iyear) = split (',', $myitem); 1985 my($rmin,$rmax) = split (/\s*-\s*/, $val); 1986 my($idat) = ($iyear + 1900) . ((($imon+1) < 10) ? '0'.($imon+1) : ($imon+1)) . (($iday < 10) ? '0'.$iday : $iday); 1987 $rmin = ($rmin) ? join ('', reverse split ('\.', $rmin)) : $idat; 1988 $rmax = ($rmax) ? join ('', reverse split ('\.', $rmax)) : (($val =~ /-/) ? $idat : $rmin); 1989 log_info ("type date : \"$idat\" \"$cmp\" \"$rmin\"-\"$rmax\"") 1990 if wantsdebug (qw[ all thisrequest ]); 1991 $myresult = (($rmin <= $idat) and ($rmax >= $idat)); 1992 $myresult = not($myresult) if ($cmp eq '!='); 1993 return $myresult; 1994 }, 1995 $COMP_TIME => sub { 1996 my($cmp,$val,$myitem,$request) = @_; 1997 my($myresult) = undef; 1998 my($isec,$imin,$ihour,$iday,$imon,$iyear) = split (',', $myitem); 1999 my($rmin,$rmax) = split (/\s*-\s*/, $val); 2000 my($idat) = (($ihour < 10) ? '0'.$ihour : $ihour) . (($imin < 10) ? '0'.$imin : $imin) . (($isec < 10) ? '0'.$isec : $isec); 2001 $rmin = ($rmin) ? join ('', split ('\:', $rmin)) : $idat; 2002 $rmax = ($rmax) ? join ('', split ('\:', $rmax)) : (($val =~ /-/) ? $idat : $rmin); 2003 log_info ("type time : \"$idat\" \"$cmp\" \"$rmin\"-\"$rmax\"") 2004 if wantsdebug (qw[ all thisrequest ]); 2005 $myresult = (($rmin <= $idat) and ($rmax >= $idat)); 2006 $myresult = not($myresult) if ($cmp eq '!='); 2007 return $myresult; 2008 }, 2009 $COMP_HELO_ADDR => sub { 2010 my($cmp,$val,$myitem,$request) = @_; 2011 my($myresult) = undef; 2012 return $myresult if $postfwd_settings{dns}{disabled}; 2013 return $myresult unless $myitem =~ /\./; 2014 if ( my @answers = dns_query ("$myitem,A") ) { 2015 log_info ("type $COMP_HELO_ADDR : \"".(join ',', @answers)."\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest ]); 2016 map { $myresult = ( &{$postfwd_compare{cidr}}(($cmp,$val,$_,$request)) ); return $myresult if $myresult } @answers; 2017 }; 2018 return $myresult; 2019 }, 2020 $COMP_NS_NAME => sub { 2021 my($cmp,$val,$myitem,$request) = @_; 2022 my($myresult) = undef; 2023 return $myresult if $postfwd_settings{dns}{disabled}; 2024 return $myresult unless $myitem =~ /\./; 2025 if ( my @answers = dns_query ("$myitem,NS") ) { 2026 log_info ("type $COMP_NS_NAME : \"".(join ',', @answers)."\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest ]); 2027 map { $myresult = ( &{$postfwd_compare{default}}(($cmp,$val,$_,$request)) ); return $myresult if $myresult } @answers; 2028 } else { 2029 $myresult = ( &{$postfwd_compare{default}}(($cmp,$val,'',$request)) ); 2030 }; 2031 return $myresult; 2032 }, 2033 $COMP_MX_NAME => sub { 2034 my($cmp,$val,$myitem,$request) = @_; 2035 my($myresult) = undef; 2036 return $myresult if $postfwd_settings{dns}{disabled}; 2037 return $myresult unless $myitem =~ /\./; 2038 if ( my @answers = dns_query ("$myitem,MX") ) { 2039 log_info ("type $COMP_MX_NAME : \"".(join ',', @answers)."\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest ]); 2040 map { $myresult = ( &{$postfwd_compare{default}}(($cmp,$val,$_,$request)) ); return $myresult if $myresult } @answers; 2041 } else { 2042 $myresult = ( &{$postfwd_compare{default}}(($cmp,$val,'',$request)) ); 2043 }; 2044 return $myresult; 2045 }, 2046 $COMP_NS_ADDR => sub { 2047 my($cmp,$val,$myitem,$request) = @_; 2048 my($myresult) = undef; 2049 return $myresult if $postfwd_settings{dns}{disabled}; 2050 return $myresult unless $myitem =~ /\./; 2051 if ( my @answers = dns_query ("$myitem,NS") ) { 2052 splice (@answers, $postfwd_settings{dns}{max_ns_lookups}) if $postfwd_settings{dns}{max_ns_lookups} and $#answers > $postfwd_settings{dns}{max_ns_lookups}; 2053 if ( @answers = dns_query (@answers) ) { 2054 log_info ("type $COMP_NS_ADDR : \"".(join ',', @answers)."\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest ]); 2055 map { $myresult = ( &{$postfwd_compare{cidr}}(($cmp,$val,$_,$request)) ); return $myresult if $myresult } @answers; 2056 }; 2057 }; 2058 return $myresult; 2059 }, 2060 $COMP_MX_ADDR => sub { 2061 my($cmp,$val,$myitem,$request) = @_; 2062 my($myresult) = undef; 2063 return $myresult if $postfwd_settings{dns}{disabled}; 2064 return $myresult unless $myitem =~ /\./; 2065 if ( my @answers = dns_query ("$myitem,MX") ) { 2066 splice (@answers, $postfwd_settings{dns}{max_mx_lookups}) if $postfwd_settings{dns}{max_mx_lookups} and $#answers > $postfwd_settings{dns}{max_mx_lookups}; 2067 if ( @answers = dns_query (@answers) ) { 2068 log_info ("type $COMP_MX_ADDR : \"".(join ',', @answers)."\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest ]); 2069 map { $myresult = ( &{$postfwd_compare{cidr}}(($cmp,$val,$_,$request)) ); return $myresult if $myresult } @answers; 2070 }; 2071 }; 2072 return $myresult; 2073 }, 2074 "default" => sub { 2075 my($cmp,$val,$myitem,$request) = @_; 2076 my($var,$myresult) = undef; 2077 log_info ("type default : \"$myitem\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest ]); 2078 # backward compatibility 2079 $cmp = '==' if ( ($var) and ($cmp eq '=') ); 2080 if ($cmp eq '==') { 2081 $myresult = ( lc($myitem) eq lc($val) ) if $myitem; 2082 } elsif ($cmp eq '!=') { 2083 $myresult = not( lc($myitem) eq lc($val) ) if $myitem; 2084 } elsif ($cmp eq '=<') { 2085 $myresult = (($myitem || 0) <= $val); 2086 } elsif ($cmp eq '!<') { 2087 $myresult = not(($myitem || 0) <= $val); 2088 } elsif ($cmp eq '=>') { 2089 $myresult = (($myitem || 0) >= $val); 2090 } elsif ($cmp eq '!>') { 2091 $myresult = not(($myitem || 0) >= $val); 2092 } elsif ($cmp eq '<') { 2093 $myresult = (($myitem || 0) < $val); 2094 } elsif ($cmp eq '>') { 2095 $myresult = (($myitem || 0) > $val); 2096 } elsif ($cmp eq '=~') { 2097 $myresult = ($myitem =~ /$val/i); 2098 } elsif ($cmp eq '!~') { 2099 $myresult = ($myitem !~ /$val/i); 2100 } else { 2101 # allow // regex 2102 $val =~ s/^\/?(.*?)\/?$/$1/; 2103 $myresult = $myitem =~ /$val/i; 2104 }; 2105 return $myresult; 2106 }, 2107 "cidr" => sub { return &{$postfwd_compare{cidr_postfwd}}(@_); }, 2108 "client_address" => sub { return &{$postfwd_compare{cidr}}(@_); }, 2109 "encryption_keysize" => sub { return &{$postfwd_compare{numeric}}(@_); }, 2110 "size" => sub { return &{$postfwd_compare{numeric}}(@_); }, 2111 "recipient_count" => sub { return &{$postfwd_compare{numeric}}(@_); }, 2112 "request_score" => sub { return &{$postfwd_compare{numeric}}(@_); }, 2113 $COMP_RHSBL_KEY_CLIENT => sub { return &{$postfwd_compare{$COMP_RHSBL_KEY}}(@_); }, 2114 $COMP_RHSBL_KEY_SENDER => sub { return &{$postfwd_compare{$COMP_RHSBL_KEY}}(@_); }, 2115 $COMP_RHSBL_KEY_HELO => sub { return &{$postfwd_compare{$COMP_RHSBL_KEY}}(@_); }, 2116 $COMP_RHSBL_KEY_RCLIENT => sub { return &{$postfwd_compare{$COMP_RHSBL_KEY}}(@_); }, 2117); 2118# 2119# these subroutines define postfwd actions 2120# 2121%postfwd_actions = ( 2122 # example action foo() 2123 # "foo" => sub { 2124 # my($index,$now,$mycmd,$myarg,$myline,%request) = @_; 2125 # my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2126 # ... 2127 # return ($stop,$index,$myaction,$myline); 2128 # }, 2129 # jump() command 2130 "jump" => sub { 2131 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2132 my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2133 if (defined $Rule_by_ID{$myarg}) { 2134 my($ruleno) = $Rule_by_ID{$myarg}; 2135 log_info ("[RULES] ".$myline 2136 .", jump to rule $ruleno (id $myarg)") 2137 if wantsdebug (qw[ all thisrequest verbose ]); 2138 $index = $ruleno - 1; 2139 } else { 2140 log_warn ("[RULES] ".$myline." - error: jump failed, can not find rule-id ".$myarg." - ignoring"); 2141 }; 2142 return ($stop,$index,$myaction,$myline); 2143 }, 2144 # set() command 2145 "set" => sub { 2146 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2147 my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2148 foreach ( split (",", $myarg) ) { 2149 if ( /^\s*([^=]+?)\s*([\.\-\*\/\+=]=|=[\.\-\*\/\+=]|=)\s*(.*?)\s*$/ ) { 2150 my($r_var, $mod, $r_val) = ($1, $2, $3); 2151 my($m_val) = (defined $request->{$r_var}) ? $request->{$r_var} : 0; 2152 # saves some ifs 2153 if (($mod eq '=') or ($mod eq '==')) { 2154 $m_val = $r_val; 2155 } elsif ( ($mod eq '.=') or ($mod eq '=.') ) { 2156 $m_val .= $r_val; 2157 } elsif ( (($mod eq '+=') or ($mod eq '=+')) and (($m_val=~/^\-?\d+(\.\d+)?$/) and ($r_val=~/^\-?\d+(\.\d+)?$/)) ) { 2158 $m_val += $r_val; 2159 } elsif ( (($mod eq '-=') or ($mod eq '=-')) and (($m_val=~/^\-?\d+(\.\d+)?$/) and ($r_val=~/^\-?\d+(\.\d+)?$/)) ) { 2160 $m_val -= $r_val; 2161 } elsif ( (($mod eq '*=') or ($mod eq '=*')) and (($m_val=~/^\-?\d+(\.\d+)?$/) and ($r_val=~/^\-?\d+(\.\d+)?$/)) ) { 2162 $m_val *= $r_val; 2163 } elsif ( (($mod eq '/=') or ($mod eq '=/')) and (($m_val=~/^\-?\d+(\.\d+)?$/) and ($r_val=~/^\-?\d+(\.\d+)?$/)) ) { 2164 $m_val /= (($r_val == 0) ? 1 : $r_val); 2165 } else { 2166 $m_val = $r_val; 2167 }; 2168 $m_val = $1.($2 || '').($3 || '') if ( $m_val =~ /^(\-?\d+)([\.,]\d+)?(.*)$/ ); 2169 (defined $request->{$r_var}) 2170 ? log_info ("notice", "[RULES] ".$myline.", redefining existing ".$r_var."=".$request->{$r_var}." with ".$r_var."=".$m_val) 2171 : log_info ("[RULES] ".$myline.", defining ".$r_var."=".$m_val) 2172 if wantsdebug (qw[ all thisrequest verbose ]); 2173 $request->{$r_var} = $m_val; 2174 } else { 2175 log_warn ("[RULES] ".$myline.", ignoring unknown set() attribute ".$_); 2176 }; 2177 }; 2178 return ($stop,$index,$myaction,$myline); 2179 }, 2180 # score() command 2181 "score" => sub { 2182 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2183 my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2184 my($score) = (defined $request->{request_score}) ? $request->{request_score} : 0; 2185 if ($myarg =~/^([\+\-\*\/\=]?)(\d+)([\.,](\d+))?$/) { 2186 my($mod, $val) = ($1, $2 + ((defined $4) ? "0.$4" : 0)); 2187 if ($mod eq '-') { 2188 $score -= $val; 2189 } elsif ($mod eq '*') { 2190 $score *= $val; 2191 } elsif ($mod eq '/') { 2192 $score /= $val unless ($val == 0); 2193 } elsif ($mod eq '=') { 2194 $score = $val; 2195 } else { 2196 $score += $val; 2197 }; 2198 $score = $1.((defined $2) ? $2 : '.0') if ( $score =~ /^(\-?\d+)([\.,]\d\d?)?/ ); 2199 log_info ("[SCORE] ".$myline.", modifying score about ".$myarg." points to ". $score) 2200 if wantsdebug (qw[ all thisrequest verbose ]); 2201 $request->{score} = $request->{request_score} = $score; 2202 } elsif ($myarg) { 2203 log_warn ("[RULES] ".$myline.", invalid value for score \"$myarg\" - ignoring"); 2204 }; 2205 MAXSCORE: foreach my $max_score (reverse sort keys %{$postfwd_settings{scores}}) { 2206 if ( ($score >= $max_score) and ($postfwd_settings{scores}{$max_score}) ) { 2207 $myaction=$postfwd_settings{scores}{$max_score}; 2208 $myline .= ", score=".$score."/".$max_score; 2209 $stop = $score; last MAXSCORE; 2210 }; 2211 }; 2212 return ($stop,$index,$myaction,$myline); 2213 }, 2214 # mail() command 2215 "mail" => sub { 2216 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2217 my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2218 my($mserver,$mhelo,$mfrom,$mto,$msubject,$mbody) = split "/", $myarg, 6; 2219 ($mserver, my $mport) = split ":", $mserver; my $res = ""; 2220 my @talk = ( 2221 "HELO $mhelo", 2222 "MAIL FROM: $mfrom", 2223 "RCPT TO: $mto", 2224 "DATA", 2225 "Subject: $msubject\r\n$mbody\r\n.", 2226 "QUIT", 2227 ); 2228 if ( my $socket = IO::Socket::INET->new( 2229 PeerAddr => $mserver, 2230 PeerPort => ($mport ||= 25), 2231 Proto => 'tcp', 2232 Timeout => 30, 2233 Type => SOCK_STREAM, 2234 ) ) { 2235 SMTP: foreach (@talk) { 2236 print $socket "$_\r\n"; $res = <$socket>; chomp($res); 2237 last SMTP unless $res =~ /^[23][0-9][0-9] /; 2238 }; 2239 close($socket); 2240 log_info ("[MAIL] ".$myline.", mail server=<$mserver:$mport>, from=<$mfrom>, to=<$mto>, subject=<$msubject>, status=<$res>"); 2241 } else { 2242 log_info ("[MAIL] ".$myline.", could not open socket to $mserver:$mport: '$!'"); 2243 }; 2244 return ($stop,$index,$myaction,$myline); 2245 }, 2246 # sendmail() 2247 "sendmail" => sub { 2248 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2249 my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2250 my($mcmd,$mfrom,$mto,$msubject,$mbody) = split '::', $myarg, 5; 2251 my($msg) = "From: $mfrom\nTo: $mto\nSubject: $msubject\n\n$mbody\n"; 2252 my($sm) = undef; 2253 if ( (-x $mcmd) and open ($sm, '|-', "$mcmd -i -f $mfrom $mto") ) { 2254 if ( print $sm "$msg" ) { 2255 log_info ("[SENDMAIL] ".$myline.", $mcmd from=<$mfrom>, to=<$mto>, subject=<$msubject>"); 2256 } else { 2257 log_note ("[SENDMAIL] ".$myline.", could not print to $mcmd pipe: '$!'"); 2258 }; 2259 close($sm); 2260 } else { 2261 log_note ("[SENDMAIL] ".$myline.", could not open pipe to $mcmd: '$!'"); 2262 }; 2263 return ($stop,$index,$myaction,$myline); 2264 }, 2265 # rate() command 2266 "rate" => sub { 2267 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2268 my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2269 my($ratetype,$ratecount,$ratetime,$ratecmd) = split "/", $myarg, 4; 2270 my($rcount) = ( ($mycmd =~ /^size/) ? $request->{size} : (($mycmd =~ /^rcpt/) ? $request->{recipient_count} : 1 ) ); 2271 if ($ratetype and $ratecount and $ratetime and $ratecmd and $rcount) { 2272 my $crate = $Rules[$index]{$COMP_ID}.'+'.$ratecount.'_'.$ratetime; 2273 if ( defined $request->{$ratetype} ) { 2274 my $r = $request->{$ratetype}; 2275 unless ($mycmd =~ /5321$/) { 2276 $r = lc($r); 2277 } else { 2278 $r = ($r =~ /^([^@]+)@(\S+)$/) ? $1.'@'.lc($2) : lc($r); 2279 }; 2280 $ratetype .= "=".$r; 2281 push @{$Rate_Cache{$ratetype}{'list'}}, $crate; 2282 @{$Rate_Cache{$ratetype}{'list'}} = uniq(@{$Rate_Cache{$ratetype}{'list'}}); 2283 my $rate_exists = ( defined $Rate_Cache{$ratetype}{$crate} ); 2284 $Rate_Cache{$ratetype}{$crate}{'type'} = $mycmd; 2285 $Rate_Cache{$ratetype}{$crate}{'maxcount'} = $ratecount; 2286 # child rate cache 2287 if ($postfwd_settings{rate}{noparent}) { 2288 # create rate 2289 unless ( $rate_exists ) { 2290 $Rate_Cache{$ratetype}{$crate}{'ttl'} = $ratetime; 2291 $Rate_Cache{$ratetype}{$crate}{'until'} = $now + $ratetime; 2292 $Rate_Cache{$ratetype}{$crate}{'count'} = $rcount; 2293 log_info ("created rate limit object '$ratetype'->'".$crate."' with value '$rcount'") if wantsdebug (qw[ all thisrequest rates ]); 2294 } else { 2295 # renew rate 2296 if ( $now > $Rate_Cache{$ratetype}{$crate}{'until'} ) { 2297 $Rate_Cache{$ratetype}{$crate}{'ttl'} = $ratetime; 2298 $Rate_Cache{$ratetype}{$crate}{'until'} = $now + $ratetime; 2299 $Rate_Cache{$ratetype}{$crate}{'count'} = $rcount; 2300 log_info ("renewed rate limit object '$ratetype'->'".$crate."' with value '$rcount'") if wantsdebug (qw[ all thisrequest rates ]); 2301 # increase rate 2302 } else { 2303 $Rate_Cache{$ratetype}{$crate}{'count'} += $rcount || 0; 2304 log_info ("updated rate limit object '$ratetype'->'".$crate."' with value '$rcount' to '".$Rate_Cache{$ratetype}{$crate}{'count'}."'") if wantsdebug (qw[ all thisrequest rates ]); 2305 }; 2306 }; 2307 # parent rate cache 2308 } else { 2309 $Rate_Cache{$ratetype}{$crate}{'ttl'} = $ratetime; 2310 $Rate_Cache{$ratetype}{$crate}{'until'} = $now + $ratetime; 2311 $Rate_Cache{$ratetype}{$crate}{'count'} = $rcount; 2312 my $prate = "CMD=".$postfwd_commands{setrateitem}.";TYPE=rate;ITEM=$ratetype".$postfwd_settings{seplim}.$crate.hash_to_str($Rate_Cache{$ratetype}{$crate}); 2313 $prate = cache_query ($prate); 2314 $Rate_Cache{$ratetype}{$crate}{count} = $prate if ($prate =~ /^\d+$/); 2315 log_info ("updated parent rate limit object '$ratetype'->'".$crate."' with value '$rcount' to '".$Rate_Cache{$ratetype}{$crate}{'count'}."'") if wantsdebug (qw[ all thisrequest rates ]); 2316 }; 2317 # rate exceeded 2318 $stop = ( $Rate_Cache{$ratetype}{$crate}{count} > $Rate_Cache{$ratetype}{$crate}{maxcount} ); 2319 if ($stop) { 2320 $myaction=$ratecmd; 2321 $request->{'ratecount'} = $Rate_Cache{$ratetype}{$crate}{count} || 0; 2322 $myline .= ", rate=".$Rate_Cache{$ratetype}{$crate}{type}."/".$Rate_Cache{$ratetype}{$crate}{count}."/".$Rate_Cache{$ratetype}{$crate}{maxcount}; 2323 }; 2324 } else { 2325 log_note ("[RULES] ".$myline.", ignoring empty index for ".$mycmd." limit '".$ratetype."'") if wantsdebug (qw[ all thisrequest rates ]); 2326 }; 2327 } else { 2328 log_note ("[RULES] ".$myline.(($rcount) ? ", ignoring unknown ".$mycmd."() attribute \'".$myarg."\'" : ", ignoring empty counter")); 2329 }; 2330 return ($stop,$index,$myaction,$myline); 2331 }, 2332 # size() command 2333 "size" => sub { return &{$postfwd_actions{rate}}(@_); }, 2334 # rcpt() command 2335 "rcpt" => sub { return &{$postfwd_actions{rate}}(@_); }, 2336 # rate() command, according to rfc5321 case-sensivity 2337 "rate5321" => sub { return &{$postfwd_actions{rate}}(@_); }, 2338 # rcpt() command, according to rfc5321 case-sensivity 2339 "rcpt5321" => sub { return &{$postfwd_actions{rate}}(@_); }, 2340 # size() command, according to rfc5321 case-sensivity 2341 "size5321" => sub { return &{$postfwd_actions{rate}}(@_); }, 2342 # groupadd() command 2343 "groupadd" => sub { 2344 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2345 my($myaction) = $postfwd_settings{default}; 2346 my($groupname,$groupitem,$groupttl,$stop) = split "/", $myarg, 4; $stop ||= 0; 2347 $groupname =~ s/^$COMP_GROUP//; $groupitem =~ s/^$COMP_VAR//; 2348 if ( $groupname and $groupitem and $request->{$groupitem} ) { 2349 $groupttl ||= $postfwd_settings{group}{ttl}; 2350 log_info ("[RULES] ".$myline.", adding item '".$request->{$groupitem}."' to group '".$groupname."' with ttl=".$groupttl."s") if wantsdebug (qw[ all thisrequest groups ]); 2351 if ($postfwd_settings{group}{noparent}) { 2352 $Group_Cache{$groupname}{$request->{$groupitem}} = $now + $groupttl unless ( 2353 (defined $Group_Cache{$groupname}{$request->{$groupitem}} and $Group_Cache{$groupname}{$request->{$groupitem}} > $now) 2354 or (scalar keys %{$Group_Cache{$groupname}} >= $postfwd_settings{group}{maxitems}) 2355 or ($postfwd_settings{group}{maxitems} < 0) 2356 ); 2357 } else { 2358 my $cmd = "CMD=".$postfwd_commands{groupadd}.";TYPE=$groupname;ITEM=".$request->{$groupitem}.$postfwd_settings{seplim}.($now + $groupttl); 2359 my $res = cache_query ($cmd); 2360 log_info ("parent group update '".$cmd."' -> '".($res || '<undef>')."'") if wantsdebug (qw[ all thisrequest groups ]); 2361 }; 2362 }; 2363 return ($stop,$index,$myaction,$myline); 2364 }, 2365 # groupdel() command 2366 "groupdel" => sub { 2367 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2368 my($myaction) = $postfwd_settings{default}; 2369 my($groupname,$groupitem,$stop) = split "/", $myarg, 3; $stop ||= 0; 2370 $groupname =~ s/^$COMP_GROUP//; $groupitem =~ s/^$COMP_VAR//; 2371 if ( $groupname and $groupitem and $request->{$groupitem} ) { 2372 if ($postfwd_settings{group}{noparent}) { 2373 if ( defined $Group_Cache{$groupname}{$request->{$groupitem}} ) { 2374 log_info ("[RULES] ".$myline.", removing item '".$request->{$groupitem}."' from group '".$groupname."'") if wantsdebug (qw[ all thisrequest groups ]); 2375 delete $Group_Cache{$groupname}{$request->{$groupitem}}; 2376 } else { 2377 log_note ("[RULES] ".$myline.", item '".($request->{$groupitem} || '<undef>')."' not in group '".($groupname || '<undef>')."'") if wantsdebug (qw[ all thisrequest groups ]); 2378 }; 2379 } else { 2380 my $cmd = "CMD=".$postfwd_commands{groupdel}.";TYPE=$groupname;ITEM=".$request->{$groupitem}; 2381 my $res = cache_query ($cmd); 2382 log_info ("parent group update '".$cmd."' -> '".($res || '<undef>')."'") if wantsdebug (qw[ all thisrequest groups ]); 2383 }; 2384 }; 2385 return ($stop,$index,$myaction,$myline); 2386 }, 2387 # wait() command 2388 "wait" => sub { 2389 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2390 my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2391 log_info ("[RULES] ".$myline.", delaying for $myarg seconds"); 2392 sleep $myarg; 2393 return ($stop,$index,$myaction,$myline); 2394 }, 2395 # note() command 2396 "note" => sub { 2397 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2398 my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2399 log_info ("[RULES] ".$myline." - note: ".$myarg) if $myarg; 2400 return ($stop,$index,$myaction,$myline); 2401 }, 2402 # debug() command 2403 "debug" => sub { 2404 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2405 my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2406 log_info ("[RULES] ".$myline.", DEBUG=$myarg"); 2407 ($myarg =~ /^(1|y(es)?|on)$/i) ? $postfwd_settings{debug}{thisrequest} = 1 : delete $postfwd_settings{debug}{thisrequest}; 2408 return ($stop,$index,$myaction,$myline); 2409 }, 2410 # quit() command - not supported in this version 2411 "quit" => sub { 2412 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2413 my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2414 log_warn ("[RULES] ".$myline." - critical: quit (".$myarg.") unsupported in this version - ignoring"); 2415 return ($stop,$index,$myaction,$myline); 2416 }, 2417 # file() command 2418 "file" => sub { 2419 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2420 my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2421 log_warn ("[RULES] ".$myline." - error: command ".$mycmd."() has not been implemented yet - ignoring"); 2422 return ($stop,$index,$myaction,$myline); 2423 }, 2424 # dumpcache() command 2425 "dumpcache" => sub { 2426 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2427 my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2428 map { log_info ("[RULES] DUMPCACHE: $_") } (dump_cache()); 2429 return ($stop,$index,$myaction,$myline); 2430 }, 2431 # ask() command 2432 "ask" => sub { 2433 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 2434 my($myaction) = $postfwd_settings{default}; my($stop) = 0; 2435 log_info ("Opening socket to '$myarg'") if wantsdebug (qw[ all thisrequest ]); 2436 my($addr,$port,$ignore) = split ':', $myarg; 2437 my %orig = str_to_hash (\$request->{orig}); 2438 if ( ($addr and $port) and my $socket = new IO::Socket::INET ( 2439 PeerAddr => $addr, 2440 PeerPort => $port, 2441 Proto => 'tcp', 2442 Timeout => 9, 2443 Type => SOCK_STREAM ) ) { 2444 2445 my $sendstr = ''; 2446 foreach (keys %orig) { 2447 $sendstr .= $_."=".$orig{$_}."\n"; 2448 }; 2449 $sendstr .= "\n"; 2450 log_info ("Asking service $myarg -> '$sendstr'") if wantsdebug (qw[ all thisrequest ]); 2451 print $socket "$sendstr"; 2452 $sendstr = <$socket>; 2453 chomp($sendstr); 2454 log_info ("Answer from $myarg -> '$sendstr'") if wantsdebug (qw[ all thisrequest verbose ]); 2455 $sendstr =~ s/^(action=)//; 2456 if ($1 and $sendstr) { 2457 if ($ignore and ($sendstr =~ /$ignore/i)) { 2458 log_info ("ignoring answer '$sendstr' from $myarg") if wantsdebug (qw[ all thisrequest verbose ]); 2459 } else { 2460 $stop = $myaction = $sendstr; 2461 }; 2462 } else { 2463 log_note ("rule: $index got invalid answer '$sendstr' from $myarg"); 2464 }; 2465 } else { 2466 log_note ("Could not open socket to '$myarg' - $!"); 2467 }; 2468 return ($stop,$index,$myaction,$myline); 2469 }, 2470 # exec() command 2471 "exec" => sub { return &{$postfwd_actions{file}}(@_); }, 2472); 2473 2474# load plugin-items 2475sub get_plugins { 2476 my(@pluginfiles) = @_; 2477 my($pluginlog) = ''; 2478 foreach my $file (@pluginfiles) { 2479 unless ( -e $file ) { 2480 log_warn ("File not found: $file"); 2481 } else { 2482 $file =~ /^(.*)$/; 2483 require $1 if $1; 2484 map { delete $postfwd_items_plugin{$_} unless ($_ and defined $postfwd_items_plugin{$_}) } (keys %postfwd_items_plugin); 2485 map { delete $postfwd_compare_plugin{$_} unless ($_ and defined $postfwd_compare_plugin{$_}) } (keys %postfwd_compare_plugin); 2486 map { delete $postfwd_actions_plugin{$_} unless ($_ and defined $postfwd_actions_plugin{$_}) } (keys %postfwd_actions_plugin); 2487 map { log_note ("[PLUGIN] overriding prior item \'".$_."\'") if (defined $postfwd_items{$_}) } (keys %postfwd_items_plugin); 2488 map { log_note ("[PLUGIN] overriding prior compare function \'".$_."\'") if (defined $postfwd_compare{$_}) } (keys %postfwd_compare_plugin); 2489 map { log_note ("[PLUGIN] overriding prior action \'".$_."\'") if (defined $postfwd_actions{$_}) } (keys %postfwd_actions_plugin); 2490 %postfwd_items = ( %postfwd_items, %postfwd_items_plugin ) if %postfwd_items_plugin; 2491 %postfwd_compare = ( %postfwd_compare, %postfwd_compare_plugin ) if %postfwd_compare_plugin; 2492 %postfwd_actions = ( %postfwd_actions, %postfwd_actions_plugin ) if %postfwd_actions_plugin; 2493 $pluginlog = "[PLUGIN] Loaded plugins file: ".$file; 2494 $pluginlog .= " items: \"".(join ", ", (sort keys %postfwd_items_plugin))."\"" 2495 if %postfwd_items_plugin; 2496 $pluginlog .= " compare: \"".(join ", ", (sort keys %postfwd_compare_plugin))."\"" 2497 if %postfwd_compare_plugin; 2498 $pluginlog .= " actions: \"".(join ", ", (sort keys %postfwd_actions_plugin))."\"" 2499 if %postfwd_actions_plugin; 2500 log_info ($pluginlog); 2501 }; 2502 }; 2503}; 2504 2505 2506### SUB ruleset 2507 2508# compare item main 2509# use: compare_item ( $TYPE, $RULEITEM, $MINIMUMHITS, $REQUESTITEM, %REQUEST, %REQUESTINFO ); 2510sub compare_item { 2511 my($now,$mykey,$mymask,$mymin,$myitem,$request) = @_; 2512 my($val,$var,$cmp,$neg,$myresult,$postfwd_compare_proc); 2513 my($rcount) = 0; 2514 $mymin ||= 1; 2515 2516 # 2517 # determine the right compare function 2518 $postfwd_compare_proc = (defined $postfwd_compare{$mykey}) ? $mykey : "default"; 2519 # 2520 # save list due to possible modification 2521 my @items = @{$mymask}; 2522 # now compare request to every single item 2523 ITEM: foreach (@items) { 2524 ($cmp, $val) = split ";"; 2525 next ITEM unless ($cmp and (defined $val) and $mykey); 2526 # prepare_file 2527 if ($val =~ /$COMP_LIVE_FILE_TABLE/) { 2528 push @items, prepare_file (0, $1, $cmp, $2); 2529 next ITEM; 2530 }; 2531 # prepare_group 2532 if ($val =~ /^$COMP_GROUP/) { 2533 my @mygroup = degroup_item($now, $val); 2534 map { push @items, $cmp.";".$_ } @mygroup if @mygroup; 2535 next ITEM; 2536 }; 2537 log_info ("compare $mykey: \"$myitem\" \"$cmp\" \"$val\"") if wantsdebug (qw[ all thisrequest ]); 2538 $val = $neg if ($neg = deneg_item($val)); 2539 log_info ("deneg $mykey: \"$myitem\" \"$cmp\" \"$val\"") if ($neg and wantsdebug (qw[ all thisrequest ])); 2540 next ITEM unless (defined $val); 2541 # substitute check for $$vars in rule item 2542 if ( $var = devar_item ($cmp,$val,$myitem,$request) ) { 2543 $val = $var; $val =~ s/([^-_@\.\w\s])/\\$1/g unless ($cmp eq '=='); 2544 }; 2545 $myresult = &{$postfwd_compare{$postfwd_compare_proc}}($cmp,$val,$myitem,$request); 2546 log_info ("match $mykey: ".($myresult ? "TRUE" : "FALSE")) if wantsdebug (qw[ all thisrequest ]); 2547 if ($neg) { 2548 $myresult = not($myresult); 2549 log_info ("negate match $mykey: ".($myresult ? "TRUE" : "FALSE")) if wantsdebug (qw[ all thisrequest ]); 2550 }; 2551 $rcount++ if $myresult; 2552 $myresult = not($mymin eq 'all'); 2553 $myresult = ( $rcount >= $mymin ) if $myresult; 2554 log_info ("count $mykey: request=$rcount minimum: $mymin result: ".($myresult ? "TRUE" : "FALSE")) if wantsdebug (qw[ all thisrequest ]); 2555 last ITEM if $myresult; 2556 }; 2557 $myresult = $rcount if ($myresult or ($mymin eq 'all')); 2558 return $myresult; 2559}; 2560 2561 2562# 2563# compare request against a single rule 2564# 2565sub compare_rule { 2566 my($now,$index,$date,$request) = @_; 2567 my(@ruleitems) = keys %{$Rules[$index]}; 2568 my($has_rbl) = exists($Rules[$index]{$COMP_RBL_KEY}); 2569 my($has_rhl) = ( 2570 exists($Rules[$index]{$COMP_RHSBL_KEY}) or exists($Rules[$index]{$COMP_RHSBL_KEY_RCLIENT}) or 2571 exists($Rules[$index]{$COMP_RHSBL_KEY_CLIENT}) or exists($Rules[$index]{$COMP_RHSBL_KEY_SENDER}) or 2572 exists($Rules[$index]{$COMP_RHSBL_KEY_HELO}) 2573 ); 2574 my($has_senderdns) = ( exists($Rules[$index]{$COMP_NS_NAME}) 2575 or exists($Rules[$index]{$COMP_MX_NAME}) 2576 or exists($Rules[$index]{$COMP_NS_ADDR}) 2577 or exists($Rules[$index]{$COMP_MX_ADDR}) 2578 ); 2579 my($hasdns) = ( not($postfwd_settings{dns}{disabled}) and ($has_senderdns or $has_rhl or $has_rbl) ); 2580 my($myitem,$val,$cmp,$res,$myline,$timed) = undef; 2581 my(@myresult) = (0,0,0,0); 2582 my(@queries,@timedout) = (); 2583 my($num) = 1; 2584 undef @DNSBL_Text; 2585 my($ownres,$ownsel,$bgsock) = undef; 2586 my %ownsock = (); 2587 my @ownready = (); 2588 2589 log_info ("[RULES] rule: $index, id: $Rules[$index]{$COMP_ID}, items: '".((@ruleitems) ? join ';', @ruleitems: '')."'") if wantsdebug (qw[ all thisrequest ]); 2590 2591 # COMPARE-ITEMS 2592 # check all non-dns items 2593 ITEM: for my $mykey ( keys %{$Rules[$index]} ) { 2594 # always true 2595 if ( ($mykey eq $COMP_ID) or ($mykey eq $COMP_ACTION) or ($mykey eq $COMP_CACHE) ) { 2596 $myresult[0]++; 2597 next ITEM; 2598 }; 2599 next ITEM if ( (($mykey eq $COMP_RBL_CNT) or ($mykey eq $COMP_RHSBL_CNT)) ); 2600 next ITEM if ( (($mykey eq $COMP_RBL_KEY) or ($mykey eq $COMP_RHSBL_KEY)) ); 2601 next ITEM if ( ($mykey eq $COMP_RHSBL_KEY_RCLIENT) or ($mykey eq $COMP_RHSBL_KEY_CLIENT) or ($mykey eq $COMP_RHSBL_KEY_SENDER) or ($mykey eq $COMP_RHSBL_KEY_HELO) ); 2602 2603 # integration at this point enables redefining scores within ruleset 2604 if ($mykey eq $COMP_SCORES) { 2605 modify_score ($Rules[$index]{$mykey},$Rules[$index]{$COMP_ACTION}); 2606 $myresult[0] = 0; 2607 } else { 2608 $val = ( $mykey =~ /^$COMP_DATECALC$/ ) 2609 # prepare date check 2610 ? $date 2611 # default: compare against request attribute 2612 : $request->{$mykey}; 2613 $myresult[0] = ($res = compare_item($now, $mykey, $Rules[$index]{$mykey}, $num, ((defined $val) ? $val : ''), $request)) ? ($myresult[0] + $res) : 0; 2614 }; 2615 last ITEM unless ($myresult[0] > 0); 2616 }; 2617 log_info ("[RULES] pre-dns: rule: $index, id: $Rules[$index]{$COMP_ID}, RESULT: ".$myresult[0]) if wantsdebug (qw[ all thisrequest ]); 2618 2619 # DNSQUERY-SECTION 2620 # fire bgsend()s with callback to result cache, 2621 # if they are not contained already, 2622 # and $postfwd_settings{dns}{disabled} is not set 2623 if ($hasdns and $myresult[0]) { 2624 2625 # prepare dns queries 2626 $ownres = Net::DNS::Resolver->new( 2627 tcp_timeout => $postfwd_settings{dns}{timeout}, 2628 udp_timeout => $postfwd_settings{dns}{timeout}, 2629 persistent_tcp => 0, persistent_udp => 0, 2630 retrans => 0, retry => 1, dnsrch => 0, defnames => 0, 2631 ); 2632 $ownsel = IO::Select->new(); 2633 2634 map { $timed .= (($timed) ? ", $_" : $_) if $Timeouts{$_} > $postfwd_settings{dns}{max_timeout} } (keys %Timeouts); 2635 log_note ("[DNSBL] skipping rbls: $timed - too much timeouts") if $timed; 2636 2637 push @queries, rbl_prepare_lookups ( $COMP_RBL_KEY, $request->{reverse_address}, @{$Rules[$index]{$COMP_RBL_KEY}} ) 2638 if (defined $Rules[$index]{$COMP_RBL_KEY}); 2639 2640 push @queries, rbl_prepare_lookups ( $COMP_RHSBL_KEY, $request->{client_name}, @{$Rules[$index]{$COMP_RHSBL_KEY}} ) 2641 if (defined $Rules[$index]{$COMP_RHSBL_KEY}); 2642 2643 push @queries, rbl_prepare_lookups ( $COMP_RHSBL_KEY_CLIENT, $request->{client_name}, @{$Rules[$index]{$COMP_RHSBL_KEY_CLIENT}} ) 2644 if (defined $Rules[$index]{$COMP_RHSBL_KEY_CLIENT}); 2645 2646 push @queries, rbl_prepare_lookups ( $COMP_RHSBL_KEY_RCLIENT, $request->{reverse_client_name}, @{$Rules[$index]{$COMP_RHSBL_KEY_RCLIENT}} ) 2647 if (defined $Rules[$index]{$COMP_RHSBL_KEY_RCLIENT}); 2648 2649 push @queries, rbl_prepare_lookups ( $COMP_RHSBL_KEY_HELO, $request->{helo_name}, @{$Rules[$index]{$COMP_RHSBL_KEY_HELO}} ) 2650 if (defined $Rules[$index]{$COMP_RHSBL_KEY_HELO}); 2651 2652 push @queries, rbl_prepare_lookups ( $COMP_RHSBL_KEY_SENDER, $request->{sender_domain}, @{$Rules[$index]{$COMP_RHSBL_KEY_SENDER}} ) 2653 if (defined $Rules[$index]{$COMP_RHSBL_KEY_SENDER}); 2654 2655 # send dns queries 2656 if ( @queries ) { 2657 @queries = uniq(@queries); 2658 QUERY: foreach my $query (@queries) { 2659 next QUERY unless $query; 2660 log_info ("[SENDDNS] sending query \'$query\'") 2661 if wantsdebug (qw[ all thisrequest ]); 2662 # send A query 2663 $bgsock = $ownres->bgsend($query, 'A'); 2664 $ownsel->add($bgsock); 2665 $ownsock{$bgsock} = 'A:'.$query; 2666 # send TXT query 2667 if ($postfwd_settings{dns}{async_txt}) { 2668 $bgsock = $ownres->bgsend($query, 'TXT'); 2669 $ownsel->add($bgsock); 2670 $ownsock{$bgsock} = 'TXT:'.$query; 2671 }; 2672 }; 2673 log_info ("[SENDDNS] rule: $index, id: $Rules[$index]{$COMP_ID}, lookups: ".($#queries + 1)) 2674 if wantsdebug (qw[ all thisrequest ]); 2675 $myresult[3] = "dnsqueries=".($#queries + 1).$postfwd_settings{sepreq}."dnsinterval=".($#queries + 1); 2676 }; 2677 2678 # DNSRESULT-SECTION 2679 # wait for select() and check the results unless $postfwd_settings{dns}{disabled} 2680 my($ownstart) = time(); @queries = (); 2681 while ((scalar keys %ownsock) and (@ownready = $ownsel->can_read($postfwd_settings{dns}{timeout}))) { 2682 foreach my $sock (@ownready) { 2683 if (defined $ownsock{$sock}) { 2684 log_note ("[DNSBL] answer for ".$ownsock{$sock}) 2685 if wantsdebug (qw[ all thisrequest ]); 2686 my $packet = $ownres->bgread($sock); 2687 push @queries, (split ':', $ownsock{$sock})[1] if rbl_read_dns ($packet); 2688 delete $ownsock{$sock}; 2689 } else { 2690 $ownsel->remove($sock); 2691 $sock = undef; 2692 }; 2693 }; 2694 }; 2695 2696 # timeout handling 2697 map { push @timedout, (split ':', $ownsock{$_})[1] } (keys %ownsock); 2698 if (@timedout) { 2699 @timedout = uniq(@timedout); 2700 $myresult[3] .= $postfwd_settings{sepreq}."dnstimeouts=".($#timedout + 1); 2701 foreach (@timedout) { 2702 # @{$DNS_Cache{$_}{A}} = ('__TIMEOUT__'); 2703 $DNS_Cache{$_}{ttl} = $postfwd_settings{dns}{ttl} unless $DNS_Cache{$_}{ttl}; 2704 $DNS_Cache{$_}{'delay'} = $now - $ownstart; 2705 $DNS_Cache{$_}{'until'} = $now + $DNS_Cache{$_}{ttl}; 2706 $DNS_Cache{$_}{'timed'} = 1; 2707 $Timeouts{$DNS_Cache{$_}{name}} = (defined $Timeouts{$DNS_Cache{$_}{name}}) 2708 ? $Timeouts{$DNS_Cache{$_}{name}} + 1 2709 : 1 2710 if ( $postfwd_settings{dns}{max_timeout} > 0 ); 2711 log_note ("[DNSBL] warning: timeout (".$Timeouts{$DNS_Cache{$_}{name}}."/".$postfwd_settings{dns}{max_timeout}.") for ".$DNS_Cache{$_}{name}." after ".ts($DNS_Cache{$_}{'delay'})." seconds"); 2712 }; 2713 }; 2714 2715 # perform outstanding TXT queries unless --dns_async_txt is set 2716 if (not($postfwd_settings{dns}{async_txt}) and @queries) { 2717 @queries = uniq(@queries); 2718 log_info ("[DNSBL] sending TXT queries for ".(join ',', @queries)) if wantsdebug (qw[ all thisrequest debugdns ]); 2719 foreach my $query (@queries) { 2720 log_info ("[SENDDNS] sending TXT query \'$query\'") if wantsdebug (qw[ all thisrequest ]); 2721 # send TXT query 2722 $bgsock = $ownres->bgsend($query, 'TXT'); 2723 $ownsel->add($bgsock); 2724 $ownsock{$bgsock} = 'TXT:'.$query; 2725 }; 2726 while ((scalar keys %ownsock) and (@ownready = $ownsel->can_read($postfwd_settings{dns}{timeout}))) { 2727 foreach my $sock (@ownready) { 2728 if (defined $ownsock{$sock}) { 2729 log_info ("[DNSBL] answer for ".$ownsock{$sock}) 2730 if wantsdebug (qw[ all thisrequest ]); 2731 my $packet = $ownres->bgread($sock); 2732 rbl_read_dns ($packet); 2733 delete $ownsock{$sock}; 2734 } else { 2735 $ownsel->remove($sock); 2736 $sock = undef; 2737 }; 2738 }; 2739 }; 2740 }; 2741 2742 # compare dns results 2743 if ( ($myresult[0] > 0) and exists($Rules[$index]{$COMP_RBL_KEY}) ) { 2744 $res = compare_item( 2745 $now, 2746 $COMP_RBL_KEY, 2747 $Rules[$index]{$COMP_RBL_KEY}, 2748 ($Rules[$index]{$COMP_RBL_CNT} ||= 1), 2749 $request->{reverse_address}, 2750 $request 2751 ); 2752 $myresult[0] = ($res or ($Rules[$index]{$COMP_RBL_CNT} eq 'all')) ? ($myresult[0] + $res) : 0; 2753 $myresult[1] = ($res) ? $res : 0; 2754 }; 2755 2756 if ( $has_rhl and ($myresult[0] > 0) ) { 2757 if ( exists($Rules[$index]{$COMP_RHSBL_KEY}) ) { 2758 $res = compare_item( 2759 $now, 2760 $COMP_RHSBL_KEY, 2761 $Rules[$index]{$COMP_RHSBL_KEY}, 2762 ($Rules[$index]{$COMP_RHSBL_CNT} ||= 1), 2763 $request->{client_name}, 2764 $request 2765 ); 2766 $myresult[0] = ($res or ($Rules[$index]{$COMP_RHSBL_CNT} eq 'all')) ? ($myresult[0] + $res) : 0; 2767 $myresult[2] += $res if $res; 2768 }; 2769 if ( exists($Rules[$index]{$COMP_RHSBL_KEY_CLIENT}) ) { 2770 $res = compare_item( 2771 $now, 2772 $COMP_RHSBL_KEY_CLIENT, 2773 $Rules[$index]{$COMP_RHSBL_KEY_CLIENT}, 2774 ($Rules[$index]{$COMP_RHSBL_CNT} ||= 1), 2775 $request->{client_name}, 2776 $request 2777 ); 2778 $myresult[0] = ($res or ($Rules[$index]{$COMP_RHSBL_CNT} eq 'all')) ? ($myresult[0] + $res) : 0; 2779 $myresult[2] += $res if $res; 2780 }; 2781 if ( exists($Rules[$index]{$COMP_RHSBL_KEY_SENDER}) ) { 2782 $res = compare_item( 2783 $now, 2784 $COMP_RHSBL_KEY_SENDER, 2785 $Rules[$index]{$COMP_RHSBL_KEY_SENDER}, 2786 ($Rules[$index]{$COMP_RHSBL_CNT} ||= 1), 2787 $request->{sender_domain}, 2788 $request 2789 ); 2790 $myresult[0] = ($res or ($Rules[$index]{$COMP_RHSBL_CNT} eq 'all')) ? ($myresult[0] + $res) : 0; 2791 $myresult[2] += $res if $res; 2792 }; 2793 if ( exists($Rules[$index]{$COMP_RHSBL_KEY_HELO}) ) { 2794 $res = compare_item( 2795 $now, 2796 $COMP_RHSBL_KEY_HELO, 2797 $Rules[$index]{$COMP_RHSBL_KEY_HELO}, 2798 ($Rules[$index]{$COMP_RHSBL_CNT} ||= 1), 2799 $request->{helo_name}, 2800 $request 2801 ); 2802 $myresult[0] = ($res or ($Rules[$index]{$COMP_RHSBL_CNT} eq 'all')) ? ($myresult[0] + $res) : 0; 2803 $myresult[2] += $res if $res; 2804 }; 2805 if ( exists($Rules[$index]{$COMP_RHSBL_KEY_RCLIENT}) ) { 2806 $res = compare_item( 2807 $now, 2808 $COMP_RHSBL_KEY_RCLIENT, 2809 $Rules[$index]{$COMP_RHSBL_KEY_RCLIENT}, 2810 ($Rules[$index]{$COMP_RHSBL_CNT} ||= 1), 2811 $request->{reverse_client_name}, 2812 $request 2813 ); 2814 $myresult[0] = ($res or ($Rules[$index]{$COMP_RHSBL_CNT} eq 'all')) ? ($myresult[0] + $res) : 0; 2815 $myresult[2] += $res if $res; 2816 }; 2817 }; 2818 }; 2819 if ( wantsdebug (qw[ all thisrequest ]) ) { 2820 $myline = "[RULES] RULE: ".$index." MATCHES: ".((($myresult[0] - 2) > 0) ? ($myresult[0] - 2) : 0); 2821 $myline .= " RBLCOUNT: ".$myresult[1] if $myresult[1]; 2822 $myline .= " RHSBLCOUNT: ".$myresult[2] if $myresult[2]; 2823 $myline .= " DNSBLTEXT: ".(join ("; ", @DNSBL_Text)) if ( (@DNSBL_Text) and (($myresult[1] > 0) or ($myresult[2] > 0)) ); 2824 log_info ($myline); 2825 }; 2826 return @myresult; 2827}; 2828 2829 2830### SUB access policy 2831 2832# access policy routine 2833sub smtpd_access_policy { 2834 my($parent,$request) = @_; 2835 my(@ruleactions) = (); 2836 my($postfixaction) = $postfwd_settings{default}; 2837 my($index) = 1; 2838 my($stop) = 1; 2839 my($now) = time(); 2840 my($date) = join(',', localtime($now)); 2841 my($counters) = "request=1".$postfwd_settings{sepreq}."interval=1"; 2842 my ($rhit) = "ruleset"; 2843 my($matched,$rblcnt,$rhlcnt,$t1,$t2,$t3,$ai) = 0; 2844 my($mykey,$cacheid,$myline,$checkval,$var,$ratehit,$rateindex,$rulehits) = ""; 2845 2846 # save original request 2847 $request->{orig} = hash_to_str ($request); 2848 2849 # replace empty sender with <> 2850 $request->{sender} = '<>' unless ($request->{sender}); 2851 2852 # load postfwd_items attributes 2853 postfwd_items ($request); 2854 2855 # wipe out old group members 2856 if ( $postfwd_settings{group}{noparent} and $Cleanup_Groups and (scalar keys %Group_Cache > 0) and (($now - ($Cleanup_Groups || 0)) > $postfwd_settings{group}{cleanup}) ) { 2857 $t1 = time(); 2858 $t3 = scalar keys %Group_Cache; 2859 cleanup_group_cache($now); 2860 $t2 = time(); 2861 log_info ("[CLEANUP] cleaning group-cache needed ".ts($t2 - $t1) 2862 ." seconds for group cleanup of " 2863 .($t3 - scalar keys %Group_Cache)." out of ".$t3 2864 ." groups after cleanup time ".$postfwd_settings{group}{cleanup}."s") 2865 if ( wantsdebug (qw[ all thisrequest verbose cleanup childcleanup groups ]) or (($t2 - $t1) >= 1) ); 2866 $Cleanup_Groups = $t1; 2867 }; 2868 2869 # clear dnsbl timeout counters 2870 if ( $Cleanup_Timeouts and ($postfwd_settings{dns}{max_interval} > 0) and (($now - $Cleanup_Timeouts) > $postfwd_settings{dns}{max_interval}) ) { 2871 undef %Timeouts; 2872 log_info ("[CLEANUP] clearing dnsbl timeout counters") if wantsdebug (qw[ all thisrequest cleanup verbose ]); 2873 $Cleanup_Timeouts = $now; 2874 }; 2875 2876 # wipe out old cache items 2877 if ( $Cleanup_Rates and ($postfwd_settings{rate}{cleanup} > 0) and (scalar keys %Rate_Cache > 0) and (($now - $Cleanup_Rates) > $postfwd_settings{rate}{cleanup}) ) { 2878 $t1 = time(); 2879 $t3 = scalar keys %Rate_Cache; 2880 cleanup_rate_cache($now); 2881 $t2 = time(); 2882 log_info ("[CLEANUP] cleaning rate-cache needed ".ts($t2 - $t1) 2883 ." seconds for rate cleanup of " 2884 .($t3 - scalar keys %Rate_Cache)." out of ".$t3 2885 ." cached items after cleanup time ".$postfwd_settings{rate}{cleanup}."s") 2886 if ( wantsdebug (qw[ all thisrequest verbose rates cleanup childcleanup ]) or (($t2 - $t1) >= 1) ); 2887 $Cleanup_Rates = $t1; 2888 }; 2889 2890 # Request cache enabled? 2891 if ( $postfwd_settings{request}{ttl} > 0 ) { 2892 # construct cache identifier 2893 if ($postfwd_settings{cacheid}) { 2894 map { $cacheid .= $request->{$_}.';' if (defined $request->{$_}) } @{$postfwd_settings{cacheid}}; 2895 } else { 2896 REQITEM: foreach my $checkreq (sort keys %{$request}) { 2897 next REQITEM unless $request->{$checkreq}; 2898 next REQITEM if %AutoCacheID and not defined $AutoCacheID{lc($checkreq)}; 2899 next REQITEM if ( ($checkreq eq "instance") or ($checkreq eq "queue_id") or ($checkreq eq "orig")); 2900 next REQITEM if ( $postfwd_settings{request}{no_size} and ($checkreq eq "size") ); 2901 next REQITEM if ( $postfwd_settings{request}{no_sender} and ($checkreq eq "sender") ); 2902 if ( $postfwd_settings{request}{rdomain_only} and ($checkreq eq "recipient") ) { 2903 $cacheid .= $request->{recipient_domain}.';'; 2904 } else { 2905 $cacheid .= $request->{$checkreq}.';'; 2906 }; 2907 }; 2908 }; 2909 $cacheid = md5_base64($cacheid) if ($DIGESTMD5 and $postfwd_settings{request}{usemd5} and length($cacheid) > 32); 2910 log_info ("created cache-id: $cacheid") if wantsdebug (qw[ all thisrequest cache ]); 2911 2912 # wipe out old cache entries 2913 if ( $Cleanup_Requests and (scalar keys %Request_Cache > 0) and (($now - $Cleanup_Requests) > $postfwd_settings{request}{cleanup}) ) { 2914 $t1 = time(); 2915 $t3 = scalar keys %Request_Cache; 2916 cleanup_request_cache($now); 2917 $t2 = time(); 2918 log_info ("[CLEANUP] cleaning request-cache needed ".ts($t2 - $t1) 2919 ." seconds for request cleanup of " 2920 .($t3 - scalar keys %Request_Cache)." out of ".$t3 2921 ." cached items after cleanup time ".$postfwd_settings{request}{cleanup}."s") 2922 if ( wantsdebug (qw[ all thisrequest verbose cleanup childcleanup ]) or (($t2 - $t1) >= 1) ); 2923 $Cleanup_Requests = $t1; 2924 }; 2925 }; 2926 2927 # check own cache 2928 if ( ($postfwd_settings{request}{ttl} > 0) 2929 and ((exists($Request_Cache{$cacheid}{$COMP_ACTION})) and ($now <= $Request_Cache{$cacheid}{'until'})) ) { 2930 $counters .= $postfwd_settings{sepreq}."ccache=1"; 2931 $postfixaction = $Request_Cache{$cacheid}{$COMP_ACTION}; 2932 if ( $Request_Cache{$cacheid}{hit} ) { 2933 $Matches{$Request_Cache{$cacheid}{$COMP_ID}}++; 2934 $rulehits = join $postfwd_settings{sepreq}, (split ';', $Request_Cache{$cacheid}{hits}) if $Request_Cache{$cacheid}{hits}; 2935 log_info ("[CACHE] rule=".$Rule_by_ID{$Request_Cache{$cacheid}{$COMP_ID}} 2936 . ", id=".$Request_Cache{$cacheid}{$COMP_ID} 2937 . ( ($request->{queue_id}) ? ", queue=".$request->{queue_id} : '' ) 2938 . ", client=".$request->{client_name}."[".$request->{client_address}."]" 2939 . ( ($request->{sasl_username}) ? ", user=".$request->{sasl_username} : '' ) 2940 . ", sender=<".(($request->{sender} eq '<>') ? "" : $request->{sender}).">" 2941 . ( ($request->{recipient}) ? ", recipient=<".$request->{recipient}.">" : '' ) 2942 . ", helo=<".$request->{helo_name}.">" 2943 . ", proto=".$request->{protocol_name} 2944 . ", state=".$request->{protocol_state} 2945 . ", delay=".ts(time() - $now)."s" 2946 . ", hits=".$Request_Cache{$cacheid}{hits} 2947 . ", action=".$postfixaction 2948 ) unless $postfwd_settings{request}{nolog}; 2949 }; 2950 2951 # check parent cache 2952 } elsif ( ($postfwd_settings{request}{ttl} > 0) 2953 and not($postfwd_settings{request}{noparent}) 2954 and not((my $pans = cache_query ("CMD=".$postfwd_commands{getcacheitem}.";TYPE=request;ITEM=$cacheid")) eq '<undef>') ) { 2955 map { $Request_Cache{$cacheid}{$1} = $2 if m/$postfwd_patterns{keyval}/ } (split $postfwd_settings{sepreq}, $pans); 2956 $counters .= $postfwd_settings{sepreq}."pcache=1"; 2957 $postfixaction = $Request_Cache{$cacheid}{$COMP_ACTION}; 2958 if ( $Request_Cache{$cacheid}{hit} ) { 2959 $Matches{$Request_Cache{$cacheid}{$COMP_ID}}++; 2960 $rulehits = join $postfwd_settings{sepreq}, (split ';', $Request_Cache{$cacheid}{hits}) if $Request_Cache{$cacheid}{hits}; 2961 log_info ("[CACHE] rule=".$Rule_by_ID{$Request_Cache{$cacheid}{$COMP_ID}} 2962 . ", id=".$Request_Cache{$cacheid}{$COMP_ID} 2963 . ( ($request->{queue_id}) ? ", queue=".$request->{queue_id} : '' ) 2964 . ", client=".$request->{client_name}."[".$request->{client_address}."]" 2965 . ( ($request->{sasl_username}) ? ", user=".$request->{sasl_username} : '' ) 2966 . ", sender=<".(($request->{sender} eq '<>') ? "" : $request->{sender}).">" 2967 . ( ($request->{recipient}) ? ", recipient=<".$request->{recipient}.">" : '' ) 2968 . ", helo=<".$request->{helo_name}.">" 2969 . ", proto=".$request->{protocol_name} 2970 . ", state=".$request->{protocol_state} 2971 . ", delay=".ts(time() - $now)."s" 2972 . ", hits=".$Request_Cache{$cacheid}{hits} 2973 . ", action=".$postfixaction 2974 ) unless $postfwd_settings{request}{nolog}; 2975 }; 2976 2977 # check rules 2978 } else { 2979 2980 # refresh config if '-I' was set 2981 read_config(0) if $postfwd_settings{instant}; 2982 2983 if ($#Rules < 0) { 2984 log_note("critical: no rules found - i feel useless (have you set -f or -r?)"); 2985 2986 } else { 2987 2988 # clean up rbl cache 2989 if ( not($postfwd_settings{dns}{disabled}) and (scalar keys %DNS_Cache > 0) and (($now - ($Cleanup_RBLs || 0)) > $postfwd_settings{dns}{cleanup}) ) { 2990 $t1 = time(); 2991 $t3 = scalar keys %DNS_Cache; 2992 cleanup_dns_cache($now); 2993 $t2 = time(); 2994 log_info ("[CLEANUP] cleaning dns-cache needed ".ts($t2 - $t1) 2995 ." seconds for rbl cleanup of " 2996 .($t3 - scalar keys %DNS_Cache)." out of ".$t3 2997 ." cached items after cleanup time ".$postfwd_settings{dns}{cleanup}."s") 2998 if ( wantsdebug (qw[ all thisrequest verbose cleanup childcleanup ]) or (($t2 - $t1) >= 1) ); 2999 $Cleanup_RBLs = $t1; 3000 }; 3001 3002 # prepares hit counters 3003 $request->{$COMP_MATCHES} = 0; 3004 $request->{$COMP_RBL_CNT} = 0; 3005 $request->{$COMP_RHSBL_CNT} = 0; 3006 3007 RULE: for ($index=0;$index<=$#Rules;$index++) { 3008 3009 # compare request against rule 3010 next unless exists $Rules[$index]; 3011 ($matched,$rblcnt,$rhlcnt,my $compcnt) = compare_rule ($now, $index, $date, $request); 3012 3013 # enables/overrides hit counters for later use 3014 $request->{$COMP_MATCHES} = $matched; 3015 $request->{$COMP_RBL_CNT} = $rblcnt; 3016 $request->{$COMP_RHSBL_CNT} = $rhlcnt; 3017 $counters .= $postfwd_settings{sepreq}.$compcnt if $compcnt; 3018 3019 # matched? prepare logline, increase counters 3020 if ($stop = $matched > 0) { 3021 @ruleactions = @{$Rules[$index]{$COMP_ACTION}}; 3022 $Matches{$Rules[$index]{$COMP_ID}}++; 3023 $rulehits .= $postfwd_settings{sepreq} if $rulehits; 3024 $rulehits .= $Rules[$index]{$COMP_ID}; 3025 $request->{$COMP_HITS} .= ';' if (defined $request->{$COMP_HITS}); 3026 $request->{$COMP_HITS} .= $Rules[$index]{$COMP_ID}; 3027 $myline = "rule=".$index 3028 . ", id=".$Rules[$index]{$COMP_ID} 3029 . ( ($request->{queue_id}) ? ", queue=".$request->{queue_id} : '' ) 3030 . ", client=".$request->{client_name}."[".$request->{client_address}."]" 3031 . ( ($request->{sasl_username}) ? ", user=".$request->{sasl_username} : '' ) 3032 . ", sender=<".(($request->{sender} eq '<>') ? "" : $request->{sender}).">" 3033 . ( ($request->{recipient}) ? ", recipient=<".$request->{recipient}.">" : '' ) 3034 . ", helo=<".$request->{helo_name}.">" 3035 . ", proto=".$request->{protocol_name} 3036 . ", state=".$request->{protocol_state}; 3037 3038 # check for postfwd action 3039 ACT: foreach my $act (@ruleactions) { 3040 # substitute check for $$vars in action 3041 $act = $var if ( $var = devar_item ("==",$act,"action",$request) ); 3042 $stop = 1; $ai = 0; # (re)set max_command_recursion counter 3043 while ($ai++ < $postfwd_settings{max_command_recursion} and $act =~ /$COMP_ACTION_MATCH/) { 3044 my($mycmd,$myarg) = ($1, $2); $stop = 0; 3045 if (defined $postfwd_actions{$mycmd}) { 3046 log_info ("[PLUGIN] executing postfwd-action $mycmd") if wantsdebug (qw[ all thisrequest ]); 3047 ($stop, $index, $act, $myline) = &{$postfwd_actions{$mycmd}}($index, $now, $mycmd, $myarg, $myline, $request); 3048 $rhit = "rate" if ($stop and $mycmd =~ /^(rate|size|rcpt)$/); 3049 # substitute again after postfwd-actions 3050 $act = $var if ( $var = devar_item ("==",$act,"action",$request) ); 3051 } else { 3052 log_warn ("[RULES] ".$myline." - error: unknown command \"".$1."\" - ignoring"); 3053 $act = $postfwd_settings{default}; 3054 }; 3055 }; 3056 $postfixaction = $act; 3057 last ACT if $stop; 3058 }; 3059 if ($stop) { 3060 $myline .= ", delay=".ts(time() - $now)."s, hits=".$request->{$COMP_HITS}.", action=".$postfixaction; 3061 log_info ("[RULES] ".$myline) unless $postfwd_settings{request}{nolog}; 3062 $counters .= $postfwd_settings{sepreq}.$rhit."=1"; 3063 # update cache 3064 if ( $postfwd_settings{request}{ttl} > 0 ) { 3065 $Request_Cache{$cacheid}{ttl} = ($Rules[$index]{$COMP_CACHE} || $postfwd_settings{request}{ttl}); 3066 $Request_Cache{$cacheid}{'until'} = $now + $Request_Cache{$cacheid}{ttl}; 3067 $Request_Cache{$cacheid}{$COMP_ACTION} = $postfixaction; 3068 $Request_Cache{$cacheid}{$COMP_ID} = $Rules[$index]{$COMP_ID}; 3069 $Request_Cache{$cacheid}{hit} = $matched; 3070 $Request_Cache{$cacheid}{hits} = $request->{$COMP_HITS}; 3071 cache_query ("CMD=".$postfwd_commands{setcacheitem}.";TYPE=request;ITEM=$cacheid".hash_to_str($Request_Cache{$cacheid})) 3072 unless ($postfwd_settings{request}{noparent}); 3073 }; 3074 last RULE; 3075 }; 3076 } else { undef $myline; }; 3077 }; 3078 }; 3079 }; 3080 # increase counters and return action 3081 if ($postfwd_settings{summary}) { 3082 if (defined $parent) { 3083 print $parent "CMD=".$postfwd_commands{countcache}.";TYPE=$counters" 3084 .(($rulehits) ? $postfwd_settings{seplst}."CMD=".$postfwd_commands{matchcache}.";TYPE=$rulehits" : "") 3085 ."\n"; $parent->getline(); 3086 } else { 3087 count_cache(undef, $counters) if $counters; 3088 match_cache(undef, $rulehits) if $rulehits; 3089 }; 3090 }; 3091 $postfixaction = $postfwd_settings{default} if ($postfwd_settings{test} or !($postfixaction)); 3092 map { log_info (" %$_") } hash_to_list ('Request_Cache', \%Request_Cache) if wantsdebug (qw[ child_cache child_request_cache ]); 3093 map { log_info (" %$_") } hash_to_list ('Rate_Cache', \%Rate_Cache) if wantsdebug (qw[ child_cache child_rate_cache ]); 3094 map { log_info (" %$_") } hash_to_list ('DNS_Cache', \%DNS_Cache) if wantsdebug (qw[ child_cache child_dns_cache ]); 3095 return $postfixaction; 3096}; 3097 3098# increase counters 3099sub count_cache { map { $Count{$1} += $2 if m/$postfwd_patterns{cntval}/ } (split ($postfwd_settings{sepreq}, $_[1])) if $_[1] }; 3100 3101# increase matches 3102sub match_cache { map { $Hits{$_}++ } (split ($postfwd_settings{sepreq}, $_[1])) if $_[1] }; 3103 3104# program usage statistics 3105sub list_stats { 3106 my $now = time(); 3107 my $uptime = $now - $StartTime; 3108 my @output =(); 3109 return @output unless $uptime and $Count{request}; 3110 3111 # averages, hitrates and counters 3112 map { $Count{$_} ||= 0 } qw(ruleset interval top rate pcache ccache dnsqueries dnstimeouts dnsinterval dnstop); 3113 my $lastreq = (($now - $Summary) > 0) ? $Count{interval} / ($now - $Summary) * 60 : 0; 3114 my $lastdns = (($now - $Summary) > 0) ? $Count{dnsinterval} / ($now - $Summary) * 60 : 0; 3115 $Count{top} = $lastreq if $lastreq > $Count{top}; 3116 $Count{dnstop} = $lastdns if $lastdns > $Count{dnstop}; 3117 my $dnstimeoutrate = ($Count{dnsqueries}) ? $Count{dnstimeouts} / $Count{dnsqueries} * 100 : 0; 3118 3119 # log program statistics 3120 if ( not($postfwd_settings{syslog}{noidlestats}) or ($Count{interval} > 0) ) { 3121 push ( @output, sprintf ( 3122 "[STATS] %s::policy %s: %d requests since %d days, %02d:%02d:%02d hours", 3123 $postfwd_settings{name}, 3124 $postfwd_settings{version}, 3125 $Count{request}, 3126 ($uptime / 60 / 60 / 24), 3127 (($uptime / 60 / 60) % 24), 3128 (($uptime / 60) % 60), 3129 ($uptime % 60) 3130 ) ); 3131 3132 push ( @output, sprintf ( 3133 "[STATS] Requests: %.2f/min last, %.2f/min overall, %.2f/min top", 3134 $lastreq, 3135 ($uptime) ? $Count{request} / $uptime * 60 : 0, 3136 $Count{top} 3137 ) ); 3138 3139 push ( @output, sprintf ( 3140 "[STATS] Dnsstats: %.2f/min last, %.2f/min overall, %.2f/min top", 3141 $lastdns, 3142 ($uptime) ? $Count{dnsqueries} / $uptime * 60 : 0, 3143 $Count{dnstop} 3144 ) ) unless ($postfwd_settings{dns}{disable}); 3145 3146 push ( @output, sprintf ( 3147 "[STATS] Hitrates: %.1f%% ruleset, %.1f%% parent, %.1f%% child, %.1f%% rates", 3148 ($Count{request}) ? $Count{ruleset} / $Count{request} * 100 : 0, 3149 ($Count{request}) ? $Count{pcache} / $Count{request} * 100 : 0, 3150 ($Count{request}) ? $Count{ccache} / $Count{request} * 100 : 0, 3151 ($Count{request}) ? $Count{rate} / $Count{request} * 100 : 0 3152 ) ); 3153 3154 push ( @output, sprintf ( 3155 "[STATS] Timeouts: %.1f%% (%d of %d dns queries)", 3156 $dnstimeoutrate, 3157 $Count{dnstimeouts}, 3158 $Count{dnsqueries} 3159 ) ) unless ($postfwd_settings{dns}{disable}); 3160 3161 # per rule stats 3162 if (%Hits and not($postfwd_settings{syslog}{norulestats})) { 3163 my @rulecharts = (sort { ($Hits{$b} || 0) <=> ($Hits{$a} || 0) } (keys %Hits)); my $cntln = length(($Hits{$rulecharts[0]} || 2)) + 2; 3164 map { push ( @output, sprintf ("[STATS] %".$cntln."d matches for id: %s", ($Hits{$_} || 0), $_)) } @rulecharts; 3165 }; 3166 }; 3167 3168 $Count{interval} = $Count{dnsinterval} = 0; 3169 $Summary = $now; 3170 return @output; 3171}; 3172 3173sub set_personality { 3174 @ISA = 'Net::Server::'.$postfwd_settings{personality}; 3175}; 3176 3177## Net::Server::PreFork methods 3178 3179# ignore syslog failures 3180sub handle_syslog_error {}; 3181 3182# reload config on HUP signal 3183sub sig_hup { 3184 my $self = shift; 3185 log_note ("catched HUP signal - reloading ruleset on next request"); 3186 read_config(1); 3187 map { kill ("HUP", $_) } (keys %{$self->{server}->{children}}) if defined $self->{server}->{children}; 3188}; 3189 3190# server end 3191sub post_child_cleanup_hook { 3192 if ($postfwd_settings{personality} eq 'Multiplex') { 3193 save_rates(); 3194 save_groups(); 3195 }; 3196}; 3197 3198# parent start 3199sub pre_loop_hook { 3200 my $self = shift; 3201 # change parent's name 3202 $0 = $self->{server}->{commandline} = " ".$postfwd_settings{name}.'::policy'; 3203 $postfwd_settings{name} .= "/policy"; 3204 # load plugin-items 3205 get_plugins (@{$postfwd_settings{Plugins}}) if $postfwd_settings{Plugins}; 3206 # read configuration 3207 read_config(1); 3208 if ($postfwd_settings{personality} eq 'Multiplex') { 3209 load_rates(); 3210 load_groups(); 3211 }; 3212 $StartTime = $Summary = $Cleanup_Timeouts = $Cleanup_Requests = $Cleanup_RBLs = $Cleanup_Rates = $Cleanup_Groups = time(); 3213 log_info ("ready for input"); 3214}; 3215 3216# parent processes child input 3217sub child_is_talking_hook { 3218 my($self,$sock) = @_; 3219 my $answer = "\n"; 3220 my $msg = $sock->getline(); 3221 # during tests it turned out that children 3222 # send empty messages in some situations 3223 if (defined $msg) { 3224 log_info ("child said '$msg'") if wantsdebug (qw[ all ]); 3225 if ($msg =~ m/$postfwd_patterns{command}/) { 3226 foreach (split $postfwd_settings{seplst}, $msg) { 3227 if (m/$postfwd_patterns{countcache}/) { 3228 $self->count_cache($1); 3229 } elsif (m/$postfwd_patterns{matchcache}/) { 3230 $self->match_cache($1); 3231 } elsif (m/$postfwd_patterns{dumpstats}/) { 3232 $answer = (join $postfwd_settings{sepreq}.$postfwd_settings{seplst}, list_stats())."\n"; 3233 } else { 3234 log_note ("warning: child sent unknown command '$_'"); 3235 }; 3236 }; 3237 } else { 3238 log_note ("warning: child sent unknown message '$msg'"); 3239 }; 3240 }; 3241 print $sock "$answer"; 3242}; 3243 3244# child start 3245sub child_init_hook { 3246 my $self = shift; 3247 # change children's names 3248 $0 = $self->{server}->{commandline} = " ".$postfwd_settings{name}.'::policy::child'; 3249 log_info ("ready for input") if wantsdebug (qw[ all verbose child ]); 3250}; 3251 3252# child process request 3253sub process_request { 3254 my($self) = shift; 3255 my(%attr) = (); 3256 my($client) = $self->{server}->{client}; 3257 my($parent) = $self->{server}->{parent_sock}; 3258 while (<$client>) { 3259 s/\r?\n$//; 3260 # respond to masters ping 3261 if ($_ eq $postfwd_patterns{ping}) { 3262 $client->print("$postfwd_patterns{pong}\n"); 3263 } elsif (m/$postfwd_patterns{dumpstats}/) { 3264 $parent->print("$_\n"); 3265 $client->print($parent->getline()."\n"); 3266 # process input 3267 } else { 3268 process_input ($parent, $client, $_, \%attr); 3269 }; 3270 }; 3271}; 3272 3273# multiplex process request 3274sub mux_input() { 3275 my ($self, $mux, $client, $data) = @_; 3276 my(%attr) = (); 3277 my ($request) = undef; 3278 # check request and print output 3279 while ( $$data =~ s/^([^\r\n]*)\r?\n// ) { 3280 # check request line and print output 3281 next unless defined $1; 3282 $request = $1; 3283 # respond to masters ping 3284 if ($request eq $postfwd_patterns{ping}) { 3285 $client->print("$postfwd_patterns{pong}\n"); 3286 } elsif ($request =~ m/$postfwd_patterns{dumpstats}/) { 3287 $client->print((join $postfwd_settings{sepreq}.$postfwd_settings{seplst}, list_stats())."\n"); 3288 } elsif ($request =~ m/$postfwd_patterns{dumpcache}/) { 3289 $client->print((join $postfwd_settings{sepreq}.$postfwd_settings{seplst}, dump_cache())."\n"); 3290 } elsif ($request =~ m/$postfwd_patterns{delrate}/) { 3291 my $del = $1; $del =~ s/^[%]?//; 3292 my $action; 3293 if (defined $Rate_Cache{$del}) { 3294 delete $Rate_Cache{$del}; 3295 log_info ("[DELRATEITEM] rate cache item '$del' removed"); 3296 $action = "rate cache item '$del' removed"; 3297 } else { 3298 log_info ("[DELRATEITEM] rate cache removal of '$del' failed: item not found"); 3299 $action = "rate cache removal of '$del' failed: item not found"; 3300 }; 3301 $client->print((join $postfwd_settings{sepreq}.$postfwd_settings{seplst}, $action)."\n"); 3302 # process input 3303 } else { 3304 process_input (undef, $client, $request, \%attr); 3305 }; 3306 }; 3307}; 3308 3309# process delegation protocol input 3310sub process_input { 3311 my($parent,$client,$msg,$attr) = @_; 3312 # remember argument=value 3313 if ( $msg =~ /^([^=]{1,512})=(.{0,512})/ ) { 3314 $$attr{$1} = $2; 3315 # evaluate request 3316 } elsif ( $msg eq '' ) { 3317 map { log_info ("Attribute: $_=$$attr{$_}") } (keys %$attr) if wantsdebug (qw[ all thisrequest request ]); 3318 unless ( (defined $$attr{request}) and ($$attr{request} eq "smtpd_access_policy") ) { 3319 log_note ("Ignoring unrecognized request type: '".((defined $$attr{request}) ? substr($$attr{request},0,100) : '')."'"); 3320 } else { 3321 my $action = smtpd_access_policy($parent, \%$attr) || $postfwd_settings{default}; 3322 log_info ("Action: $action") if wantsdebug (qw[ all thisrequest verbose ]); 3323 if ($client) { 3324 print $client ("action=$action\n\n"); 3325 } else { 3326 print STDOUT ("action=$action\n\n"); 3327 }; 3328 %$attr = (); delete $postfwd_settings{debug}{thisrequest}; 3329 }; 3330 # unknown command 3331 } else { 3332 log_note ("Ignoring garbage '".substr($msg, 0, 100)."'"); 3333 }; 3334}; 3335 33361; # EOF postfwd3::server 3337 3338 3339## postfwd::master Start 3340 3341use warnings; 3342use strict; 3343use Getopt::Long 2.25 qw(:config no_ignore_case bundling); 3344use Pod::Usage; 3345use Data::Dumper; 3346# master daemon 3347use Net::Server::Daemonize qw(daemonize); 3348# own modules 3349# program settings, syslogging 3350import postfwd3::basic qw(:DEFAULT %postfwd_commands &check_inet &check_unix &wantsdebug &hash_to_list $TIMEHIRES $NETADDR $DIGESTMD5); 3351# cache daemon (requests, dns, limits), Net::Server::Multiplex 3352import postfwd3::cache qw(); 3353# policy daemon, Net::Server::PreFork 3354import postfwd3::server qw(&read_config &show_config &process_input &get_plugins); 3355 3356 3357# functions to start, override with '--daemons' at command line 3358my @daemons = @{$postfwd_settings{master}{daemons}}; 3359 3360use vars qw( 3361 %options %children %failures 3362); 3363 3364# parse command-line 3365my $Commandargs = ' '.(join ' ', @ARGV); 3366my $Commandline = $0.$Commandargs; 3367# read settings files before loading commandline options ... 3368while ($Commandargs =~ s/\s+\-(\-load(settings)?|F)[=\s]*(\S+)\b//) { load_settings($3) }; 3369# ... and allow commandline options to override 3370GetOptions( \%options, 3371 # Ruleset 3372 'rule|r=s' => sub{ my($opt,$value) = @_; push (@{$postfwd_settings{Configs}}, $opt.$postfwd_settings{sepreq}.$value) }, 3373 'file|f=s' => sub{ my($opt,$value) = @_; push (@{$postfwd_settings{Configs}}, $opt.$postfwd_settings{sepreq}.$value) }, 3374 "loadsettings|load|F=s", 3375 "showsettings|savesettings|settings|save:s", 3376 'scores|score|s=s%' => \%{$postfwd_settings{scores}}, 3377 "test|t" => \$postfwd_settings{test}, 3378 "instantcfg|I" => \$postfwd_settings{instant}, 3379 "config_timeout=i" => \$postfwd_settings{timeout}{config}, 3380 "no_netaddr", 3381 "no_netcidr", 3382 "cidr_method=s", 3383 "aggregate_addrs|A!", 3384 "showconfig|C", 3385 "defaults|D", 3386 # Networking 3387 "umask=s" => \$postfwd_settings{base}{umask}, 3388 "user|u=s" => \$postfwd_settings{base}{user}, 3389 "group|g=s" => \$postfwd_settings{base}{group}, 3390 "server_socket|socket=s" => sub{ ($postfwd_settings{server}{proto}, $postfwd_settings{server}{host}, $postfwd_settings{server}{port}) = (split ':', $_[1]) }, 3391 "interface|i=s" => \$postfwd_settings{server}{host}, 3392 "port|p=s" => \$postfwd_settings{server}{port}, 3393 "proto=s" => \$postfwd_settings{server}{proto}, 3394 "server_umask=s" => \$postfwd_settings{server}{umask}, 3395 "min_servers=i" => \$postfwd_settings{server}{min_servers}, 3396 "max_servers=i" => \$postfwd_settings{server}{max_servers}, 3397 "min_spare_servers=i" => \$postfwd_settings{server}{min_spare_servers}, 3398 "max_spare_servers=i" => \$postfwd_settings{server}{max_spare_servers}, 3399 "nodns|n" => \$postfwd_settings{dns}{disabled}, 3400 "dns_timeout=i" => \$postfwd_settings{dns}{timeout}, 3401 "dns_async_txt" => \$postfwd_settings{dns}{async_txt}, 3402 "dns_timeout_max=i" => \$postfwd_settings{dns}{max_timeout}, 3403 "dns_timeout_interval=i" => \$postfwd_settings{dns}{max_interval}, 3404 "dns_max_ns_lookups=i" => \$postfwd_settings{dns}{max_ns_lookups}, 3405 "dns_max_mx_lookups=i" => \$postfwd_settings{dns}{max_mx_lookups}, 3406 "ipv6_dnsbl" => \$postfwd_settings{dns}{ipv6_dnsbl}, 3407 "cache-rbl-timeout=i" => \$postfwd_settings{dns}{ttl}, 3408 "cache-rbl-default=s" => \$postfwd_settings{dns}{mask}, 3409 "cleanup-rbls=i" => \$postfwd_settings{dns}{cleanup}, 3410 "no_parent_dns_cache" => \$postfwd_settings{dns}{noparent}, 3411 "parent_dns_cache" => sub { $postfwd_settings{dns}{noparent} = 0 }, 3412 # Stats 3413 "summary|stats|S=i" => \$postfwd_settings{summary}, 3414 "norulestats" => \$postfwd_settings{syslog}{norulestats}, 3415 "no-rulestats" => \$postfwd_settings{syslog}{norulestats}, 3416 "noidlestats" => \$postfwd_settings{syslog}{noidlestats}, 3417 "no-idlestats" => \$postfwd_settings{syslog}{noidlestats}, 3418 "stdoutlog|stdout|L" => \$postfwd_settings{syslog}{stdout}, 3419 "stdin" => \$postfwd_settings{syslog}{stdin}, 3420 "commandline|command|cmd" => sub{ $postfwd_settings{syslog}{stdin} = $postfwd_settings{syslog}{stdout} = 1; $postfwd_settings{daemon} = 0 }, 3421 # Cache 3422 "cache_socket=s" => sub{ ($postfwd_settings{cache}{proto}, $postfwd_settings{cache}{host}, $postfwd_settings{cache}{port}) = (split ':', $_[1]) }, 3423 "cache_interface=s" => \$postfwd_settings{cache}{host}, 3424 "cache_port=s" => \$postfwd_settings{cache}{port}, 3425 "cache_proto=s" => \$postfwd_settings{cache}{proto}, 3426 "cache_umask=s" => \$postfwd_settings{server}{umask}, 3427 "cache|c=i" => \$postfwd_settings{request}{ttl}, 3428 "cacheid=s" => sub { push @{$postfwd_settings{cacheid}}, (split /[,\s]+/, $_[1]) }, 3429 "cacheid_md5!" => \$postfwd_settings{request}{usemd5}, 3430 "cache-rdomain-only" => \$postfwd_settings{request}{rdomain_only}, 3431 "cache-no-sender" => \$postfwd_settings{request}{no_sender}, 3432 "cache-no-size" => \$postfwd_settings{request}{no_size}, 3433 "cleanup-requests=i" => \$postfwd_settings{request}{cleanup}, 3434 "no_parent_request_cache" => \$postfwd_settings{request}{noparent}, 3435 "no_parent_rate_cache" => \$postfwd_settings{rate}{noparent}, 3436 "no_parent_cache" => sub{ $postfwd_settings{request}{noparent} = $postfwd_settings{rate}{noparent} = $postfwd_settings{dns}{noparent} = $postfwd_settings{group}{noparent} = 1 }, 3437 # Groups 3438 "no_parent_group_cache" => \$postfwd_settings{group}{noparent}, 3439 "default_group_ttl=i" => \$postfwd_settings{group}{ttl}, 3440 "group_maxitems=i" => \$postfwd_settings{group}{maxitems}, 3441 # Limits 3442 "cleanup-rates=i" => \$postfwd_settings{rate}{cleanup}, 3443 "keep_rates|keep_limits|keep_rates_on_reload" => \$postfwd_settings{keep_rates}, 3444 "keep_groups|keep_groups_on_reload" => \$postfwd_settings{keep_groups}, 3445 "save_rates|save_limits|save_rates_on_restart=s" => \$postfwd_settings{rate}{store}, 3446 "save_groups|save_groups_on_restart=s" => \$postfwd_settings{group}{store}, 3447 "fast_limit_evaluation", 3448 # Control 3449 'version|V' => sub{ print "$postfwd_settings{name} $postfwd_settings{version} (Net::DNS ".(Net::DNS->VERSION || '<undef>').", Net::Server ".(Net::Server->VERSION || '<undef>').(($NETADDR) ? ", NetAddr::IP $NETADDR" : '').(($NETCIDR) ? ", Net::CIDR::Lite $NETCIDR" : '').(($DIGESTMD5) ? ", Digest::MD5 $DIGESTMD5" : '').", Sys::Syslog ".($Sys::Syslog::VERSION || '<undef>').", ".(($TIMEHIRES) ? "Time::HiRes $TIMEHIRES, " : '').(($STORABLE) ? "Storable $STORABLE, " : '')."Perl ".$]." on ".$^O.")\n"; exit; }, 3450 'versionshort|shortversion' => sub{ print "$postfwd_settings{version}\n"; exit; }, 3451 'manual|m' => sub{ # contructing command string (de-tainting $0) 3452 $postfwd_settings{manual} .= ($0 =~ /^([-\@\/\w. ]+)$/) ? " \"".$1 : " \"".$postfwd_settings{name}; 3453 $postfwd_settings{manual} .= "\" | ".$postfwd_settings{pager}; 3454 system ($postfwd_settings{manual}); exit; }, 3455 "term|kill|stop|k", 3456 "hup|reload", 3457 "delcache=s", 3458 "delrate=s", 3459 "dumpcache", 3460 "dumpstats", 3461 "pid|pidfile|pid_file=s" => \$postfwd_settings{master}{pid_file}, 3462 "watchdog=i" => \$postfwd_settings{master}{watchdog}, 3463 "respawn=i" => \$postfwd_settings{master}{respawn}, 3464 "failures=i" => \$postfwd_settings{master}{failures}, 3465 "daemon|d!" => \$postfwd_settings{daemon}, 3466 "daemons=s" => sub { push @{$options{daemons}}, (split /[,\s]+/, $_[1]) }, 3467 "personality=s", 3468 "autopersonality!" => \$postfwd_settings{autopersonality}, 3469 "v1|postfwd1" => sub { $options{personality} = 'Multiplex' }, 3470 "v2|postfwd3" => sub { $options{personality} = 'PreFork' }, 3471 "chroot|R=s" => \$postfwd_settings{chroot}, 3472 # Logging 3473 "debug=s" => sub { push @{$options{debug}}, (split /[,\s]+/, $_[1]) }, 3474 "debugclasses" => sub { my %tags = (); my $pfwd = undef; open ($pfwd, '<', $0) or die "Can not open '$0': $! - $@\n\n"; while (<$pfwd>) { chomp; next unless /wantsdebug\s+\(\s*qw\[\s*([^\]]+)\]/; my $cstr = $1; map { $tags{$_}++ } (split /\s+/, $cstr) }; close($pfwd); print STDERR "\n HOOKS NAME\n ----- ----\n"; map { printf STDERR "%6d %s\n", $tags{$_}, $_ } (sort keys %tags); print STDERR "\n"; exit }, 3475 "verbose|v+" => \$postfwd_settings{verbose}, 3476 "logname|l=s" => sub{ $postfwd_settings{name} = $_[1]; 3477 $postfwd_settings{cache}{syslog_ident} = $_[1].'/cache'; 3478 $postfwd_settings{server}{syslog_ident} = $_[1].'/policy'; }, 3479 "facility=s" => \$postfwd_settings{syslog}{facility}, 3480 "socktype=s" => \$postfwd_settings{syslog}{socktype}, 3481 "nodnslog" => \$postfwd_settings{dns}{nolog}, 3482 "no-dnslog" => \$postfwd_settings{dns}{nolog}, 3483 "anydnslog" => \$postfwd_settings{dns}{anylog}, 3484 "norulelog" => \$postfwd_settings{request}{nolog}, 3485 "no-rulelog" => \$postfwd_settings{request}{nolog}, 3486 "perfmon|P" => \$postfwd_settings{syslog}{nolog}, 3487 "plugins=s" => sub { push @{$postfwd_settings{Plugins}}, $_[1] }, 3488 3489 # Unused 3490 "start", 3491 "shortlog", 3492 "dns_queuesize=i", 3493 "dns_retries=i", 3494) or pod2usage (-msg => "\nPlease see \"".$postfwd_settings{name}." -m\" for detailed instructions.\n", -verbose => 1); 3495 3496# basic syntax checks 3497if ($postfwd_settings{verbose} > 1) { 3498 $postfwd_settings{debug}{all} = 1; 3499} elsif ($postfwd_settings{verbose}) { 3500 $postfwd_settings{debug}{verbose} = 1; 3501}; 3502# set personality by argument or ... 3503if (defined $options{personality}) { 3504 $postfwd_settings{personality} = $options{personality}; 3505# ... determine personality by program name to keep old scripts compatible 3506} elsif ($postfwd_settings{autopersonality}) { 3507 $postfwd_settings{personality} = 'Multiplex' if $0 =~ /postfwd[1]?$/i; 3508 $postfwd_settings{personality} = 'PreFork' if $0 =~ /postfwd2$/i; 3509}; 3510die "\nERROR: Bad personality type! --personality must be 'Multiplex' or 'Prefork'. You can use --v1 or --v2 as shortcut.\n\n" unless $postfwd_settings{personality} =~ /^(Multiplex|PreFork)$/; 3511if ($postfwd_settings{personality} eq 'Multiplex') { 3512 $postfwd_settings{request}{noparent} = $postfwd_settings{rate}{noparent} = $postfwd_settings{dns}{noparent} = $postfwd_settings{group}{noparent} = 1; 3513 @{$postfwd_settings{master}{daemons}} = @daemons = qw( server ); 3514}; 3515map { $postfwd_settings{debug}{$_} = 1 } uniq(@{$options{debug}}); 3516map { $postfwd_settings{daemons}{$_} = 1 } ((defined $options{daemons}) ? uniq(@{$options{daemons}}) : uniq(@daemons)); 3517map { $postfwd_settings{$_}{check} = ($postfwd_settings{$_}{proto} eq 'unix') ? \&check_unix : \&check_inet } @daemons; 3518 3519# de-taint command-line 3520%postfwd_settings = detaint_hash (%postfwd_settings); 3521 3522# check for commands (e.g. 'postfwd stop') 3523if (@ARGV) { 3524 if ($ARGV[0] =~ /^(term|kill|stop)$/i) { 3525 $options{'term'} = 1; 3526 } elsif ($ARGV[0] =~ /^(hup|reload)$/i) { 3527 $options{'hup'} = 1; 3528 } elsif ($ARGV[0] =~ /^(defaults|dumpcache|dumpstats)$/i) { 3529 $options{$1} = 1; 3530 } elsif ($ARGV[0] =~ /^(showsettings|settings)$/i) { 3531 $options{showsettings} = ' '; 3532 } elsif ($ARGV[0] =~ /^(delcache|delrate|debugclasses)$/i) { 3533 my $a = $1; 3534 (defined $ARGV[1] and $ARGV[1] =~ /^(.*)$/) 3535 ? $options{$a} = $1 3536 : die "\nCommand '$a' needs an argument!\n\n"; 3537 } elsif ($ARGV[0] =~ /^(loadsettings|load)$/i) { 3538 my $a = 'loadsettings'; 3539 (defined $ARGV[1] and $ARGV[1] =~ /^(.*)$/) 3540 ? $options{$a} = $1 3541 : die "\nCommand '$a' needs an argument!\n\n"; 3542 } elsif ($ARGV[0] =~ /^(savesettings|save)$/i) { 3543 my $a = 'showsettings'; 3544 (defined $ARGV[1] and $ARGV[1] =~ /^(.*)$/) 3545 ? $options{$a} = $1 3546 : die "\nCommand '$a' needs an argument!\n\n"; 3547 } else { 3548 die "\nUnknown command '".$ARGV[0]."'\n\n" 3549 unless $ARGV[0] =~ /^(start|run)$/i; 3550 }; 3551}; 3552map { $postfwd_settings{syslog}{stdout} = 1 if defined $options{$_} } qw(term hup showconfig dumpcache dumpstats defaults delcache delrate debugclasses); 3553 3554# save options 3555%{$postfwd_settings{Options}} = %options; 3556 3557# terminate at -k or --kill 3558if (defined $options{'term'}) { 3559 kill "TERM", get_master_pid(); 3560 exit (0); 3561# reload at --reload 3562} elsif (defined $options{'hup'}) { 3563 kill "HUP", get_master_pid(); 3564 exit (0); 3565}; 3566 3567# chroot master 3568if (defined $postfwd_settings{chroot}) { 3569 unless (eval {chroot($postfwd_settings{chroot});}) { 3570 print "Cannot chroot to $postfwd_settings{chroot}: $!\n"; 3571 exit (2); 3572 }; 3573}; 3574 3575# init_log 3576init_log ($postfwd_settings{name}."/master"); 3577log_info ("Loaded program settings from '".$postfwd_settings{Settings}."'") if defined $postfwd_settings{Settings}; 3578 3579# determine CIDR functions 3580$NETADDR = 0 if $options{'no_netaddr'}; 3581$NETCIDR = 0 if $options{'no_netcidr'}; 3582if ($options{'cidr_method'} and $options{'cidr_method'} =~ /^(netcidr|netaddr|postfwd)$/i) { 3583 $postfwd_settings{cidr_method} = lc($1); 3584 if ( 3585 ( not($NETCIDR) and $postfwd_settings{cidr_method} eq 'netcidr') 3586 or 3587 ( not($NETADDR) and $postfwd_settings{cidr_method} eq 'netaddr') 3588 ) { 3589 log_note("Can not use method '".$postfwd_settings{cidr_method}."' for network checks. Module not available"); 3590 $postfwd_settings{cidr_method} = 'postfwd'; 3591 }; 3592} else { 3593 # prefer Net::CIDR::Lite, because it's a bit faster (v4 and v6 cidr) 3594 if ($NETCIDR) { 3595 $postfwd_settings{cidr_method} = 'netcidr'; 3596 # else use NetAddr::IP (v4 and v6 cidr) 3597 } elsif ($NETADDR) { 3598 $postfwd_settings{cidr_method} = 'netaddr'; 3599 # else use old builtin method (v4 cidr, v6 regex) 3600 } else { 3601 $postfwd_settings{cidr_method} = 'postfwd'; 3602 }; 3603}; 3604if (defined $postfwd_compare{'cidr_'.$postfwd_settings{cidr_method}}) { 3605 log_info("Using 'cidr_".$postfwd_settings{cidr_method}."'-method for network checks") if wantsdebug (qw[ all verbose cidr ]); 3606 $postfwd_compare{cidr} = $postfwd_compare{'cidr_'.$postfwd_settings{cidr_method}}; 3607} else { 3608 log_note("Unknown method 'cidr_".$postfwd_settings{cidr_method}."' for network checks, reverting to 'cidr_postfwd'"); 3609 $postfwd_compare{cidr} = $postfwd_compare{cidr_postfwd}; 3610}; 3611 3612# check for Network aggregation via NetAddr::IP 3613if ($options{aggregate_addrs}) { 3614 if ($NETADDR) { 3615 log_info("Module NetAddr::IP found, aggregating CIDR notated lists") if wantsdebug (qw[ all verbose aggr cidr ]); 3616 $postfwd_settings{aggregate_addrs} = ($options{aggregate_addrs} || 0); 3617 } else { 3618 log_note("Module NetAddr::IP not found, ignoring options --aggregate_addrs"); 3619 $postfwd_settings{aggregate_addrs} = 0; 3620 }; 3621}; 3622 3623# check for Storable 3624if (($postfwd_settings{rate}{keep} or $postfwd_settings{group}{keep}) and not($STORABLE)) { 3625 log_note("Module Storable not found, ignoring options --save_rates and --save_groups"); 3626 $postfwd_settings{rate}{keep} = 0; $postfwd_settings{group}{keep} = 0; 3627}; 3628 3629# check for Digest::MD5 3630if ($postfwd_settings{request}{usemd5} and not($DIGESTMD5)) { 3631 log_note("Module Digest::MD5 not found, ignoring option --cache_md5"); 3632 $postfwd_settings{request}{usemd5} = 0; 3633}; 3634 3635# read and display configuration 3636if (defined $options{'showconfig'}) { 3637 read_config(1); 3638 show_config(); 3639 exit; 3640}; 3641 3642# show program settings 3643if (defined $options{'defaults'}) { 3644 print "\n"; map { print " %$_\n" } hash_to_list ('postfwd_settings', \%postfwd_settings); 3645 if (wantsdebug (qw[ all verbose ])) { 3646 map { print " %$_\n" } hash_to_list ('postfwd_commands', \%postfwd_commands); 3647 map { print " %$_\n" } hash_to_list ('postfwd_patterns', \%postfwd_patterns); 3648 }; 3649 print "\n"; exit; 3650}; 3651 3652# dump stats 3653if (defined $options{'dumpstats'}) { 3654 foreach my $daemon (sort keys %{$postfwd_settings{daemons}}) { 3655 print "\n"; 3656 map { print ("$_\n") } get_stats ($daemon); 3657 }; 3658 print "\n"; 3659 exit; 3660}; 3661 3662# dump cache contents 3663if (defined $options{'dumpcache'}) { 3664 my $dctarget = (defined $postfwd_settings{daemons}{cache}) ? 'cache' : 'server'; 3665 print "\n".( join "\n", 3666 split $postfwd_settings{sepreq}.$postfwd_settings{seplst}, 3667# (&{$postfwd_settings{cache}{check}} ('cache', 'CMD=DC;') || '<undef>') 3668 (&{$postfwd_settings{$dctarget}{check}} ($dctarget, 'CMD=DC;') || '<undef>') 3669 )."\n\n"; 3670 exit; 3671}; 3672 3673# remove cache item 3674if (defined $options{'delcache'} or defined $options{'delrate'}) { 3675 my $dctarget = (defined $postfwd_settings{daemons}{cache}) ? 'cache' : 'server'; 3676 print "\n".( join "\n", 3677 split $postfwd_settings{sepreq}.$postfwd_settings{seplst}, 3678 (&{$postfwd_settings{$dctarget}{check}} ($dctarget, (($options{'delcache'}) ? 'CMD=RC '.$options{'delcache'} : 'CMD=RR '.$options{'delrate'})) || '<undef>') 3679 )."\n\n"; 3680 exit; 3681}; 3682 3683# save configuration 3684if (defined $options{'showsettings'}) { 3685 print STDERR "Saving program settings to file '".$options{'showsettings'}."'...\n" if $options{'showsettings'}; 3686 save_settings($options{'showsettings'}); 3687 exit; 3688}; 3689 3690# -n - skip dns based checks 3691log_note ("NODNS: set - will skip all dns based checks") if $postfwd_settings{dns}{disabled}; 3692 3693# daemonize master 3694unless ($postfwd_settings{daemon}) { 3695 $postfwd_settings{base}{setsid} = 0; 3696}; 3697log_info ($postfwd_settings{name}." " 3698 .$postfwd_settings{version}." starting [daemons: ".(join ',', sort keys %{$postfwd_settings{daemons}})."]" 3699 .((scalar keys %{$postfwd_settings{debug}}) ? " with debug levels: ".(join ',', keys %{$postfwd_settings{debug}}) : '')) unless $postfwd_settings{syslog}{stdin}; 3700log_info ("Net::DNS ".(Net::DNS->VERSION || '<undef>').", Net::Server ".(Net::Server->VERSION || '<undef>').(($NETADDR) ? ", NetAddr::IP $NETADDR" : '').(($NETCIDR) ? ", Net::CIDR::Lite $NETCIDR" : '').(($DIGESTMD5) ? ", Digest::MD5 $DIGESTMD5" : '').", Sys::Syslog ".($Sys::Syslog::VERSION || '<undef>').", ".(($TIMEHIRES) ? "Time::HiRes $TIMEHIRES, " : '').(($STORABLE) ? "Storable $STORABLE, " : '')."Perl ".$]." on ".$^O) if wantsdebug (qw[ all verbose ]); 3701umask oct($postfwd_settings{base}{umask}); 3702daemonize($postfwd_settings{base}{user}, $postfwd_settings{base}{group}, $postfwd_settings{master}{pid_file}) if $postfwd_settings{daemon}; 3703$0 = $Commandline; 3704 3705# prepare shared SIG handlers 3706$SIG{__WARN__} = sub { log_warn("warning: $_[0]") }; 3707$SIG{__DIE__} = sub { log_crit("FATAL: $_[0]"); die @_; }; 3708 3709# check for --stdin option 3710if ($postfwd_settings{syslog}{stdin}) { 3711 my(%attr) = (); 3712 get_plugins (@{$postfwd_settings{Plugins}}) if $postfwd_settings{Plugins}; 3713 read_config(1); 3714 map { $postfwd_settings{daemons}{$_} = 0 } (keys %{$postfwd_settings{daemons}}); 3715 $postfwd_settings{request}{noparent} = $postfwd_settings{rate}{noparent} = $postfwd_settings{dns}{noparent} = $postfwd_settings{group}{noparent} = 1; 3716 while (<>) { 3717 chomp; 3718 process_input (undef, undef, $_, \%attr); 3719 }; 3720 exit; 3721}; 3722 3723# fork daemons: cache and server 3724umask oct($postfwd_settings{base}{umask}); 3725foreach my $daemon (sort keys %{$postfwd_settings{daemons}}) { 3726 umask oct($postfwd_settings{$daemon}{umask}); 3727 if (my $pid = spawn_daemon ($daemon)) { 3728 log_info ("Started $daemon at pid $pid"); 3729 $children{$daemon} = $pid; 3730 }; 3731}; 3732$postfwd_settings{name} .= "/master"; 3733 3734# prepare master SIG handlers and enter main loop 3735$SIG{TERM} = sub { end_program(); }; 3736$SIG{INT} = sub { end_program(); } unless ($postfwd_settings{daemon}); 3737$SIG{HUP} = sub { reload_program(); }; 3738if ($postfwd_settings{summary}) { 3739 $SIG{ALRM} = sub { 3740 log_stats(); 3741 alarm ($postfwd_settings{summary}) 3742 }; 3743 alarm ($postfwd_settings{summary}); 3744}; 3745 3746while (1) { 3747 # check daemons every <watchdog> seconds 3748 if ($postfwd_settings{master}{watchdog}) { 3749 sleep ($postfwd_settings{master}{watchdog}); 3750 foreach my $daemon (sort keys %{$postfwd_settings{daemons}}) { 3751 if (check_daemon ($daemon)) { 3752 $failures{$daemon} = 0; 3753 } else { 3754 if (++$failures{$daemon} >= $postfwd_settings{master}{failures}) { 3755 # terminate program 3756 log_crit ("$daemon-daemon check failed $failures{$daemon} times - terminating program"); 3757 end_program(); 3758 } else { 3759 # restart daemon 3760 log_crit ("$daemon-daemon check failed $failures{$daemon} times - respawning in ".$postfwd_settings{master}{respawn}." seconds"); 3761 kill 15, $children{$daemon}; sleep $postfwd_settings{master}{respawn}; 3762 if (my $pid = spawn_daemon ($daemon)) { 3763 log_info ("Started $daemon at pid $pid"); 3764 $children{$daemon} = $pid; 3765 }; 3766 }; 3767 }; 3768 }; 3769 # no watchdog -> sleep until signal 3770 } else { 3771 sleep; 3772 }; 3773}; 3774die "master-daemon: should never see me!\n"; 3775 3776 3777## SUBS 3778 3779# cleanup children and files and terminate 3780sub end_program { 3781 # ignore further TERM signals 3782 $SIG{TERM} = 'IGNORE'; $terminating = 1; 3783 if ($postfwd_settings{summary}) { 3784 undef $postfwd_settings{syslog}{noidlestats}; 3785 log_stats(); 3786 }; 3787 log_note ($postfwd_settings{name}." ".$postfwd_settings{version}." terminating..."); 3788 unlink $postfwd_settings{master}{pid_file} if (-T $postfwd_settings{master}{pid_file}); 3789 # negative signal no. kills the whole process group 3790 kill -15, $$; 3791 exit (0); 3792}; 3793 3794# send hup to child processes 3795sub reload_program { 3796 log_note ($postfwd_settings{name}." ".$postfwd_settings{version}." reloading..."); 3797 map { kill 1, $_ } (values %children) if %children; 3798}; 3799 3800# check a cache or server daemon 3801sub check_daemon { return ((&{$postfwd_settings{$_[0]}{check}}($_[0],$postfwd_patterns{ping}) || '') eq $postfwd_patterns{pong}) }; 3802 3803# spawn a cache or server daemon 3804sub spawn_daemon { 3805 my ($type) = @_; 3806 my $pid = fork(); 3807 die "Can not fork $type: $!\n" unless defined $pid; 3808 if ($pid == 0) { 3809 my %service = %{$postfwd_settings{$type}}; 3810 # Net::Server dies when a unix domain socket without dot "." is used 3811 $service{port} .= '|unix' if (($service{proto} eq 'unix') and not($service{port} =~ /\|unix$/)); 3812 $service{syslog_logopt} = $postfwd_settings{syslog}{options} if defined $postfwd_settings{syslog}{options}; 3813 $service{syslog_logsock} = $postfwd_settings{syslog}{socktype} if defined $postfwd_settings{syslog}{socktype}; 3814 $service{syslog_facility} = $postfwd_settings{syslog}{facility} if defined $postfwd_settings{syslog}{facility}; 3815 my %daemonopts = (%{$postfwd_settings{base}}, %service); 3816 map { log_info("spawn $_") } (hash_to_list($type, \%daemonopts)) if wantsdebug (qw[ all verbose daemon spawn ]); 3817 my $daemon = bless { server => { %daemonopts } }, "postfwd3::$type"; 3818 $daemon->set_personality(); 3819 $daemon->run(); 3820 die "$type-daemon: should never see me!\n"; 3821 }; 3822 return $pid; 3823}; 3824 3825# get pid of running master process 3826sub get_master_pid { 3827 (-e $postfwd_settings{master}{pid_file}) or die $postfwd_settings{name}.": Can not find pid_file ".$postfwd_settings{master}{pid_file}.": $!\n"; 3828 (-T $postfwd_settings{master}{pid_file}) or die $postfwd_settings{name}.": Can not open pid_file ".$postfwd_settings{master}{pid_file}.": not a textfile\n"; 3829 open my $pidf, '<', $postfwd_settings{master}{pid_file} or die $postfwd_settings{name}.": Can open pid_file ".$postfwd_settings{master}{pid_file}.": $!\n"; 3830 my $pid = <$pidf>; 3831 close($pidf); 3832 ($pid =~ m/^(\d+)$/) or die $postfwd_settings{name}.": Invalid pid_file content '$pid' (pid_file ".$postfwd_settings{master}{pid_file}.")\n"; 3833 return $1; 3834}; 3835 3836# merge default and loaded program settings 3837sub merge_settings { 3838 my($left, $right) = @_; 3839 foreach my $k (keys %{$right}) { 3840 my $r = ref $right->{$k}; 3841 if ($r eq 'HASH') { 3842 unless (defined $left->{$k} and %{$left->{$k}}) { 3843 %{$left->{$k}} = %{$right->{$k}}; 3844 } else { 3845 merge_settings($left->{$k}, $right->{$k}); 3846 }; 3847 } elsif ($r eq 'ARRAY') { 3848 @{$left->{$k}} = @{$right->{$k}}; 3849 } else { 3850 $left->{$k} = $right->{$k}; 3851 }; 3852 }; 3853}; 3854 3855# cleanup program settings 3856sub clean_settings { 3857 my %set = @_; 3858 return () unless %set; 3859 map { delete $set{base}{$_} if defined $set{base}{$_} } qw( log_file log_level no_client_stdout ); 3860 map { delete $set{syslog}{$_} if defined $set{syslog}{$_} } qw( logger ); 3861 map { delete $set{server}{$_} if defined $set{server}{$_} } qw( check child_communication leave_children_open_on_hup ); 3862 map { delete $set{cache}{$_} if defined $set{cache}{$_} } qw( check ); 3863 map { delete $set{Options}{$_} if defined $set{Options}{$_} } qw( term hup showconfig dumpcache dumpstats defaults delcache delrate loadsettings savesettings showsettings debugclasses ); 3864 map { delete $set{$_} if defined $set{$_} } qw( Settings manual version seplim seplst sepreq ); 3865 return %set; 3866}; 3867 3868# save program settings 3869sub save_settings { 3870 my $fil = shift; 3871 my $fh = *STDOUT; 3872 my %set = clean_settings(%postfwd_settings); 3873 local $Data::Dumper::Indent = $postfwd_settings{dumper}{Indent}; 3874 local $Data::Dumper::Purity = $postfwd_settings{dumper}{Purity}; 3875 local $Data::Dumper::Quotekeys = $postfwd_settings{dumper}{Quotekeys}; 3876 local $Data::Dumper::Sortkeys = $postfwd_settings{dumper}{Sortkeys}; 3877 local $Data::Dumper::Terse = $postfwd_settings{dumper}{Terse}; 3878 open $fh, '>', $1 or die "\nERROR: Can not write to file '$1': $! - $@\n\n" if $fil =~ /^\s*(\S.*?)\s*$/; 3879 print $fh "#\n# postfwd settings file\n#\n"; 3880 print $fh "# Version: $NAME $VERSION (Perl ".$].", Data::Dumper ".Data::Dumper->VERSION.")\n"; 3881 print $fh "# Command: ".$NAME.$Commandargs."\n#\n"; 3882 print $fh Dumper(\%set); 3883 close($fh) if $fil; 3884}; 3885 3886# show program settings 3887sub load_settings { 3888 my $fil = shift; 3889 my %set = (); my $dat = undef; 3890 unless (open ($dat, '<', $fil)) { 3891 warn "\nCan not read settings from '$fil': $! - $@\n\n"; 3892 return; 3893 }; 3894 my @slurp = <$dat>; 3895 close($dat); 3896 unless (@slurp) { 3897 warn "\nNo valid settings found in '$fil'\n\n"; 3898 return; 3899 }; 3900 map { $_ = $1 if /^(.*)$/; $_ = '' if /^\s*#/ } @slurp; 3901 { 3902 local $SIG{'__DIE__'}; 3903 %set = %{ eval(join ' ', @slurp) }; 3904 }; 3905 if ($@) { 3906 warn "\nError loading settings from '$fil': $@\n\n"; 3907 return; 3908 }; 3909 %set = clean_settings(%set) if %set; 3910 unless (%set) { 3911 warn "\nNo valid settings found in '$fil'\n\n"; 3912 return; 3913 }; 3914 delete $options{loadsettings} if defined $options{loadsettings}; 3915 merge_settings( \%postfwd_settings, \%set); 3916 merge_settings( \%options, $postfwd_settings{Options}); 3917 $postfwd_settings{Settings} = $fil; 3918}; 3919 3920# detaints postfwd3 settings 3921sub detaint_hash { 3922 my (%request) = @_; 3923 # cycle through key=value pairs 3924 while ( my($s, $v) = each %request ) { 3925 my $r = ref $v; 3926 # type hash: recursively call ourself 3927 if ($r eq 'HASH') { 3928 %{$v} = detaint_hash ( %{$v} ); 3929 # type array: detaint whole list 3930 } elsif ($r eq 'ARRAY') { 3931 @{$request{$s}} = map { $_ = (($_ =~ m/^(.*)$/) ? $1 : $_ ) if $_ } @{$v}; 3932 # type scalar: detaint argument 3933 } elsif ($r eq '') { 3934 $request{$s} = (($v =~ m/^(.*)$/) ? $1 : $v) if ($s and $v); 3935 }; 3936 }; 3937 return %request; 3938}; 3939 3940# send stats to syslog 3941sub log_stats { map { log_note ("$_") unless ($_ eq '<undef>') } get_stats(sort keys %{$postfwd_settings{daemons}}); }; 3942 3943# retrieve status from children 3944sub get_stats { 3945 my @daemons = @_; my @output = (); 3946 map { push @output, (split $postfwd_settings{sepreq}.$postfwd_settings{seplst}, (&{$postfwd_settings{$_}{check}} ($_, 'CMD=DS;') || '<undef>')) } @daemons; 3947 return @output; 3948}; 3949 3950 3951# EOF postfwd3 3952 3953__END__ 3954 3955=head1 NAME 3956 3957postfwd3 - postfix firewall daemon 3958 3959=head1 SYNOPSIS 3960 3961B<postfwd3> [OPTIONS] [COMMAND] 3962 3963B<postfwd3> [OPTIONS] --cmd [SOURCE1, SOURCE2, ...] 3964 3965 Ruleset: (at least one, multiple use is allowed): 3966 -f, --file <file> reads rules from <file> 3967 -r, --rule <rule> adds <rule> to config 3968 -s, --scores <v>=<r> returns <r> when score exceeds <v> 3969 3970 Settings: (multiple use allowed) 3971 -F, --loadsettings <file> loads program settings from <file> 3972 --savesettings <file> saves program settings to <file> 3973 --showsettings exports program settings to stdout 3974 3975 Server: 3976 -i, --interface <dev> listen on interface <dev> 3977 -p, --port <port> listen on port <port> 3978 --proto <proto> socket type (tcp or unix) 3979 --server_socket <sock> e.g. tcp:127.0.0.1:10045 3980 -u, --user <name> set uid to user <name> 3981 -g, --group <name> set gid to group <name> 3982 --umask <mask> umask for master filepermissions 3983 --server_umask <mask> umask for server filepermissions 3984 --pidfile <path> create pidfile under <path> 3985 --min_servers <i> spawn at least <i> children 3986 --max_servers <i> do not spawn more than <i> children 3987 --min_spare_servers <i> minimum idle children 3988 --max_spare_servers <i> maximum idle children 3989 3990 Cache: 3991 -c, --cache <int> sets the request-cache timeout to <int> seconds 3992 --cleanup-requests <int> cleanup interval in seconds for request cache 3993 --cache_interface <dev> listen on interface <dev> 3994 --cache_port <port> listen on port <port> 3995 --cache_proto <proto> socket type (tcp or unix) 3996 --cache_socket <sock> e.g. tcp:127.0.0.1:10043 3997 --cache_umask <mask> umask for cache filepermissions 3998 --cacheid <list> list of request items for cache-id 3999 --cacheid_md5 cacheid => md5sum(request) 4000 --cache-rdomain-only skip recipient localpart for cache-id 4001 --cache-no-sender skip sender address for cache-id 4002 --cache-no-size skip size for cache-id 4003 --no_parent_request_cache disable parent request cache 4004 --no_parent_rate_cache disable parent rate cache 4005 --no_parent_dns_cache disable parent dns cache (default) 4006 --no_parent_group_cache disable parent group cache 4007 --no_parent_cache disable all parent caches 4008 4009 Groups: 4010 --default_group_ttl <i> default group TTL 4011 --group_maxitems <i> max items per group 4012 --cleanup-groups <int> cleanup interval in seconds for group objects 4013 4014 Rates: 4015 --cleanup-rates <int> cleanup interval in seconds for rate cache 4016 4017 Control: 4018 -k, --kill, --stop terminate postfwd3 4019 --reload, --hup reload postfwd3 4020 --watchdog <w> watchdog timer in seconds 4021 --respawn <r> respawn delay in seconds 4022 --failures <f> max respawn failure counter 4023 -d, --daemon execute program in background 4024 --nodaemon execute program in foreground 4025 --daemons <list> list of daemons to start 4026 --personality <type> type of policy server, allows 'PreFork' or 'Multiplex' 4027 --autopersonality determine personality by program name (see manpage) 4028 --noautopersonality don't do it (see above :) 4029 --v1 set personality to 'Multiplex' 4030 --v2 set personality to 'PreFork' 4031 --dumpcache show cache contents 4032 --dumpstats show statistics 4033 -R, --chroot <path> chroot to <path> before start 4034 --delcache <item> removes an item from the request cache 4035 --delrate <item> removes an item from the rate cache 4036 4037 DNS: 4038 -n, --nodns skip any dns based test 4039 --dns_timeout <i> dns query timeout in seconds 4040 --dns_timeout_max <i> disable dnsbl after <i> timeouts 4041 --dns_timeout_interval <i> reenable dnsbl after <i> seconds 4042 --cache-rbl-timeout <i> default dns ttl if not specified in ruleset 4043 --cache-rbl-default <s> default dns pattern if not specified in ruleset 4044 --cleanup-rbls <i> cleanup old dns cache items every <i> seconds 4045 --dns_async_txt perform dnsbl A and TXT lookups simultaneously 4046 --dns_max_ns_lookups max names to look up with sender_ns_addrs 4047 --dns_max_mx_lookups max names to look up with sender_mx_addrs 4048 --ipv6_dnsbl enables dnsbl checks for IPv6 addresses 4049 4050 Optional: 4051 -t, --test testing, always returns "dunno" 4052 -S, --summary <i> show stats every <i> seconds 4053 --noidlestats disables statistics when idle 4054 --norulestats disables per rule statistics 4055 -I, --instantcfg reloads ruleset on every new request 4056 --config_timeout <i> parser timeout in seconds 4057 --keep_groups do not clear group cache on reload 4058 --save_groups <file> save and load group cache on disk 4059 --keep_rates do not clear rate limit counters on reload 4060 --save_rates <file> save and load rate limits on disk 4061 -A, --aggregate_addrs pre-compute ip address lists to subnets 4062 --no_netaddr don't use NetAddr::IP functions 4063 --no_netcidr don't use Net::CIDR::Lite functions 4064 --cidr_method=s use method <s> for network checks 4065 4066 4067 Plugins: 4068 --plugins <file> loads postfwd plugins from file 4069 4070 Logging: 4071 -l, --logname <label> label for syslog messages 4072 --facility <s> use syslog facility <s> 4073 --socktype <s> use syslog socktype <s> 4074 --nodnslog do not log dns results 4075 --anydnslog log any dns (even cached) results 4076 --norulelog do not log rule actions 4077 --nolog|--perfmon no logging at all 4078 -v, --verbose verbose logging, use twice to increase 4079 --debug <s> list of debugging classes 4080 --debugclasses shows all available debug classes 4081 and exits the program 4082 4083 Information (use only at command-line!): 4084 -h, --help display this help and exit 4085 -m, --manual shows program manual 4086 -V, --version output version information and exit 4087 -D, --defaults show postfwd3 settings and exit 4088 -C, --showconfig show postfwd3 ruleset and exit (-v allowed) 4089 -L, --stdout redirect syslog messages to stdout (--stdoutlog works for compatibility) 4090 --stdin pull request from stdin instead of a network socket 4091 --cmd shorthand form of --stdin, --stdout and --nodaemon 4092 -q, --quiet no syslogging, no stdout (-P works for compatibility) 4093 4094 Commands: 4095 start start the program [default] 4096 stop same as --stop 4097 reload same as --reload 4098 dumpcache same as --dumpcache 4099 dumpstats same as --dumpstats 4100 defaults same as --defaults 4101 showsettings same as --showsettings 4102 delcache <item> same as --delcache <item> 4103 delrate <item> same as --delrate <item> 4104 4105 Obsolete (only for compatibility with postfwd v1): 4106 --shortlog, --dns_queuesize, --dns_retries 4107 4108 4109=head1 DESCRIPTION 4110 4111 4112=head2 INTRODUCTION 4113 4114postfwd3 is written to combine complex postfix restrictions in a ruleset similar to those of the most firewalls. 4115The program uses the postfix policy delegation protocol to control access to the mail system before a message 4116has been accepted (please visit L<http://www.postfix.org/SMTPD_POLICY_README.html> for more information). 4117 4118postfwd3 allows you to choose an action (e.g. reject, dunno) for a combination of several smtp parameters 4119(like sender and recipient address, size or the client's TLS fingerprint). Also it offers simple macros/acls 4120which should allow straightforward and easy-to-read configurations. 4121 4122I<Features:> 4123 4124* Complex combinations of smtp parameters 4125 4126* Combined RBL/RHSBL lookups with arbitrary actions depending on results 4127 4128* Scoring system 4129 4130* Date/time based rules 4131 4132* Macros/ACLs, Dynamic groups, Negation 4133 4134* Compare request attributes (e.g. client_name and helo_name) 4135 4136* Internal caching for requests and dns lookups 4137 4138* Built in statistics for rule efficiency analysis 4139 4140 4141=head2 CONFIGURATION 4142 4143A configuration line consists of optional item=value pairs, separated by semicolons 4144(`;`) and the appropriate desired action: 4145 4146 [ <item1>=<value>; <item2>=<value>; ... ] action=<result> 4147 4148I<Example:> 4149 4150 client_address=192.168.1.1 ; sender==no@bad.local ; action=REJECT 4151 4152This will deny all mail from 192.168.1.1 with envelope sender no@bad.local. The order of the elements 4153is not important. So the following would lead to the same result as the previous example: 4154 4155 action=REJECT ; client_address=192.168.1.1 ; sender==no@bad.local 4156 4157The way how request items are compared to the ruleset can be influenced in the following way: 4158 4159 ==================================================================== 4160 ITEM == VALUE true if ITEM equals VALUE 4161 ITEM => VALUE true if ITEM >= VALUE 4162 ITEM =< VALUE true if ITEM <= VALUE 4163 ITEM > VALUE true if ITEM > VALUE 4164 ITEM < VALUE true if ITEM < VALUE 4165 ITEM =~ VALUE true if ITEM ~= /^VALUE$/i 4166 ITEM != VALUE false if ITEM equals VALUE 4167 ITEM !> VALUE false if ITEM >= VALUE 4168 ITEM !< VALUE false if ITEM <= VALUE 4169 ITEM !~ VALUE false if ITEM ~= /^VALUE$/i 4170 ITEM = VALUE default behaviour (see ITEMS section) 4171 ==================================================================== 4172 4173To identify single rules in your log files, you may add an unique identifier for each of it: 4174 4175 id=R_001 ; action=REJECT ; client_address=192.168.1.1 ; sender==no@bad.local 4176 4177You may use these identifiers as target for the `jump()` command (see ACTIONS section below). Leading 4178or trailing whitespace characters will be ignored. Use '#' to comment your configuration. Others will 4179appreciate. 4180 4181A ruleset consists of one or multiple rules, which can be loaded from files or passed as command line 4182arguments. Please see the COMMAND LINE section below for more information on this topic. 4183 4184Since postfwd version 1.30 rules spanning span multiple lines can be defined by prefixing the following 4185lines with one or multiple whitespace characters (or '}' for macros): 4186 4187 id=RULE001 4188 client_address=192.168.1.0/24 4189 sender==no@bad.local 4190 action=REJECT no access 4191 4192postfwd versions prior to 1.30 require trailing ';' and '\'-characters: 4193 4194 id=RULE001; \ 4195 client_address=192.168.1.0/24; \ 4196 sender==no@bad.local; \ 4197 action=REJECT no access 4198 4199 4200=head2 ITEMS 4201 4202 id - a unique rule id, which can be used for log analysis 4203 ids also serve as targets for the "jump" command. 4204 4205 date, time - a time or date range within the specified rule shall hit 4206 # FORMAT: 4207 # Feb, 29th 4208 date=29.02.2008 4209 # Dec, 24th - 26th 4210 date=24.12.2008-26.12.2008 4211 # from today until Nov, 23rd 4212 date=-23.09.2008 4213 # from April, 1st until today 4214 date=01.04.2008- 4215 4216 days, months - a range of weekdays (Sun-Sat) or months (Jan-Dec) 4217 within the specified rule shall hit 4218 4219 score - when the specified score is hit (see ACTIONS section) 4220 the specified action will be returned to postfix 4221 scores are set global until redefined! 4222 4223 request_score - this value allows to access a request's score. it 4224 may be used as variable ($$request_score). 4225 4226 rbl, rhsbl, - query the specified RBLs/RHSBLs, possible values are: 4227 rhsbl_client, <name>[/<reply>/<maxcache>, <name>/<reply>/<maxcache>] 4228 rhsbl_sender, (defaults: reply=^127\.0\.0\.\d+$ maxcache=3600) 4229 rhsbl_reverse_client the results of all rhsbl_* queries will be combined 4230 in rhsbl_count (see below). 4231 4232 rblcount, rhsblcount - minimum RBL/RHSBL hitcounts to match. if not specified 4233 a single RBL/RHSBL hit will match the rbl/rhsbl items. 4234 you may specify 'all' to evaluate all items, and use 4235 it as variable in an action (see ACTIONS section) 4236 (default: 1) 4237 4238 sender_localpart, - the local-/domainpart of the sender address 4239 sender_domain 4240 4241 recipient_localpart, - the local-/domainpart of the recipient address 4242 recipient_domain 4243 4244 helo_address - postfwd3 tries to look up the helo_name. use 4245 helo_address=!!(0.0.0.0/0) to check for unknown. 4246 Please do not use this for positive access control 4247 (whitelisting), as it might be forged. 4248 4249 sender_ns_names, - postfwd3 tries to look up the names/ip addresses 4250 sender_ns_addrs of the nameservers for the sender domain part. 4251 Please do not use this for positive access control 4252 (whitelisting), as it might be forged. 4253 4254 sender_mx_names, - postfwd3 tries to look up the names/ip addresses 4255 sender_mx_addrs of the mx records for the sender domain part. 4256 Please do not use this for positive access control 4257 (whitelisting), as it might be forged. 4258 4259 version - postfwd3 version, contains "postfwd3 n.nn" 4260 this enables version based checks in your rulesets 4261 (e.g. for migration). works with old versions too, 4262 because a non-existing item always returns false: 4263 # version >= 1.10 4264 id=R01; version~=1\.[1-9][0-9]; sender_domain==some.org \ 4265 ; action=REJECT sorry no access 4266 4267 postfwd_port - postfwd server port, allows to use the same ruleset different 4268 instances: 4269 # rule only hits for instance at port 10045 4270 id=PORT10045; postfwd_port==10045; action=DUNNO 4271 4272 postfwd_interface - postfwd server initerface, see postfwd_port 4273 4274 ratecount - only available for rate(), size() and rcpt() actions. 4275 contains the actual limit counter: 4276 id=R01; action=rate(sender/200/600/REJECT limit of 200 exceeded [$$ratecount hits]) 4277 id=R02; action=rate(sender/100/600/WARN limit of 100 exceeded [$$ratecount hits]) 4278 4279Besides these you can specify any attribute of the postfix policy delegation protocol. 4280Feel free to combine them the way you need it (have a look at the EXAMPLES section below). 4281 4282Most values can be specified as regular expressions (PCRE). Please see the table below 4283for details: 4284 4285 # ========================================================== 4286 # ITEM=VALUE TYPE 4287 # ========================================================== 4288 id=something mask = string 4289 date=01.04.2007-22.04.2007 mask = date (DD.MM.YYYY-DD.MM.YYYY) 4290 time=08:30:00-17:00:00 mask = time (HH:MM:SS-HH:MM:SS) 4291 days=Mon-Wed mask = weekdays (Mon-Wed) or numeric (1-3) 4292 months=Feb-Apr mask = months (Feb-Apr) or numeric (1-3) 4293 score=5.0 mask = maximum floating point value 4294 rbl=zen.spamhaus.org mask = <name>/<reply>/<maxcache>[,...] 4295 rblcount=2 mask = numeric, will match if rbl hits >= 2 4296 helo_address=<a.b.c.d/nn> mask = CIDR[,CIDR,...] 4297 sender_ns_names=some.domain.tld mask = PCRE 4298 sender_mx_names=some.domain.tld mask = PCRE 4299 sender_ns_addrs=<a.b.c.d/nn> mask = CIDR[,CIDR,...] 4300 sender_mx_addrs=<a.b.c.d/nn> mask = CIDR[,CIDR,...] 4301 # ------------------------------ 4302 # Postfix version 2.1 and later: 4303 # ------------------------------ 4304 client_address=<a.b.c.d/nn> mask = CIDR[,CIDR,...] 4305 client_name=another.domain.tld mask = PCRE 4306 reverse_client_name=another.domain.tld mask = PCRE 4307 helo_name=some.domain.tld mask = PCRE 4308 sender=foo@bar.tld mask = PCRE 4309 recipient=bar@foo.tld mask = PCRE 4310 recipient_count=5 mask = numeric, will match if recipients >= 5 4311 # ------------------------------ 4312 # Postfix version 2.2 and later: 4313 # ------------------------------ 4314 sasl_method=plain mask = PCRE 4315 sasl_username=you mask = PCRE 4316 sasl_sender= mask = PCRE 4317 size=12345 mask = numeric, will match if size >= 12345 4318 ccert_subject=blackhole.nowhere.local mask = PCRE (only if tls verified) 4319 ccert_issuer=John+20Doe mask = PCRE (only if tls verified) 4320 ccert_fingerprint=AA:BB:CC:DD:EE:... mask = PCRE (do NOT use "..." here) 4321 # ------------------------------ 4322 # Postfix version 2.3 and later: 4323 # ------------------------------ 4324 encryption_protocol=TLSv1/SSLv3 mask = PCRE 4325 encryption_cipher=DHE-RSA-AES256-SHA mask = PCRE 4326 encryption_keysize=256 mask = numeric, will match if keysize >= 256 4327 ... 4328 4329the current list can be found at L<http://www.postfix.org/SMTPD_POLICY_README.html>. Please read carefully about which 4330attribute can be used at which level of the smtp transaction (e.g. size will only work reliably at END-OF-MESSAGE level). 4331Pattern matching is performed case insensitive. 4332 4333Multiple use of the same item is allowed and will compared as logical OR, which means that this will work as expected: 4334 4335 id=TRUST001; action=OK; encryption_keysize=64 4336 ccert_fingerprint=11:22:33:44:55:66:77:88:99 4337 ccert_fingerprint=22:33:44:55:66:77:88:99:00 4338 ccert_fingerprint=33:44:55:66:77:88:99:00:11 4339 sender=@domain\.local$ 4340 4341client_address, rbl and rhsbl items may also be specified as whitespace-or-comma-separated values: 4342 4343 id=SKIP01; action=dunno 4344 client_address=192.168.1.0/24, 172.16.254.23 4345 id=SKIP02; action=dunno 4346 client_address= 10.10.3.32 10.216.222.0/27 4347 4348The following items must be unique: 4349 4350 id, minimum and maximum values, rblcount and rhsblcount 4351 4352Any item can be negated by preceeding '!!' to it, e.g.: 4353 4354 id=HOST001 ; hostname == !!secure.trust.local ; action=REJECT only secure.trust.local please 4355 4356or using the right compare operator: 4357 4358 id=HOST001 ; hostname != secure.trust.local ; action=REJECT only secure.trust.local please 4359 4360To avoid confusion with regexps or simply for better visibility you can use '!!(...)': 4361 4362 id=USER01 ; sasl_username =~ !!( /^(bob|alice)$/ ) ; action=REJECT who is that? 4363 4364Request attributes can be compared by preceeding '$$' characters, e.g.: 4365 4366 id=R-003 ; client_name = !! $$helo_name ; action=WARN helo does not match DNS 4367 # or 4368 id=R-003 ; client_name = !!($$(helo_name)) ; action=WARN helo does not match DNS 4369 4370This is only valid for PCRE values (see list above). The comparison will be performed as case insensitive exact match. 4371Use the '-vv' option to debug. 4372 4373These special items will be reset for any new rule: 4374 4375 rblcount - contains the number of RBL answers 4376 rhsblcount - contains the number of RHSBL answers 4377 matches - contains the number of matched items 4378 dnsbltext - contains the dns TXT part of all RBL and RHSBL replies in the form 4379 rbltype:rblname:<txt>; rbltype:rblname:<txt>; ... 4380 4381These special items will be changed for any matching rule: 4382 4383 request_hits - contains ids of all matching rules 4384 4385This means that it might be necessary to save them, if you plan to use these values in later rules: 4386 4387 # set vals 4388 id=RBL01 ; rhsblcount=all; rblcount=all 4389 action=set(HIT_rhls=$$rhsblcount,HIT_rbls=$$rblcount,HIT_txt=$$dnsbltext) 4390 rbl=list.dsbl.org, bl.spamcop.net, dnsbl.sorbs.net, zen.spamhaus.org 4391 rhsbl_client=rddn.dnsbl.net.au, rhsbl.ahbl.org, rhsbl.sorbs.net 4392 rhsbl_sender=rddn.dnsbl.net.au, rhsbl.ahbl.org, rhsbl.sorbs.net 4393 4394 # compare 4395 id=RBL02 ; HIT_rhls>=1 ; HIT_rbls>=1 ; action=554 5.7.1 blocked using $$HIT_rhls RHSBLs and $$HIT_rbls RBLs [INFO: $$HIT_txt] 4396 id=RBL03 ; HIT_rhls>=2 ; action=554 5.7.1 blocked using $$HIT_rhls RHSBLs [INFO: $$HIT_txt] 4397 id=RBL04 ; HIT_rbls>=2 ; action=554 5.7.1 blocked using $$HIT_rbls RBLs [INFO: $$HIT_txt] 4398 4399 4400=head2 FILES 4401 4402Since postfwd1 v1.15 and postfwd3 v0.18 long item lists can be stored in separate files: 4403 4404 id=R001 ; ccert_fingerprint==file:/etc/postfwd/wl_ccerts ; action=DUNNO 4405 4406postfwd3 will read a list of items (one item per line) from /etc/postfwd/wl_ccerts. comments are allowed: 4407 4408 # client1 4409 11:22:33:44:55:66:77:88:99 4410 # client2 4411 22:33:44:55:66:77:88:99:00 4412 # client3 4413 33:44:55:66:77:88:99:00:11 4414 4415To use existing tables in key=value format, you can use: 4416 4417 id=R001 ; ccert_fingerprint==table:/etc/postfwd/wl_ccerts ; action=DUNNO 4418 4419This will ignore the right-hand value. Items can be mixed: 4420 4421 id=R002 ; action=REJECT 4422 client_name==unknown 4423 client_name==file:/etc/postfwd/blacklisted 4424 4425and for non pcre (comma separated) items: 4426 4427 id=R003 ; action=REJECT 4428 client_address==10.1.1.1, file:/etc/postfwd/blacklisted 4429 4430 id=R004 ; action=REJECT 4431 rbl=myrbl.home.local, zen.spamhaus.org, file:/etc/postfwd/rbls_changing 4432 4433You can check your configuration with the --show_config option at the command line: 4434 4435 # postfwd3 --showconfig --rule='action=DUNNO; client_address=10.1.0.0/16, file:/etc/postfwd/wl_clients, 192.168.2.1' 4436 4437should give something like: 4438 4439 Rule 0: id->"R-0"; action->"DUNNO"; client_address->"=;10.1.0.0/16, =;194.123.86.10, =;186.4.6.12, =;192.168.2.1" 4440 4441If a file can not be read, it will be ignored: 4442 4443 # postfwd3 --showconfig --rule='action=DUNNO; client_address=10.1.0.0/16, file:/etc/postfwd/wl_clients, 192.168.2.1' 4444 [LOG warning]: error: file /etc/postfwd/wl_clients not found - file will be ignored ? 4445 Rule 0: id->"R-0"; action->"DUNNO"; client_address->"=;10.1.0.0/16, =;192.168.2.1" 4446 4447File items are evaluated at configuration stage. Therefore postfwd3 needs to be reloaded if a file has changed 4448 4449If you want to specify a file, that will be reloaded for each request, you can use lfile: and ltable: 4450 4451 id=R001; client_address=lfile:/etc/postfwd/client_whitelist; action=dunno 4452 4453This will check the modification time of /etc/postfwd/client_whitelist every time the rule is evaluated and reload it as 4454necessary. Of course this might increase the system load, so please use it with care. 4455 4456The --showconfig option illustrates the difference: 4457 4458 ## evaluated at configuration stage 4459 # postfwd3 --nodaemon -L --rule='client_address=table:/etc/postfwd/clients; action=dunno' -C 4460 Rule 0: id->"R-0"; action->"dunno"; client_address->"=;1.1.1.1, =;1.1.1.2, =;1.1.1.3" 4461 4462 ## evaluated for any rulehit 4463 # postfwd3 --nodaemon -L --rule='client_address=ltable:/etc/postfwd/clients; action=dunno' -C 4464 Rule 0: id->"R-0"; action->"dunno"; client_address->"=;ltable:/etc/postfwd/clients" 4465 4466Files can refer to other files. The following is valid. 4467 4468 -- FILE /etc/postfwd/rules.cf -- 4469 id=R01; client_address=file:/etc/postfwd/clients_master.cf; action=DUNNO 4470 4471 -- FILE /etc/postfwd/clients_master.cf -- 4472 192.168.1.0/24 4473 file:/etc/postfwd/clients_east.cf 4474 file:/etc/postfwd/clients_west.cf 4475 4476 -- FILE /etc/postfwd/clients_east.cf -- 4477 192.168.2.0/24 4478 4479 -- FILE /etc/postfwd/clients_west.cf -- 4480 192.168.3.0/24 4481 4482Note that there is currently no loop detection (/a/file calls /a/file) and that this feature is only available 4483with postfwd1 v1.15 and postfwd3 v0.18 and higher. 4484 4485 4486=head2 ACTIONS 4487 4488I<General> 4489 4490Actions will be executed, when all rule items have matched a request (or at least one of any item list). You can refer to 4491request attributes by preceeding $$ characters, like: 4492 4493 id=R-003; client_name = !!$$helo_name; action=WARN helo '$$helo_name' does not match DNS '$$client_name' 4494 # or 4495 id=R-003; client_name = !!$$helo_name; action=WARN helo '$$(helo_name)' does not match DNS '$$(client_name)' 4496 4497I<postfix actions> 4498 4499Actions will be replied to postfix as result to policy delegation requests. Any action that postfix understands is allowed - see 4500"man 5 access" or L<http://www.postfix.org/access.5.html> for a description. If no action is specified, the postfix WARN action 4501which simply logs the event will be used for the corresponding rule. 4502 4503postfwd3 will return dunno if it has reached the end of the ruleset and no rule has matched. This can be changed by placing a last 4504rule containing only an action statement: 4505 4506 ... 4507 action=dunno ; sender=@domain.local # sender is ok 4508 action=reject # default deny 4509 4510I<postfwd3 actions> 4511 4512postfwd3 actions control the behaviour of the program. Currently you can specify the following: 4513 4514 jump (<id>) 4515 jumps to rule with id <id>, use this to skip certain rules. 4516 you can jump backwards - but remember that there is no loop 4517 detection at the moment! jumps to non-existing ids will be skipped. 4518 4519 score (<score>) 4520 the request's score will be modified by the specified <score>, 4521 which must be a floating point value. the modificator can be either 4522 +n.nn adds n.nn to current score 4523 -n.nn sustracts n.nn from the current score 4524 *n.nn multiplies the current score by n.nn 4525 /n.nn divides the current score through n.nn 4526 =n.nn sets the current score to n.nn 4527 if the score exceeds the maximum set by `--scores` option (see 4528 COMMAND LINE) or the score item (see ITEMS section), the action 4529 defined for this case will be returned (default: 5.0=>"REJECT postfwd3 score exceeded"). 4530 4531 set (<item>=<value>,<item>=<value>,...) 4532 this command allows you to insert or override request attributes, which then may be 4533 compared to your further ruleset. use this to speed up repeated comparisons to large item lists. 4534 please see the EXAMPLES section for more information. you may separate multiple key=value pairs 4535 by "," characters. 4536 4537 rate (<item>/<max>/<time>/<action>) 4538 this command creates a counter for the given <item>, which will be increased any time a request 4539 containing it arrives. if it exceeds <max> within <time> seconds it will return <action> to postfix. 4540 rate counters are very fast as they are executed before the ruleset is parsed. 4541 please note that <action> was limited to postfix actions (no postfwd actions) for postfwd versions <1.33! 4542 # no more than 3 requests per 5 minutes 4543 # from the same "unknown" client 4544 id=RATE01 ; client_name==unknown 4545 action=rate(client_address/3/300/450 4.7.1 sorry, max 3 requests per 5 minutes) 4546 4547 size (<item>/<max>/<time>/<action>) 4548 this command works similar to the rate() command with the difference, that the rate counter is 4549 increased by the request's size attribute. to do this reliably you should call postfwd3 from 4550 smtpd_end_of_data_restrictions. if you want to be sure, you could check it within the ruleset: 4551 # size limit 1.5mb per hour per client 4552 id=SIZE01 ; protocol_state==END-OF-MESSAGE ; client_address==!!(10.1.1.1) 4553 action=size(client_address/1572864/3600/450 4.7.1 sorry, max 1.5mb per hour) 4554 4555 rcpt (<item>/<max>/<time>/<action>) 4556 this command works similar to the rate() command with the difference, that the rate counter is 4557 increased by the request's recipient_count attribute. to do this reliably you should call postfwd 4558 from smtpd_data_restrictions or smtpd_end_of_data_restrictions. if you want to be sure, you could 4559 check it within the ruleset: 4560 # recipient count limit 3 per hour per client 4561 id=RCPT01 ; protocol_state==END-OF-MESSAGE ; client_address==!!(10.1.1.1) 4562 action=rcpt(client_address/3/3600/450 4.7.1 sorry, max 3 recipients per hour) 4563 4564 rate5321,size5321,rcpt5321 (<item>/<max>/<time>/<action>) 4565 same as the corresponding non-5321 functions, with the difference that the localpart of 4566 sender oder recipient addresses are evaluated case-sensitive according to rfc5321. That 4567 means that requests from bob@example.local and BoB@example.local will be treated differently 4568 4569 groupadd(<groupname>/<item>[/<ttl>]) 4570 saves <item> to dynamic group <group> for later use. the object will be removed from that 4571 group after ttl seconds. if not set, postfwd3 will use the default --default_group_ttl [3600s]. 4572 Please read the chapter about dynamic groups before use! 4573 # add client_address to group %%blacklisted_hosts 4574 id=ADDGROUP 4575 rbl=zen.spamhaus.org,bl.spamcop.net,ix.dnsbl.manitu.net 4576 action=groupadd(%%blacklisted_hosts/client_address/86400) 4577 4578 groupdel(<groupname>/<item>) 4579 removes <item> from dynamic group <group>. Please read the chapter about dynamic groups before use! 4580 # remove a client_address from group %%blacklisted_hosts 4581 id=DELGROUP 4582 rbl=lists.dnswl.org 4583 action=groupdel(%%blacklisted_hosts/client_address) 4584 4585 ask (<addr>:<port>[:<ignore>]) 4586 allows to delegate the policy decision to another policy service (e.g. postgrey). the first 4587 and the second argument (address and port) are mandatory. a third optional argument may be 4588 specified to tell postfwd3 to ignore certain answers and go on parsing the ruleset: 4589 # example1: query postgrey and return it's answer to postfix 4590 id=GREY; client_address==10.1.1.1; action=ask(127.0.0.1:10031) 4591 # example2: query postgrey but ignore it's answer, if it matches 'DUNNO' 4592 # and continue parsing postfwd's ruleset 4593 id=GREY; client_address==10.1.1.1; action=ask(127.0.0.1:10031:^dunno$) 4594 4595 mail(server/helo/from/to/subject/body) 4596 This command is deprecated. You should try to use the sendmail() action instead. 4597 Very basic mail command, that sends a message with the given arguments. LIMITATIONS: 4598 This basically performs a telnet. No authentication or TLS are available. Additionally it does 4599 not track notification state and will notify you any time, the corresponding rule hits. 4600 4601 sendmail(sendmail-path::from::to::subject::body) 4602 Mail command, that uses an existing sendmail binary and sends a message with the given arguments. 4603 LIMITATIONS: The command does not track notification state and will notify you any time, the 4604 corresponding rule hits (which could mean 100 mails for a mail with 100 recipients at RCPT stage). 4605 4606 wait (<delay>) 4607 pauses the program execution for <delay> seconds. use this for 4608 delaying or throtteling connections. 4609 4610 note (<string>) 4611 just logs the given string and continues parsing the ruleset. 4612 if the string is empty, nothing will be logged (noop). 4613 4614 quit (<code>) 4615 terminates the program with the given exit-code. postfix doesn`t 4616 like that too much, so use it with care. 4617 4618You can reference to request attributes, like 4619 4620 id=R-HELO ; helo_name=^[^\.]+$ ; action=REJECT invalid helo '$$helo_name' 4621 4622Since postfwd3 version 2.00 a rule can have multiple postfwd actions, like 4623 4624 # if client is found on list.dnswl.org, 4625 # 1. send a note 4626 # 2. add it's ip to group %%WHITELISTED 4627 # 3. permit the request 4628 id=ADDR01 4629 rbl=list.dnswl.org 4630 action=note(adding $$client_address to WHITELIST) 4631 action=groupadd(%%WHITELISTED/client_address) 4632 action=PERMIT_AUTH_DESTINATION 4633 4634 4635=head2 MACROS/ACLS 4636 4637Multiple use of long items or combinations of them may be abbreviated by macros. Those must be prefixed by '&&' (two '&' characters). 4638First the macros have to be defined as follows: 4639 4640 &&RBLS { rbl=zen.spamhaus.org,list.dsbl.org,bl.spamcop.net,dnsbl.sorbs.net,ix.dnsbl.manitu.net; }; 4641 4642Then these may be used in your rules, like: 4643 4644 &&RBLS ; client_name=^unknown$ ; action=REJECT 4645 &&RBLS ; client_name=(\d+[\.-_]){4} ; action=REJECT 4646 &&RBLS ; client_name=[\.-_](adsl|dynamic|ppp|)[\.-_] ; action=REJECT 4647 4648Macros can contain actions, too: 4649 4650 # definition 4651 &&GONOW { action=REJECT your request caused our spam detection policy to reject this message. More info at http://www.domain.local; }; 4652 # rules 4653 &&GONOW ; &&RBLS ; client_name=^unknown$ 4654 &&GONOW ; &&RBLS ; client_name=(\d+[\.-_]){4} 4655 &&GONOW ; &&RBLS ; client_name=[\.-_](adsl|dynamic|ppp|)[\.-_] 4656 4657Macros can contain macros, too: 4658 4659 # definition 4660 &&RBLS{ 4661 rbl=zen.spamhaus.org 4662 rbl=list.dsbl.org 4663 rbl=bl.spamcop.net 4664 rbl=dnsbl.sorbs.net 4665 rbl=ix.dnsbl.manitu.net 4666 }; 4667 &&DYNAMIC{ 4668 client_name=^unknown$ 4669 client_name=(\d+[\.-_]){4} 4670 client_name=[\.-_](adsl|dynamic|ppp|)[\.-_] 4671 }; 4672 &&GOAWAY { &&RBLS; &&DYNAMIC; }; 4673 # rules 4674 &&GOAWAY ; action=REJECT dynamic client and listed on RBL 4675 4676Basically macros are simple text substitutions - see the L</PARSER> section for more information. 4677 4678 4679=head2 DYNAMIC GROUPS 4680 4681Dynamic groups are list objects. You can add or remove items and compare request items to their content within your ruleset. 4682Groups are saved persistant for further requests. This way you can save information for later use. Every item of a group has 4683a time-to-live value. 4684 4685Group-names have to be preceeded by double '%'-characters, like %%GROUPNAME: 4686 4687 # add client_address to group %%BLACKLISTED, if sent to spamtrap 4688 id=SPAMTRAP; recipient==spamtrap@domain.local; action=groupadd(%%BLACKLISTED/client_address) 4689 4690 # reject any request from client_address in group %%BLACKLISTED 4691 id=BLACKLISTED; client_address=%%BLACKLISTED; action=REJECT 4692 4693The groupadd() function receives the following arguments. The <ttl> value is optional. If not specified, postfwd3 4694will use the default --default_group_ttl [300s]: 4695 4696 groupadd ( <Groupname> / <item> [ / <ttl> ] ) 4697 4698postfwd will save this data in the following structure: 4699 4700 %Group_Cache -> %<Groupname> -> $<item> = <ttl> 4701 4702The groupdel() function removes an item from a group: 4703 4704 groupdel ( <Groupname> / <item> ) 4705 4706Expired members will be removed at cleanup stage after --cleanup_groups <i> seconds. 4707 4708To compare requests with dynamic groups, these must be refered by preceeding '%%'-characters: 4709 4710 <item> = %%<Groupname> 4711 4712Empty groups always compare false. The following ruleset is possible: 4713 4714 # allow any request from client_address in group %%WHITELISTED 4715 id=WHITEGROUP; client_address=%%WHITELISTED; action=DUNNO 4716 4717 # reject any request from client_address in group %%BLACKLISTED 4718 id=BLACKGROUP; client_address=%%BLACKLISTED; action=REJECT 4719 4720 # ... other rules ... 4721 4722 # ... whitelist file ... 4723 id=W_TRUST; action=set(WHITE=1) 4724 client_address=file:/etc/postfwd/whitelist_networks 4725 4726 # ... whitelist dns ... 4727 id=W_DNSWL; action=set(WHITE=1) 4728 rbl=list.dnswl.org 4729 4730 # ... -> add it to whitelist for a day and allow request 4731 id=W_GROUP; WHITE==1 4732 action=groupadd(%%WHITELISTED/client_address/86400) 4733 action=DUNNO 4734 4735 # ... or blacklist by sending to spamtrap ... 4736 id=B_SPAMTRAP; action=set(BLACK=1) 4737 recipient==spamtrap@domain.local 4738 4739 # ... or blacklisted by dns ... 4740 id=B_DNSBL; action=set(BLACK=1) 4741 rbl=zen.spamhaus.org 4742 4743 # ... -> add it to group BLACKLISTED for 1 hour and reject 4744 id=B_GROUP; BLACK==1 4745 action=groupadd(%%BLACKLISTED/client_address/3600) 4746 action=REJECT 4747 4748B<Limitations> 4749 4750Currently dynamic groups are kept in memory. By default postfwd3 will only accept a maximum of 999999 members 4751in a group. To deactivate this limit set --group_maxitems to '-1'. 4752 4753With PreFork personality, you should be aware that this information must be shared through the cache daemon. 4754 4755In short: Don't let these lists get excessively big unless you have enough system capacity to do so. If necessary 4756tune your config by setting the <ttl> and --group_maxitems. 4757 4758 4759=head2 PLUGINS 4760 4761B<Description> 4762 4763The plugin interface allow you to define your own checks and enhance postfwd's 4764functionality. Feel free to share useful things! 4765 4766B<Warning> 4767 4768Note that the plugin interface is still at devel stage. Please test your plugins 4769carefully, because errors may cause postfwd to break! It is also 4770allowed to override attributes or built-in functions, but be sure that you know 4771what you do because some of them are used internally. 4772 4773Please keep security in mind, when you access sensible ressources and never, ever 4774run postfwd as privileged user! Also never trust your input (especially hostnames, 4775and e-mail addresses). 4776 4777B<ITEMS> 4778 4779Item plugins are perl subroutines which integrate additional attributes to requests 4780before they are evaluated against postfwd's ruleset like any other item of the 4781policy delegation protocol. This allows you to create your own checks. 4782 4783plugin-items can not be used selective. these functions will be executed for every 4784request postfwd receives, so keep performance in mind. 4785 4786 SYNOPSIS: postfwd_items_plugin{<name>}($request) 4787 4788means that your subroutine, called <name>, has access to a hash-reference called 4789$request, which contains all request attributes, like $request->{client_name} and 4790saves values in the following form: 4791 4792 save: $result->{<item>} = <value> 4793 4794this creates the new item <item> containing <value>, which will be integrated in 4795the policy delegation request and therefore may be used in postfwd's ruleset. 4796 4797 # do NOT remove the next line 4798 %postfwd_items_plugin = ( 4799 4800 # EXAMPLES - integrated in postfwd. no need to activate them here. 4801 4802 # allows to check postfwd version in ruleset 4803 "version" => sub { 4804 my($request) = shift; 4805 $request->{version} => $NAME." ".$VERSION, 4806 }, 4807 4808 # sender_domain and recipient_domain 4809 "address_parts" => sub { 4810 my($request) = shift; 4811 $request->{sender} =~ /@([^@]*)$/; 4812 $request->{sender_domain} = ($1 || ''); 4813 $request->{recipient} =~ /@([^@]*)$/; 4814 $request->{recipient_domain} = ($1 || ''); 4815 }, 4816 4817 # do NOT remove the next line 4818 ); 4819 4820B<COMPARE> 4821 4822Compare plugins allow you to define how your new items should be compared to the ruleset. 4823These are optional. If you don't specify one, the default (== for exact match, =~ for PCRE, ...) 4824will be used. 4825 4826 SYNOPSIS: <item> => sub { return &{$postfwd_compare{<type>}}(@_); }, 4827 4828 # do NOT remove the next line 4829 %postfwd_compare_plugin = ( 4830 4831 EXAMPLES - integrated in postfwd. no need to activate them here. 4832 4833 # Simple example 4834 # SYNOPSIS: <result> = <item> (return &{$postfwd_compare{<type>}}(@_)) 4835 "client_address" => sub { return &{$postfwd_compare{cidr}}(@_); }, 4836 "size" => sub { return &{$postfwd_compare{numeric}}(@_); }, 4837 "recipient_count" => sub { return &{$postfwd_compare{numeric}}(@_); }, 4838 4839 # Complex example 4840 # SYNOPSIS: <result> = <item>(<operator>, <ruleset value>, <request value>, <request>) 4841 "numeric" => sub { 4842 my($cmp,$val,$myitem,$request) = @_; 4843 my($myresult) = undef; $myitem ||= "0"; $val ||= "0"; 4844 if ($cmp eq '==') { 4845 $myresult = ($myitem == $val); 4846 } elsif ($cmp eq '=<') { 4847 $myresult = ($myitem <= $val); 4848 } elsif ($cmp eq '=>') { 4849 $myresult = ($myitem >= $val); 4850 } elsif ($cmp eq '<') { 4851 $myresult = ($myitem < $val); 4852 } elsif ($cmp eq '>') { 4853 $myresult = ($myitem > $val); 4854 } elsif ($cmp eq '!=') { 4855 $myresult = not($myitem == $val); 4856 } elsif ($cmp eq '!<') { 4857 $myresult = not($myitem <= $val); 4858 } elsif ($cmp eq '!>') { 4859 $myresult = not($myitem >= $val); 4860 } else { 4861 $myresult = ($myitem >= $val); 4862 }; 4863 return $myresult; 4864 }, 4865 4866 # do NOT remove the next line 4867 ); 4868 4869B<ACTIONS> 4870 4871Action plugins allow to define new postfwd actions. By setting the $stop-flag you can decide to 4872continue or to stop parsing the ruleset. 4873 4874 SYNOPSIS: (<stop rule parsing>, <next rule index>, <return action>, <logprefix>) = 4875 <action> (<current rule index>, <current time>, <command name>, <argument>, <logprefix>, <request>) 4876 4877 # do NOT remove the next line 4878 %postfwd_actions_plugin = ( 4879 4880 # EXAMPLES - integrated in postfwd. no need to activate them here. 4881 4882 # note(<logstring>) command 4883 "note" => sub { 4884 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 4885 my($myaction) = 'dunno'; my($stop) = 0; 4886 log_info "[RULES] ".$myline." - note: ".$myarg if $myarg; 4887 return ($stop,$index,$myaction,$myline); 4888 }, 4889 4890 # skips next <myarg> rules 4891 "skip" => sub { 4892 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 4893 my($myaction) = 'dunno'; my($stop) = 0; 4894 $index += $myarg if ( $myarg and not(($index + $myarg) > $#Rules) ); 4895 return ($stop,$index,$myaction,$myline); 4896 }, 4897 4898 # dumps current request contents to syslog 4899 "dumprequest" => sub { 4900 my($index,$now,$mycmd,$myarg,$myline,$request) = @_; 4901 my($myaction) = 'dunno'; my($stop) = 0; 4902 map { log_info "[DUMP] rule=$index, Attribute: $_=$request{$_}" } (keys %{$request}); 4903 return ($stop,$index,$myaction,$myline); 4904 }, 4905 4906 # do NOT remove the next line 4907 ); 4908 4909 4910=head2 COMMAND LINE 4911 4912I<Ruleset> 4913 4914The following arguments are used to specify the source of the postfwd3 ruleset. This means 4915that at least one of the following is required for postfwd3 to work. 4916 4917 -f, --file <file> 4918 Reads rules from <file>. Please see the CONFIGURATION section 4919 for more information. 4920 4921 -r, --rule <rule> 4922 Adds <rule> to ruleset. Remember that you might have to quote 4923 strings that contain whitespaces or shell characters. 4924 4925I<Settings> 4926 4927 -F, --loadsettings <file> 4928 Loads program settings from <file>. data must be in Data::Dumper format. 4929 Please read the SETTINGS section for more information. 4930 4931 --savesettings <file> 4932 Saves program settings to <file> in Data::Dumper format. 4933 Please read the SETTINGS section for more information. 4934 4935 --showsettings 4936 Exports program settings in Data::Dumper format to stdout. This output 4937 can be saved to a file and later used with the --loadsettings option. 4938 4939I<Scoring> 4940 4941 -s, --scores <val>=<action> 4942 Returns <action> to postfix, when the request's score exceeds <val> 4943 4944Multiple usage is allowed. Just chain your arguments, like: 4945 4946 postfwd3 -r "<item>=<value>;action=<result>" -f <file> -f <file> ... 4947 or 4948 postfwd3 --scores 4.5="WARN high score" --scores 5.0="REJECT postfwd3 score too high" ... 4949 4950In case of multiple scores, the highest match will count. The order of the arguments will be 4951reflected in the postfwd3 ruleset. 4952 4953I<Networking> 4954 4955postfwd3 can be run as daemon so that it listens on the network for incoming requests. 4956The following arguments will control it's behaviour in this case. 4957 4958 -d, --daemon, --nodaemon 4959 postfwd3 will run as daemon and listen on the network for incoming 4960 queries (default 127.0.0.1:10045). Use --nodaemon to keep postfwd3 4961 running in foreground. 4962 4963 -i, --interface <dev> 4964 Bind postfwd3 to the specified interface (default 127.0.0.1). 4965 4966 -p, --port <port> 4967 postfwd3 listens on the specified port (default tcp/10045). 4968 4969 --proto <type> 4970 The protocol type for postfwd's socket. Currently you may use 'tcp' or 'unix' here. 4971 To use postfwd3 with a unix domain socket, run it as follows: 4972 postfwd3 --proto=unix --port=/somewhere/postfwd.socket 4973 4974 -u, --user <name> 4975 Changes real and effective user to <name>. 4976 4977 -g, --group <name> 4978 Changes real and effective group to <name>. 4979 4980 --personality <type> 4981 Type of policy server, allows 'PreFork' or 'Multiplex'. 4982 This option overrides --autopersonality. 4983 4984 --v1 4985 set personality to 'Multiplex' 4986 This option overrides --autopersonality. 4987 4988 --v2 4989 set personality to 'PreFork' 4990 This option overrides --autopersonality. 4991 4992 --autopersonality 4993 postfwd3 determines the personality by program name: 4994 * 'Multiplex' for 'postfwd' or 'postfwd1' 4995 * 'PreFork' for 'postfwd2' 4996 you can disable this by using --noautopersonality or 4997 explicitly using --personality, --v1 or --v2 4998 4999 --umask <mask> 5000 Changes the umask for filepermissions of the master process (pidfile). 5001 Attention: This is umask, not chmod - you have to specify the bits that 5002 should NOT apply. E.g.: umask 077 equals to chmod 700. 5003 5004 --cache_umask <mask> 5005 Changes the umask for filepermissions of the cache process (unix domain socket). 5006 5007 --server_umask <mask> 5008 Changes the umask for filepermissions of the server process (unix domain socket). 5009 5010 -R, --chroot <path> 5011 Chroot the process to the specified path. 5012 Please look at http://postfwd.org/postfwd3-chroot.html before use! 5013 5014 --pidfile <path> 5015 The process id will be saved in the specified file. 5016 5017 --facility <f> 5018 sets the syslog facility, default is 'mail' 5019 5020 --socktype <s> 5021 sets the Sys::Syslog socktype to 'native', 'inet' or 'unix'. 5022 Default is to auto-detect this depening on module version and os. 5023 5024 -l, --logname <label> 5025 Labels the syslog messages. Useful when running multiple 5026 instances of postfwd. 5027 5028 --loglen <int> 5029 Truncates any syslog message after <int> characters. 5030 5031I<Plugins> 5032 5033 --plugins <file> 5034 Loads postfwd plugins from file. Please see http://postfwd.org/postfwd.plugins 5035 or the plugins.postfwd.sample that is available from the tarball for more info. 5036 5037I<Optional arguments> 5038 5039These parameters influence the way postfwd3 is working. Any of them can be combined. 5040 5041 -v, --verbose 5042 Verbose logging displays a lot of useful information but can cause 5043 your logfiles to grow noticeably. So use it with caution. Set the option 5044 twice (-vv) to get more information (logs all request attributes). 5045 5046 -c, --cache <int> (default=600) 5047 Timeout for request cache, results for identical requests will be 5048 cached until config is reloaded or this time (in seconds) expired. 5049 A setting of 0 disables this feature. 5050 5051 --cache-no-size 5052 Ignores size attribute for cache comparisons which will lead to better 5053 cache-hit rates. You should set this option, if you don't use the size 5054 item in your ruleset. 5055 5056 --cache-no-sender 5057 Ignores sender address for cache comparisons which will lead to better 5058 cache-hit rates. You should set this option, if you don't use the sender 5059 item in your ruleset. 5060 5061 --cache-rdomain-only 5062 This will strip the localpart of the recipient's address before filling the 5063 cache. This may considerably increase cache-hit rates. 5064 5065 --cache-rbl-timeout <timeout> (default=3600) 5066 This default value will be used as timeout in seconds for rbl cache items, 5067 if not specified in the ruleset. 5068 5069 --cache-rbl-default <pattern> (default=^127\.0\.0\.\d+$) 5070 Matches <pattern> to rbl/rhsbl answers (regexp) if not specified in the ruleset. 5071 5072 --cacheid <item>, <item>, ... 5073 This csv-separated list of request attributes will be used to construct 5074 the request cache identifier. Use this only, if you know exactly what you 5075 are doing. If you, for example, use postfwd3 only for RBL/RHSBL control, 5076 you may set this to 5077 postfwd3 --cache=3600 --cacheid=client_name,client_address 5078 This increases efficiency of caching and improves postfwd's performance. 5079 Warning: You should list all items here, which are used in your ruleset! 5080 5081 --cacheid_md5, --nocacheid_md5 (default=1) 5082 The cacheid will be created with a md5sum of the request items. 5083 5084 --cleanup-requests <interval> (default=600) 5085 The request cache will be searched for timed out items after this <interval> in 5086 seconds. It is a minimum value. The cleanup process will only take place, when 5087 a new request arrives. 5088 5089 --cleanup-rbls <interval> (default=600) 5090 The rbl cache will be searched for timed out items after this <interval> in 5091 seconds. It is a minimum value. The cleanup process will only take place, when 5092 a new request arrives. 5093 5094 --cleanup-rates <interval> (default=600) 5095 The rate cache will be searched for timed out items after this <interval> in 5096 seconds. It is a minimum value. The cleanup process will only take place, when 5097 a new request arrives. 5098 5099 -S, --summary <int> (default=600) 5100 Shows some usage statistics (program uptime, request counter, matching rules) 5101 every <int> seconds. This option is included by the -v switch. 5102 This feature uses the alarm signal, so you can force postfwd3 to dump the stats 5103 using `kill -ALRM <pid>` (where <pid> is the process id of postfwd). 5104 5105 Example: 5106 Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Counters: 213000 seconds uptime, 39 rules 5107 Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Requests: 71643 overall, 49 last interval, 62.88% cache hits 5108 Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Averages: 20.18 overall, 4.90 last interval, 557.30 top 5109 Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Contents: 44 cached requests, 239 cached dnsbl results 5110 Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Rule ID: R-001 matched: 2704 times 5111 Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Rule ID: R-002 matched: 9351 times 5112 Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Rule ID: R-003 matched: 3116 times 5113 ... 5114 5115 --no-rulestats 5116 Disables per rule statistics. Keeps your log clean, if you do not use them. 5117 This option has no effect without --summary or --verbose set. 5118 5119 -L, --stdout 5120 Redirects all syslog messages to stdout for debugging. Do not use this in daemon mode! 5121 5122 --stdin 5123 Tells postfwd to get the request data from STDIN instead of a network socket. This 5124 may be used to test rulesets at the command-line: 5125 5126 Example: 5127 postfwd -f /etc/postfwd/postfwd.cf --stdin --stdout --nodaemon ../tools/request.sample 5128 5129 --cmd 5130 Shorthand for the combination --stdin, --stdout and --nodaemon 5131 5132 Example: 5133 postfwd -f /etc/postfwd/postfwd.cf --cmd ../tools/request.sample 5134 5135 -t, --test 5136 In test mode postfwd3 always returns "dunno", but logs according 5137 to it`s ruleset. -v will be set automatically with this option. 5138 5139 -n, --nodns 5140 Disables all DNS based checks like RBL checks. Rules containing 5141 such elements will be ignored. 5142 5143 -n, --nodnslog 5144 Disables logging of dns events. 5145 5146 --dns_timeout (default: 14) 5147 Sets the timeout for asynchonous dns queries in seconds. This value will apply to 5148 all dns items in a rule. 5149 5150 --dns_timeout_max (default: 10) 5151 Sets the maximum timeout counter for dnsbl lookups. If the timeouts exceed this value 5152 the corresponding dnsbl will be deactivated for a while (see --dns_timeout_interval). 5153 5154 --dns_timeout_interval (default=1200) 5155 The dnsbl timeout counter will be cleaned after this interval in seconds. Use this 5156 in conjunction with the --dns_timeout_max parameter. 5157 5158 --dns_async_txt 5159 Perform dnsbl A and TXT lookups simultaneously (otherwise only for listings with at 5160 least one A record). This needs more network bandwidth due to increased queries but 5161 might increase throughput because the lookups can be parallelized. 5162 5163 --dns_max_ns_lookups (default=0) 5164 maximum ns names to lookup up with sender_ns_addrs item. use 0 for no maximum. 5165 5166 --dns_max_mx_lookups (default=0) 5167 maximum mx names to lookup up with sender_mx_addrs item. use 0 for no maximum. 5168 5169 --ipv6_dnsbl (default=0) 5170 enables dnsbl checks for IPv6 addresses 5171 5172 -I, --instantcfg 5173 The config files, specified by -f will be re-read for every request 5174 postfwd3 receives. This enables on-the-fly configuration changes 5175 without restarting. Though files will be read only if necessary 5176 (which means their access times changed since last read) this might 5177 significantly increase system load. 5178 5179 --config_timeout (default=3) 5180 timeout in seconds to parse a single configuration line. if exceeded, the rule will 5181 be skipped. this is used to prevent problems due to large files or loops. 5182 5183 --keep_groups (default=0) 5184 With this option set postfwd3 does not clear the group cache on reload. Please 5185 note that you have to restart (not reload) postfwd with this option if you change 5186 any group based rules. 5187 5188 --save_groups (default=none) 5189 With this option postfwd saves existing groups to disk and reloads them on program 5190 start. This allows persistent rate limits across program restarts or reboots. 5191 Please note that postfwd needs read and write access to the specified file. 5192 5193 --keep_rates (default=0) 5194 With this option set postfwd3 does not clear the rate limit counters on reload. Please 5195 note that you have to restart (not reload) postfwd with this option if you change 5196 any rate limit rules. 5197 5198 --save_rates (default=none) 5199 With this option postfwd saves existing rate limit counters to disk and reloads them 5200 on program start. This allows persistent rate limits across program restarts or reboots. 5201 Please note that postfwd needs read and write access to the specified file. 5202 5203 -A, --aggregate_addrs, --noaggregate_addrs 5204 Pre-computes ip address lists to subnets, so that e.g.: 5205 5206 client_address=10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24 5207 will result in 5208 client_address=10.0.0.0/22 5209 5210 This increases the performance of cidr-matching 5211 5212 --no_netaddr 5213 Do not use functions from module NetAddr::IP, even if it was found 5214 on the system. This option implicitly disables --aggregate_addrs. 5215 5216 --no_netcidr 5217 Do not use functions from module Net::CIDR::Lite, even if it was found 5218 on the system. 5219 5220 --cidr_method=s (default=autodetect) 5221 Use method <s> for network checks. Valid arguments are: 5222 netcidr - compare networks using Net::CIDR::Lite Module (v4 and v6 cidr) 5223 netaddr - compare networks using NetAddr::IP Module (v4 and v6 cidr) 5224 postfwd - old but very fast method (v4=cidr, v6=regex) 5225 If not specified postfwd tries to load modules in the above order and falls back 5226 to 'postfwd' if nothing found. 5227 5228I<Informational arguments> 5229 5230These arguments are for command line usage only. Never ever use them with postfix! 5231 5232 -C, --showconfig 5233 Displays the current ruleset. Use -v for verbose output. 5234 5235 -V, --version 5236 Displays the program version. 5237 5238 -h, --help 5239 Shows program usage. 5240 5241 -m, --manual 5242 Displays the program manual. 5243 5244 -D, --defaults 5245 displays complete postfwd3 settings. 5246 5247 -P, --perfmon 5248 This option turns of any syslogging and output. It is included 5249 for performance testing. 5250 5251 --dumpstats 5252 Displays program usage statistics. 5253 5254 --dumpcache 5255 Displays cache contents. 5256 5257 --delcache <item> 5258 Removes an item from the request cache. Use --dumpcache to identify objects. 5259 E.g.: 5260 # postfwd --dumpcache 5261 ... 5262 %rate_cache -> %sender=gmato@jqvo.org -> %RATE002+2_600 -> @count -> '1' 5263 %rate_cache -> %sender=gmato@jqvo.org -> %RATE002+2_600 -> @maxcount -> '2' 5264 ... 5265 # postfwd --delrate="sender=gmato@jqvo.org" 5266 rate cache item 'sender=gmato@jqvo.org' removed 5267 5268 --delrate <item> 5269 Removes an item from the rate cache. Use --dumpcache to identify objects. 5270 5271 5272=head2 REFRESH 5273 5274In daemon mode postfwd3 reloads it's ruleset after receiving a HUP signal. Please see the description of 5275the '-I' switch to have your configuration refreshed for every request postfwd3 receives. 5276 5277 5278=head2 EXAMPLES 5279 5280 ## whitelisting 5281 # 1. networks 192.168.1.0/24, 192.168.2.4 5282 # 2. client_names *.gmx.net and *.gmx.de 5283 # 3. sender *@someshop.tld from 11.22.33.44 5284 id=WL001; action=dunno ; client_address=192.168.1.0/24, 192.168.2.4 5285 id=WL002; action=dunno ; client_name=\.gmx\.(net|de)$ 5286 id=WL003; action=dunno ; sender=@someshop\.tld$ ; client_address=11.22.33.44 5287 5288 ## TLS control 5289 # 1. *@authority.tld only with correct TLS fingerprint 5290 # 2. *@secret.tld only with keysizes >=64 5291 id=TL001; action=dunno ; sender=@authority\.tld$ ; ccert_fingerprint=AA:BB:CC.. 5292 id=TL002; action=REJECT wrong TLS fingerprint ; sender=@authority\.tld$ 5293 id=TL003; action=REJECT tls keylength < 64 ; sender=@secret\.tld$ ; encryption_keysize=64 5294 5295 ## Combined RBL checks 5296 # This will reject mail if 5297 # 1. listed on ix.dnsbl.manitu.net 5298 # 2. listed on zen.spamhaus.org (sbl and xbl, dns cache timeout 1200s instead of 3600s) 5299 # 3. listed on min 2 of bl.spamcop.net, list.dsbl.org, dnsbl.sorbs.net 5300 # 4. listed on bl.spamcop.net and one of rhsbl.ahbl.org, rhsbl.sorbs.net 5301 id=RBL01 ; action=REJECT listed on ix.dnsbl.manitu.net ; rbl=ix.dnsbl.manitu.net 5302 id=RBL02 ; action=REJECT listed on zen.spamhaus.org ; rbl=zen.spamhaus.org/127.0.0.[2-8]/1200 5303 id=RBL03 ; action=REJECT listed on too many RBLs ; rblcount=2 ; rbl=bl.spamcop.net, list.dsbl.org, dnsbl.sorbs.net 5304 id=RBL04 ; action=REJECT combined RBL+RHSBL check ; rbl=bl.spamcop.net ; rhsbl=rhsbl.ahbl.org, rhsbl.sorbs.net 5305 5306 ## Message size (requires message_size_limit to be set to 30000000) 5307 # 1. 30MB for systems in *.customer1.tld 5308 # 2. 20MB for SASL user joejob 5309 # 3. 10MB default 5310 id=SZ001; protocol_state==END-OF-MESSAGE; action=DUNNO; size<=30000000 ; client_name=\.customer1.tld$ 5311 id=SZ002; protocol_state==END-OF-MESSAGE; action=DUNNO; size<=20000000 ; sasl_username==joejob 5312 id=SZ002; protocol_state==END-OF-MESSAGE; action=DUNNO; size<=10000000 5313 id=SZ100; protocol_state==END-OF-MESSAGE; action=REJECT message too large 5314 5315 ## Selective Greylisting 5316 ## 5317 ## Note that postfwd does not include greylisting. This setup requires a running postgrey service 5318 ## at port 10031 and the following postfix restriction class in your main.cf: 5319 ## 5320 ## smtpd_restriction_classes = check_postgrey, ... 5321 ## check_postgrey = check_policy_service inet:127.0.0.1:10031 5322 # 5323 # 1. if listed on zen.spamhaus.org with results 127.0.0.10 or .11, dns cache timeout 1200s 5324 # 2. Client has no rDNS 5325 # 3. Client comes from several dialin domains 5326 id=GR001; action=check_postgrey ; rbl=dul.dnsbl.sorbs.net, zen.spamhaus.org/127.0.0.1[01]/1200 5327 id=GR002; action=check_postgrey ; client_name=^unknown$ 5328 id=GR003; action=check_postgrey ; client_name=\.(t-ipconnect|alicedsl|ish)\.de$ 5329 5330 ## Date Time 5331 date=24.12.2007-26.12.2007 ; action=450 4.7.1 office closed during christmas 5332 time=04:00:00-05:00:00 ; action=450 4.7.1 maintenance ongoing, try again later 5333 time=-07:00:00 ; sasl_username=jim ; action=450 4.7.1 to early for you, jim 5334 time=22:00:00- ; sasl_username=jim ; action=450 4.7.1 to late now, jim 5335 months=-Apr ; action=450 4.7.1 see you in may 5336 days=!!Mon-Fri ; action=check_postgrey 5337 5338 ## Usage of jump 5339 # The following allows a message size of 30MB for different 5340 # users/clients while others will only have 10MB. 5341 id=R001 ; action=jump(R100) ; sasl_username=^(Alice|Bob|Jane)$ 5342 id=R002 ; action=jump(R100) ; client_address=192.168.1.0/24 5343 id=R003 ; action=jump(R100) ; ccert_fingerprint=AA:BB:CC:DD:... 5344 id=R004 ; action=jump(R100) ; ccert_fingerprint=AF:BE:CD:DC:... 5345 id=R005 ; action=jump(R100) ; ccert_fingerprint=DD:CC:BB:DD:... 5346 id=R099 ; protocol_state==END-OF-MESSAGE; action=REJECT message too big (max. 10MB); size=10000000 5347 id=R100 ; protocol_state==END-OF-MESSAGE; action=REJECT message too big (max. 30MB); size=30000000 5348 5349 ## Usage of score 5350 # The following rejects a mail, if the client 5351 # - is listed on 1 RBL and 1 RHSBL 5352 # - is listed in 1 RBL or 1 RHSBL and has no correct rDNS 5353 # - other clients without correct rDNS will be greylist-checked 5354 # - some whitelists are used to lower the score 5355 id=S01 ; score=2.6 ; action=check_postgrey 5356 id=S02 ; score=5.0 ; action=REJECT postfwd score too high 5357 id=R00 ; action=score(-1.0) ; rbl=exemptions.ahbl.org,list.dnswl.org,query.bondedsender.org,spf.trusted-forwarder.org 5358 id=R01 ; action=score(2.5) ; rbl=bl.spamcop.net, list.dsbl.org, dnsbl.sorbs.net 5359 id=R02 ; action=score(2.5) ; rhsbl=rhsbl.ahbl.org, rhsbl.sorbs.net 5360 id=N01 ; action=score(-0.2) ; client_name==$$helo_name 5361 id=N02 ; action=score(2.7) ; client_name=^unknown$ 5362 ... 5363 5364 ## Usage of rate and size 5365 # The following temporary rejects requests from "unknown" clients, if they 5366 # 1. exceeded 30 requests per hour or 5367 # 2. tried to send more than 1.5mb within 10 minutes 5368 id=RATE01 ; client_name==unknown ; protocol_state==RCPT 5369 action=rate(client_address/30/3600/450 4.7.1 sorry, max 30 requests per hour) 5370 id=SIZE01 ; client_name==unknown ; protocol_state==END-OF-MESSAGE 5371 action=size(client_address/1572864/600/450 4.7.1 sorry, max 1.5mb per 10 minutes) 5372 5373 ## Macros 5374 # definition 5375 &&RBLS { rbl=zen.spamhaus.org,list.dsbl.org,bl.spamcop.net,dnsbl.sorbs.net,ix.dnsbl.manitu.net; }; 5376 &&GONOW { action=REJECT your request caused our spam detection policy to reject this message. More info at http://www.domain.local; }; 5377 # rules 5378 &&GONOW ; &&RBLS ; client_name=^unknown$ 5379 &&GONOW ; &&RBLS ; client_name=(\d+[\.-_]){4} 5380 &&GONOW ; &&RBLS ; client_name=[\.-_](adsl|dynamic|ppp|)[\.-_] 5381 5382 ## Groups 5383 # definition 5384 &&RBLS{ 5385 rbl=zen.spamhaus.org 5386 rbl=list.dsbl.org 5387 rbl=bl.spamcop.net 5388 rbl=dnsbl.sorbs.net 5389 rbl=ix.dnsbl.manitu.net 5390 }; 5391 &&RHSBLS{ 5392 ... 5393 }; 5394 &&DYNAMIC{ 5395 client_name==unknown 5396 client_name~=(\d+[\.-_]){4} 5397 client_name~=[\.-_](adsl|dynamic|ppp|)[\.-_] 5398 ... 5399 }; 5400 &&BAD_HELO{ 5401 helo_name==my.name.tld 5402 helo_name~=^([^\.]+)$ 5403 helo_name~=\.(local|lan)$ 5404 ... 5405 }; 5406 &&MAINTENANCE{ 5407 date=15.01.2007 5408 date=15.04.2007 5409 date=15.07.2007 5410 date=15.10.2007 5411 time=03:00:00 - 04:00:00 5412 }; 5413 # rules 5414 id=COMBINED ; &&RBLS ; &&DYNAMIC ; action=REJECT dynamic client and listed on RBL 5415 id=MAINTENANCE ; &&MAINTENANCE ; action=DEFER maintenance time - please try again later 5416 5417 # now with the set() command, note that long item 5418 # lists don't have to be compared twice 5419 id=RBL01 ; &&RBLS ; action=set(HIT_rbls=1) 5420 id=HELO01 ; &&BAD_HELO ; action=set(HIT_helo=1) 5421 id=DYNA01 ; &&DYNAMIC ; action=set(HIT_dyna=1) 5422 id=REJECT01 ; HIT_rbls==1 ; HIT_helo==1 ; action=REJECT please see http://some.org/info?reject=01 for more info 5423 id=REJECT02 ; HIT_rbls==1 ; HIT_dyna==1 ; action=REJECT please see http://some.org/info?reject=02 for more info 5424 id=REJECT03 ; HIT_helo==1 ; HIT_dyna==1 ; action=REJECT please see http://some.org/info?reject=03 for more info 5425 5426 ## combined with enhanced rbl features 5427 # 5428 id=RBL01 ; rhsblcount=all ; rblcount=all ; &&RBLS ; &&RHSBLS 5429 action=set(HIT_dnsbls=$$rhsblcount,HIT_dnsbls+=$$rblcount,HIT_dnstxt=$$dnsbltext) 5430 id=RBL02 ; HIT_dnsbls>=2 ; action=554 5.7.1 blocked using $$HIT_dnsbls DNSBLs [INFO: $$HIT_dnstxt] 5431 5432 5433=head2 PARSER 5434 5435I<Configuration> 5436 5437The postfwd3 ruleset can be specified at the commandline (-r option) or be read from files (-f). The order of your arguments will be kept. You should 5438check the parser with the -C | --showconfig switch at the command line before applying a new config. The following call: 5439 5440 postfwd3 --showconfig \ 5441 -r "id=TEST; recipient_count=100; action=WARN mail with 100+ recipients" \ 5442 -f /etc/postfwd.cf \ 5443 -r "id=DEFAULT; action=dunno"; 5444 5445will produce the following output: 5446 5447 Rule 0: id->"TEST" action->"WARN mail with 100+ recipients"; recipient_count->"100" 5448 ... 5449 ... <content of /etc/postfwd.cf> ... 5450 ... 5451 Rule <n>: id->"DEFAULT" action->"dunno" 5452 5453Multiple items of the same type will be added to lists (see the L</ITEMS> section for more info): 5454 5455 postfwd3 --showconfig \ 5456 -r "client_address=192.168.1.0/24; client_address=172.16.26.32; action=dunno" 5457 5458will result in: 5459 5460 Rule 0: id->"R-0"; action->"dunno"; client_address->"192.168.1.0/24, 172.16.26.32" 5461 5462Macros are evaluated at configuration stage, which means that 5463 5464 postfwd3 --showconfig \ 5465 -r "&&RBLS { rbl=bl.spamcop.net; client_name=^unknown$; };" \ 5466 -r "id=RBL001; &&RBLS; action=REJECT listed on spamcop and bad rdns"; 5467 5468will result in: 5469 5470 Rule 0: id->"RBL001"; action->"REJECT listed on spamcop and bad rdns"; rbl->"bl.spamcop.net"; client_name->"^unknown$" 5471 5472I<Request processing> 5473 5474When a policy delegation request arrives it will be compared against postfwd`s ruleset. To inspect the processing in detail you should increase 5475verbority using use the "-v" or "-vv" switch. "-L" redirects log messages to stdout. 5476 5477Keeping the order of the ruleset in general, items will be compared in random order, which basically means that 5478 5479 id=R001; action=dunno; client_address=192.168.1.1; sender=bob@alice.local 5480 5481equals to 5482 5483 id=R001; sender=bob@alice.local; client_address=192.168.1.1; action=dunno 5484 5485Lists will be evaluated in the specified order. This allows to place faster expressions at first: 5486 5487 postfwd3 -vv --cmd -r "id=RBL001; rbl=localrbl.local zen.spamhaus.org; action=REJECT" /some/where/request.sample 5488 5489produces the following 5490 5491 [LOGS info]: compare rbl: "remotehost.remote.net[68.10.1.7]" -> "localrbl.local" 5492 [LOGS info]: count1 rbl: "2" -> "0" 5493 [LOGS info]: query rbl: localrbl.local 7.1.10.68 (7.1.10.68.localrbl.local) 5494 [LOGS info]: count2 rbl: "2" -> "0" 5495 [LOGS info]: match rbl: FALSE 5496 [LOGS info]: compare rbl: "remotehost.remote.net[68.10.1.7]" -> "zen.spamhaus.org" 5497 [LOGS info]: count1 rbl: "2" -> "0" 5498 [LOGS info]: query rbl: zen.spamhaus.org 7.1.10.68 (7.1.10.68.zen.spamhaus.org) 5499 [LOGS info]: count2 rbl: "2" -> "0" 5500 [LOGS info]: match rbl: FALSE 5501 [LOGS info]: Action: dunno 5502 5503The negation operator !!(<value>) has the highest priority and therefore will be evaluated first. Then variable substitutions are performed: 5504 5505 postfwd3 -vv --cmd -r "id=TEST; action=REJECT; client_name=!!($$heloname)" /some/where/request.sample 5506 5507will give 5508 5509 [LOGS info]: compare client_name: "unknown" -> "!!($$helo_name)" 5510 [LOGS info]: negate client_name: "unknown" -> "$$helo_name" 5511 [LOGS info]: substitute client_name: "unknown" -> "english-breakfast.cloud8.net" 5512 [LOGS info]: match client_name: TRUE 5513 [LOGS info]: Action: REJECT 5514 5515 5516I<Ruleset evaluation> 5517 5518A rule hits when all items (or at least one element of a list for each item) have matched. As soon as one item (or all elements of a list) fails 5519to compare against the request attribute the parser will jump to the next rule in the postfwd3 ruleset. 5520 5521If a rule matches, there are two options: 5522 5523* Rule returns postfix action (dunno, reject, ...) 5524The parser stops rule processing and returns the action to postfix. Other rules will not be evaluated. 5525 5526* Rule returns postfwd3 action (jump(), note(), ...) 5527The parser evaluates the given action and continues with the next rule (except for the jump() or quit() actions - please see the L</ACTIONS> section 5528for more information). Nothing will be sent to postfix. 5529 5530If no rule has matched and the end of the ruleset is reached postfwd3 will return dunno without logging anything unless in verbose mode. You may 5531place a last catch-all rule to change that behaviour: 5532 5533 ... <your rules> ... 5534 id=DEFAULT ; action=dunno 5535 5536will log any request that passes the ruleset without having hit a prior rule. 5537 5538 5539=head2 SETTINGS 5540 5541Since version postfwd3 2.00-pre6 the program settings can be retrieved from a file. The information must be 5542parseable by perl eval() function. Comments are allowed: 5543 5544 # example settings for postfwd3 5545 { 5546 # enable verbose logging 5547 verbose => 1, 5548 5549 # user and group for postfwd 5550 base => { 5551 group => 'postfw', 5552 user => 'postfw' 5553 }, 5554 5555 # server settings 5556 server => { 5557 proto => 'tcp', 5558 host => '127.0.0.1', 5559 port => '10099' 5560 } 5561 } 5562 5563postfwd3 can also export the program settings in that format using Data::Dumper. 5564 5565 # show configuration 5566 postfwd3 --showsettings 5567 5568 # save configuration to file 5569 postfwd3 --savesettings=/path/to/file 5570 5571 # read configuration from file 5572 postfwd3 --loadsettings=/path/to/file 5573 5574Multiple usage of --loadsettings or the short form -F is allowed. The order will be kept. 5575Commandline arguments override the settings retrieved from a file. 5576 5577 # load base settings, override with node settings and set port and verbose logging 5578 postfwd3 -v -p 10050 -F /path/to/basefile -F /path/to/nodefile 5579 5580Samples for settings files are distributed in the etc/-folder of the postfwd3 tarball. 5581 5582 5583=head2 DEBUGGING 5584 5585To debug special steps of the parser the '--debug' switch takes a list of debug classes. Since postfwd3 2.00 5586the list of available debug classes can be retrieved by running: 5587 5588 postfwd3 --debugclasses 5589 5590 5591=head2 INTEGRATION 5592 5593I<Integration via daemon mode> 5594 5595The common way to use postfwd3 is to start it as daemon, listening at a specified tcp port. 5596postfwd3 will spawn multiple child processes which communicate with a parent cache. This is 5597the prefered way to use postfwd3 in high volume environments. Start postfwd3 with the following parameters: 5598 5599 postfwd3 -d -f /etc/postfwd.cf -i 127.0.0.1 -p 10045 -u nobody -g nobody -S 5600 5601For efficient caching you should check if you can use the options --cacheid, --cache-rdomain-only, 5602--cache-no-sender and --cache-no-size. 5603 5604Now check your syslogs (default facility "mail") for a line like: 5605 5606 Aug 9 23:00:24 mail postfwd[5158]: postfwd3 n.nn ready for input 5607 5608and use `netstat -an|grep 10045` to check for something like 5609 5610 tcp 0 0 127.0.0.1:10045 0.0.0.0:* LISTEN 5611 5612If everything works, open your postfix main.cf and insert the following 5613 5614 127.0.0.1:10045_time_limit = 3600 <--- integration 5615 smtpd_recipient_restrictions = permit_mynetworks <--- recommended 5616 reject_unauth_destination <--- recommended 5617 check_policy_service inet:127.0.0.1:10045 <--- integration 5618 5619Reload your configuration with `postfix reload` and watch your logs. In it works you should see 5620lines like the following in your mail log: 5621 5622 Aug 9 23:01:24 mail postfwd[5158]: rule=22, id=ML_POSTFIX, client=english-breakfast.cloud9.net[168.100.1.7], sender=owner-postfix-users@postfix.tld, recipient=someone@domain.local, helo=english-breakfast.cloud9.net, proto=ESMTP, state=RCPT, action=dunno 5623 5624If you want to check for size or rcpt_count items you must integrate postfwd3 in smtp_data_restrictions or 5625smtpd_end_of_data_restrictions. Of course you can also specify a restriction class and use it in your access 5626tables. First create a file /etc/postfix/policy containing: 5627 5628 domain1.local postfwdcheck 5629 domain2.local postfwdcheck 5630 ... 5631 5632Then postmap that file (`postmap hash:/etc/postfix/policy`), open your main.cf and enter 5633 5634 # Restriction Classes 5635 smtpd_restriction_classes = postfwdcheck, <some more>... <--- integration 5636 postfwdcheck = check_policy_service inet:127.0.0.1:10045 <--- integration 5637 5638 127.0.0.1:10045_time_limit = 3600 <--- integration 5639 smtpd_recipient_restrictions = permit_mynetworks, <--- recommended 5640 reject_unauth_destination, <--- recommended 5641 ... <--- optional 5642 check_recipient_access hash:/etc/postfix/policy, <--- integration 5643 ... <--- optional 5644 5645Reload postfix and watch your logs. 5646 5647I<Integration via docker> 5648 5649postfwd can be run in a docker container. The relevant options are --nodaemon and --stdout. 5650More information can be found in the included doc/docker.html or at L<http://postfwd.org/docker>. 5651 5652 5653=head2 TESTING 5654 5655First you have to create a ruleset (see Configuration section). Check it with 5656 5657 postfwd3 -f /etc/postfwd.cf -C 5658 5659There is an example policy request distributed with postfwd, called 'request.sample'. 5660Simply change it to meet your requirements and use 5661 5662 postfwd3 --cmd -f /etc/postfwd.cf request.sample 5663 5664You should get an answer like 5665 5666 action=<whateveryouconfigured> 5667 5668For network tests I use netcat: 5669 5670 nc 127.0.0.1 10045 <request.sample 5671 5672to send a request to postfwd. If you receive nothing, make sure that postfwd3 is running and 5673listening on the specified network settings. 5674 5675 5676=head2 PERFORMANCE 5677 5678Some of these proposals might not match your environment. Please check your requirements and test new options carefully! 5679 5680 - use caching options 5681 - use the correct match operator ==, <=, >= 5682 - use ^ and/or $ in regular expressions 5683 - use item lists (faster than single rules) 5684 - use set() action on repeated item lists 5685 - use jumps and rate limits 5686 - use a pre-lookup rule for rbl/rhsbls with empty note() action 5687 5688 5689=head2 SEE ALSO 5690 5691See L<http://www.postfix.org/SMTPD_POLICY_README.html> for a description 5692of how Postfix policy servers work. 5693 5694 5695=head1 DONATIONS 5696 5697Development, testing and hosting of postfwd consumes time and ressources. It is and will stay free software 5698(see "LICENSE" below). If you want to support this, consider a donation at https://www.paypal.me/postfwd. 5699 5700 5701=head1 LICENSE 5702 5703postfwd3 is free software and released under BSD license, which basically means 5704that you can do what you want as long as you keep the copyright notice: 5705 5706Copyright (c) 2009, Jan Peter Kessler 5707All rights reserved. 5708 5709Redistribution and use in source and binary forms, with or without modification, 5710are permitted provided that the following conditions are met: 5711 5712 * Redistributions of source code must retain the above copyright 5713 notice, this list of conditions and the following disclaimer. 5714 * Redistributions in binary form must reproduce the above copyright 5715 notice, this list of conditions and the following disclaimer in 5716 the documentation and/or other materials provided with the 5717 distribution. 5718 * Neither the name of the authors nor the names of his contributors 5719 may be used to endorse or promote products derived from this 5720 software without specific prior written permission. 5721 5722THIS SOFTWARE IS PROVIDED BY ME ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, 5723INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 5724FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, 5725INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 5726NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 5727PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 5728WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 5729ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 5730POSSIBILITY OF SUCH DAMAGE. 5731 5732 5733=head1 AUTHOR 5734 5735S<Jan Peter Kessler E<lt>info (AT) postfwd (DOT) orgE<gt>>. Let me know, if you have any suggestions. 5736 5737=cut 5738 5739