]> git.proxmox.com Git - pmg-api.git/blame - bin/pmgpolicy
pmgpolicy: import from old repository
[pmg-api.git] / bin / pmgpolicy
CommitLineData
ddaec666
DM
1#!/usr/bin/perl -w
2
3use strict;
4use Carp;
5use Getopt::Long;
6use Proxmox::SafeSyslog;
7use POSIX qw(errno_h signal_h);
8
9use Net::Server::PreForkSimple;
10use Net::DNS::Resolver;
11use Mail::SPF;
12use Fcntl;
13use Fcntl ':flock';
14use IO::Multiplex;
15use Time::HiRes qw (gettimeofday);
16
17use Proxmox::Utils;
18use Proxmox::RuleDB;
19use Proxmox::Config::System;
20use Proxmox::Commtouch;
21use Proxmox::Cluster;
22
23use vars qw(@ISA);
24@ISA = qw(Net::Server::PreForkSimple);
25
26my $greylist_delay = 3*60; # greylist window
27my $greylist_lifetime = 3600*24*2; # retry window
28my $greylist_awlifetime = 3600*24*36; # expire window
29
30my $opt_commandline = [$0, @ARGV];
31my $opt_policy_port = 10022;
32my $opt_max_dequeue = 1;
33my $opt_dequeue_time = 60*2;
34
35my $opt_testmode;
36my $opt_pidfile;
37my $opt_database;
38
39if (!GetOptions ('pidfile=s' => \$opt_pidfile,
40 'testmode' => \$opt_testmode,
41 'database=s' => \$opt_database)) {
42 die "usage error\n";
43 exit (-1);
44}
45
46$opt_pidfile = "/var/run/proxpolicy.pid" if !$opt_pidfile;
47$opt_max_dequeue = 0 if $opt_testmode;
48
49initlog ('proxpolicy', 'mail');
50
51if (!$opt_testmode) {
52
53 my $system_cfg = Proxmox::Config::System->new ();
54 my $demo = $system_cfg->get ('administration', 'demo');
55
56 if ($demo) {
57 syslog ('info', 'demo mode detected - not starting server');
58 exit (0);
59 }
60}
61
62my $daemonize = 1;
63if (defined ($ENV{BOUND_SOCKETS})) {
64 $daemonize = undef;
65}
66
67my $max_servers = Proxmox::Utils::get_max_policy ();
68
69my $server_attr = {
70 port => [ $opt_policy_port ],
71 host => '127.0.0.1',
72 max_servers => $max_servers,
73 max_dequeue => $opt_max_dequeue,
74 check_for_dequeue => $opt_dequeue_time,
75 log_level => 3,
76 pid_file => $opt_pidfile,
77 commandline => $opt_commandline,
78 no_close_by_child => 1,
79 setsid => $daemonize,
80};
81
82my $database;
83if (defined($opt_database)) {
84 $database = $opt_database;
85} else {
86 $database = "Proxmox_ruledb";
87}
88
89$SIG{'__WARN__'} = sub {
90 my $err = $@;
91 my $t = $_[0];
92 chomp $t;
93 syslog('warning', "WARNING: %s", $t);
94 $@ = $err;
95};
96
97sub run_dequeue {
98 my $self = shift;
99
100 $self->log (2, "starting greylist database maintainance");
101
102 my ($csec, $usec) = gettimeofday ();
103
104 my $system_cfg = Proxmox::Config::System->new ();
105
106 my $cinfo = $system_cfg->clusterinfo();
107 my $lcid = $cinfo->{local}->{cid};
108
109 my $dbh;
110
111 eval {
112 $dbh = Proxmox::Utils::open_ruledb ($database);
113 };
114 my $err = $@;
115
116 if ($err) {
117 $self->log (0, msgquote ("ERROR: $err"));
118 return;
119 }
120
121 my $now = time ();
122
123 my $ecount = 0;
124
125 # update RBL stats in DailyStats table
126 # only write if on master node
127 if ($cinfo->{local}->{role} eq '-' || $cinfo->{local}->{role} eq 'M') {
128 eval {
129 my $stats = Proxmox::Commtouch::get_ctipd_stats();
130 Proxmox::Cluster::update_rbl_stats($dbh, 'localhost', $stats);
131 };
132 if (my $err = $@) {
133 $self->log (0, "ERROR: update_rbl_stats - %s", $err);
134 }
135 };
136
137 eval {
138
139 $dbh->begin_work;
140
141 # we do not lock the table here to avoid delays
142 # but that is OK, because we only touch expired records
143 # which do not change nornmally
144 ## $dbh->do ("LOCK TABLE CGreylist IN ROW EXCLUSIVE MODE");
145
146 # move expired and undelivered records from Greylist to Statistic
147
148 my $rntxt = '';
149 if (!$lcid) {
150 $rntxt = "AND CID = 0";
151 } else {
152 if ($cinfo->{local}->{role} eq 'M') {
153 # master is responsible for all non-cluster (deleted) nodes
154 foreach my $rcid (@{$cinfo->{remnodes}}) {
155 $rntxt .= $rntxt ? " AND CID != $rcid" : "AND (CID != $rcid";
156 }
157 $rntxt .= ")" if $rntxt;
158 } else {
159 $rntxt = "AND (CID = 0 OR CID = $lcid)";
160 }
161 }
162
163
164 my $cmds = '';
165
166 my $sth = $dbh->prepare ("SELECT distinct instance, sender FROM CGreylist " .
167 "WHERE passed = 0 AND extime < ? $rntxt");
168
169 $sth->execute ($now);
170
171
172 while (my $ref = $sth->fetchrow_hashref()) {
173 my $sth2 = $dbh->prepare ("SELECT * FROM CGreylist " .
174 "WHERE instance = ? AND sender = ?");
175
176 $sth2->execute ($ref->{instance}, $ref->{sender});
177 my $rctime;
178 my @rcvrs;
179 my $bc = 0;
180
181 while (my $ref2 = $sth2->fetchrow_hashref()) {
182 $rctime = $ref2->{rctime} if !$rctime;
183 $bc += $ref2->{blocked};
184 push @rcvrs, $ref2->{receiver};
185 }
186
187 $sth2->finish();
188
189 # hack: sometimes query sth2 does not return anything - maybe a
190 # postgres bug? We simply ignore (when rctime is undefined) it
191 # to avoid problems.
192
193 if ($rctime) {
194 $cmds .= "SELECT nextval ('cstatistic_id_seq');INSERT INTO CStatistic " .
195 "(CID, RID, ID, Time, Bytes, Direction, Spamlevel, VirusInfo, PTime, Sender) VALUES (" .
196 "$lcid, currval ('cstatistic_id_seq'), currval ('cstatistic_id_seq'), ";
197
198 my $sl = $bc >= 100000 ? 4 : 5;
199 $cmds .= $rctime . ", 0, '1', $sl, NULL, 0, ";
200 $cmds .= $dbh->quote ($ref->{sender}) . ');';
201
202 foreach my $r (@rcvrs) {
203 my $tmp = $dbh->quote ($r);
204 $cmds .= "INSERT INTO CReceivers (CStatistic_CID, CStatistic_RID, Receiver, Blocked) ".
205 "VALUES ($lcid, currval ('cstatistic_id_seq'), $tmp, '1'); ";
206 }
207 }
208
209 if (length ($cmds) > 100000) {
210 $dbh->do ($cmds);
211 $cmds = '';
212 }
213
214
215 $ecount++;
216
217 # this produces too much log traffic
218 # my $targets = join (", ", @rcvrs);
219 #my $msg = "expire mail $ref->{instance} from $ref->{sender} to $targets";
220 #$self->log (0, msgquote ($msg));
221 }
222
223 $dbh->do ($cmds) if $cmds;
224
225 $sth->finish();
226
227 if ($ecount > 0) {
228 my $msg = "found $ecount expired mails in greylisting database";
229 $self->log (0, $msg);
230 }
231
232 $dbh->do ("DELETE FROM CGreylist WHERE extime < $now");
233
234 $dbh->commit;
235 };
236
237 $err = $@;
238
239 my ($csec_end, $usec_end) = gettimeofday ();
240 my $ptime = int (($csec_end-$csec)*1000 + ($usec_end - $usec)/1000);
241
242 if ($err) {
243 $dbh->rollback if $dbh;
244 $self->log (0, msgquote ($err));
245 } else {
246 $self->log (2, "end greylist database maintainance ($ptime ms)");
247 }
248
249 $dbh->disconnect() if $dbh;
250}
251
252sub pre_loop_hook {
253 my $self = shift;
254 my $prop = $self->{server};
255
256 $prop->{log_level} = 3;
257
258 $self->log (0, "Policy daemon (re)started");
259
260 $SIG{'USR1'} = sub {
261 # reloading server configuration
262 if (defined $prop->{children}) {
263 foreach my $pid (keys %{$prop->{children}}) {
264 kill (10, $pid); # SIGUSR1 childs
265 }
266 }
267 };
268
269 my $sig_set = POSIX::SigSet->new;
270 $sig_set->addset (&POSIX::SIGHUP);
271 $sig_set->addset (&POSIX::SIGCHLD);
272 my $old_sig_set = POSIX::SigSet->new();
273
274 sigprocmask (SIG_UNBLOCK, $sig_set, $old_sig_set);
275}
276
277sub load_config {
278 my $self = shift;
279 my $prop = $self->{server};
280
281 if ($self->{ruledb}) {
282 $self->log (0, "reloading configuration $database");
283 $self->{ruledb}->close ();
284 }
285
286 my $system_cfg = Proxmox::Config::System->new ();
287 $self->{use_rbl} = $system_cfg->get ('mail', 'use_rbl');
288 $self->{use_spf} = $system_cfg->get ('mail', 'spf');
289 $self->{use_greylist} = $system_cfg->get ('mail', 'greylist');
290 my $hostname = $system_cfg->get ('dns', 'hostname');
291 my $domain = $system_cfg->get ('dns', 'domain');
292 $self->{fqdn} = "$hostname.$domain";
293 my $cinfo = $system_cfg->clusterinfo();
294 my $lcid = $cinfo->{local}->{cid};
295 $self->{cinfo} = $cinfo;
296 $self->{lcid} = $lcid;
297
298 my $lic = $system_cfg->{license};
299 $self->{lic_valid} = ($lic->valid && !$lic->expired) ? 1 : 0;
300
301 my $dbh;
302
303 eval {
304 $dbh = Proxmox::Utils::open_ruledb ($database);
305
306 $self->{ruledb} = Proxmox::RuleDB->new ($dbh);
307 $self->{rulecache} = Proxmox::RuleDB::RuleCache->new ($self->{ruledb});
308 };
309
310 my $err = $@;
311
312 if ($err) {
313 $self->log (0, msgquote ("ERROR: unable to load database : $err"));
314 }
315
316 $self->{reload_config} = 0;
317}
318
319sub child_init_hook {
320 my $self = shift;
321 my $prop = $self->{server};
322
323 $0 = 'proxpolicy child';
324
325 setup_fork_signal_mask (0); # unblocking signals for children
326
327 eval {
328 $self->load_config ();
329
330 $self->{mux} = new IO::Multiplex;
331 $self->{mux}->set_callback_object ($self);
332
333 my %dnsargs = (
334 tcp_timeout => 3,
335 udp_timeout => 3,
336 retry => 1,
337 retrans => 0,
338 dnsrch => 0,
339 defnames => 0,
340 );
341
342 if ($opt_testmode) {
343 # $dnsargs{nameservers} = [ qw (213.129.232.1 213.129.226.2) ];
344 }
345
346 $self->{dns_resolver} = Net::DNS::Resolver->new (%dnsargs);
347
348 $self->{spf_server} = Mail::SPF::Server->new (hostname => $self->{fqdn},
349 dns_resolver => $self->{dns_resolver});
350
351 };
352
353 if ($@) {
354 $self->log (0, msgquote ($@));
355 $self->child_finish_hook;
356 exit;
357 }
358
359 $SIG{'USR1'} = sub {
360 $self->{reload_config} = 1;
361 }
362}
363
364sub child_finish_hook {
365 my $self = shift;
366 my $prop = $self->{server};
367
368 $self->{ruledb}->close () if $self->{ruledb};
369}
370
371
372sub get_spf_result {
373 my ($self, $instance, $ip, $helo, $sender) = @_;
374
375 my $result;
376 my $spf_header;
377 my $local_expl;
378 my $auth_expl;
379
380 # we only use helo tests when we have no sender,
381 # helo is sometimes empty, so we cant use SPF helo tests
382 # in that case - strange
383 if ($helo && !$sender) {
384 my $query;
385
386 if (defined ($self->{cache}->{$instance}) &&
387 defined ($self->{cache}->{$instance}->{spf_helo_result})) {
388
389 $query = $self->{cache}->{$instance}->{spf_helo_result};
390
391 } else {
392 my $request = Mail::SPF::Request->new (scope => 'helo',
393 identity => $helo,
394 ip_address => $ip);
395
396 $query = $self->{cache}->{$instance}->{spf_helo_result} =
397 $self->{spf_server}->process ($request);
398 }
399
400 $result = $query->code;
401 $spf_header = $query->received_spf_header;
402 $local_expl = $query->local_explanation;
403 $auth_expl = $query->authority_explanation if $query->is_code('fail');
404
405 # return if we get a definitive result
406 if ($result eq 'pass' || $result eq 'fail' || $result eq 'temperror') {
407 return ($result, $spf_header, $local_expl, $auth_expl);
408 }
409 }
410
411 if ($sender) {
412
413 my $query;
414
415 if (defined ($self->{cache}->{$instance}) &&
416 defined ($self->{cache}->{$instance}->{spf_mfrom_result})) {
417
418 $query = $self->{cache}->{$instance}->{spf_mfrom_result};
419
420 } else {
421
422 my $request = Mail::SPF::Request->new (scope => 'mfrom',
423 identity => $sender,
424 ip_address => $ip,
425 helo_identity => $helo);
426
427 $query = $self->{cache}->{$instance}->{spf_mfrom_result} =
428 $self->{spf_server}->process($request);
429 }
430
431 $result = $query->code;
432 $spf_header = $query->received_spf_header;
433 $local_expl = $query->local_explanation;
434 $auth_expl = $query->authority_explanation if $query->is_code('fail');
435
436 return ($result, $spf_header, $local_expl, $auth_expl);
437 }
438
439 return undef;
440}
441
442sub is_backup_mx {
443 my ($self, $ip, $receiver) = @_;
444
445 my ($rdomain) = $receiver =~ /([^@]+)$/;
446
447 my $dkey = "BKMX:$rdomain";
448
449 if (defined ($self->{cache}->{$dkey}) &&
450 ($self->{cache}->{$dkey}->{status} == 1)) {
451 return $self->{cache}->{$dkey}->{$ip};
452 }
453
454 my $resolver = $self->{dns_resolver};
455
456 if (my $mx = $resolver->send ($rdomain, 'MX')) {
457 $self->{cache}->{$dkey}->{status} = 1;
458 my @mxa = grep { $_->type eq 'MX' } $mx->answer;
459 my @mxl = sort { $a->preference <=> $b->preference } @mxa;
460 # shift @mxl; # optionaly skip primary MX ?
461 foreach my $rr (@mxl) {
462 my $a = $resolver->send ($rr->exchange, 'A');
463 if ($a) {
464 foreach my $rra ($a->answer) {
465 if ($rra->type eq 'A') {
466 $self->{cache}->{$dkey}->{$rra->address} = 1;
467 }
468 }
469 }
470 }
471 } else {
472 $self->{cache}->{$dkey}->{status} = 0;
473 }
474
475 return $self->{cache}->{$dkey}->{$ip};
476}
477
478sub greylist_value {
479 my ($self, $ctime, $helo, $ip, $sender, $rcpt, $instance) = @_;
480
481 my $rulecache = $self->{rulecache};
482
483 my $dbh = $self->{ruledb}->{dbh};
484
485 # try to reconnect if database connection is broken
486 if (!$dbh->ping) {
487 $self->log (0, 'Database connection broken - trying to reconnect');
488 my $dbh;
489 eval {
490 $dbh = Proxmox::Utils::open_ruledb ($database);
491 };
492 my $err = $@;
493 if ($err) {
494 $self->log (0, msgquote ("unable to reconnect to database server: $err"));
495 return 'dunno';
496 }
497 $self->{ruledb} = Proxmox::RuleDB->new ($dbh);
498 }
499
500 # some sender substitutions
501 my ($user, $domain) = split('@', $sender, 2);
502 if (defined ($user) && defined ($domain)) {
503 # see http://cr.yp.to/proto/verp.txt
504 $user =~ s/\+.*//; # strip extensions (mailing-list VERP)
505 $user =~ s/\b\d+\b/#/g; #replace nubmers in VERP address
506 $sender = "$user\@$domain";
507 }
508
509 if ($self->is_backup_mx ($ip, $rcpt)) {
510 $self->log (3, "accept mails from backup MX host - $ip");
511 return 'dunno';
512 }
513
514 # greylist exclusion (sender whitelist)
515 if ($rulecache->greylist_match ($sender, $ip)) {
516 $self->log (3, "accept mails from whitelist - $ip");
517 return 'dunno';
518 }
519
520 # we run commtouch test first for now
521 # only run with valid license
522 # NOTE: disabled - we use rbl_reject in postfix main.cf
523 if (0 && !$opt_testmode && $self->{use_rbl} && $self->{lic_valid}) {
524 my ($ct_action, $ct_refid);
525 my ($csec, $usec) = gettimeofday ();
526 eval {
527 ($ct_action, $ct_refid) = Proxmox::Commtouch::ct_scan_ip($ip);
528 };
529 my $err = $@;
530
531 my ($csec_end, $usec_end) = gettimeofday ();
532 my $ptime = int (($csec_end-$csec)*1000 + ($usec_end - $usec)/1000);
533
534 if ($err) {
535 $self->log (0, "ctipd error: $err ($ptime ms)");
536 } elsif ($ct_action && $ct_refid) {
537 if ($ct_action ne 'accept') {
538 $self->log (3, "ctipd action: $ct_action ($ip, $ct_refid, $ptime ms)");
539 if ($ct_action eq 'tempfail') {
540 my $msg = "delivery from $ip is deferred and despite repeated attempts, this message could not be delivered. Try again later and if same response, then check your IP reputation at http://www.commtouch.com/Site/Resources/Check_IP_Reputation.asp and make sure to provide the following reference code '${ct_refid}'";
541 return "defer_if_permit $msg";
542 } elsif ($ct_action eq 'permfail') {
543 my $msg = "delivery from $ip is rejected. Check your IP reputation at http://www.commtouch.com/Site/Resources/Check_IP_Reputation.asp and make sure to provide the following reference code '${ct_refid}'";
544 return "reject $msg";
545 }
546 }
547 }
548 }
549
550 # greylist exclusion (receiver whitelist)
551 if ($rulecache->greylist_match_receiver ($rcpt)) {
552 $self->log (3, "accept mails to whitelist - <$rcpt>");
553 return 'dunno';
554 }
555
556 my ($net, $host) = $ip =~ m/(\d+\.\d+\.\d+)\.(\d+)/;
557 my $spf_header;
558
559 if ((!$opt_testmode && $self->{use_spf}) ||
560 ($opt_testmode && ($rcpt =~ m/^testspf/))) {
561
562 # ask SPF
563 my $spf_result;
564 my $local_expl,
565 my $auth_expl;
566
567 my $previous_alarm;
568
569 my ($result, $smtp_comment, $header_comment);
570
571 eval {
572 $previous_alarm = alarm (10);
573 local $SIG{ALRM} = sub { die "SPF timeout\n" };
574
575 ($result, $spf_header, $local_expl, $auth_expl) =
576 $self->get_spf_result ($instance, $ip, $helo, $sender);
577 };
578
579 my $err = $@;
580
581 alarm ($previous_alarm) if defined($previous_alarm);
582
583 if ($err) {
584 $err = $err->text if UNIVERSAL::isa ($err, 'Mail::SPF::Exception');
585 $self->log (0, msgquote ($err));
586 } else {
587
588 if ($result && $result eq 'pass') {
589 $self->log (3, "SPF says $result");
590 $spf_result = $spf_header ? "prepend $spf_header" : 'dunno';
591 }
592
593 if ($result && $result eq 'fail') {
594 $self->log (3, "SPF says $result");
595 $spf_result = "reject ${auth_expl}";
596
597 eval {
598
599 $dbh->begin_work;
600
601 # try to avoid locks everywhere - we use merge instead of insert
602 #$dbh->do ("LOCK TABLE CGreylist IN ROW EXCLUSIVE MODE");
603
604 # check if there is already a record in the GL database
605 my $sth = $dbh->prepare ("SELECT * FROM CGreylist " .
606 "where IPNet = ? AND Sender = ? AND Receiver = ?");
607
608 $sth->execute ($net, $sender, $rcpt);
609 my $ref = $sth->fetchrow_hashref();
610 $sth->finish();
611
612 # else add an entry to the GL Database with short
613 # expiration time. run_dequeue() moves those entries into the statistic
614 # table later. We set 'blocked' to 100000 to identify those entries.
615
616 if (!defined ($ref->{rctime})) {
617
618 $dbh->do ("SELECT merge_greylist(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", undef,
619 $net, $host, $sender, $rcpt, $instance,
620 $ctime, $ctime + 10, 0, 100000, 0, $ctime, $self->{lcid});
621 }
622
623 $dbh->commit;
624 };
625
626 if ($@) {
627 $dbh->rollback;
628 $self->log (0, msgquote ($@));
629 }
630 }
631 }
632
633 return $spf_result if $spf_result;
634 }
635
636
637 my $res = $spf_header ? "prepend $spf_header" : 'dunno';
638
639 return $res if !$self->{use_greylist};
640
641 my $defer_res = "defer_if_permit Service is unavailable (try later)";
642
643 eval {
644
645 # we dont use alarm here, because it does not work with DBI
646
647 $dbh->begin_work;
648
649 # try to avoid locks everywhere - we use merge instead of insert
650 #$dbh->do ("LOCK TABLE CGreylist IN ROW EXCLUSIVE MODE");
651
652 my $sth = $dbh->prepare ("SELECT * FROM CGreylist " .
653 "where IPNet = ? AND Sender = ? AND Receiver = ?");
654
655 $sth->execute ($net, $sender, $rcpt);
656
657 my $ref = $sth->fetchrow_hashref();
658
659 $sth->finish();
660
661 if (!defined ($ref->{rctime})) {
662
663 $dbh->do ("SELECT merge_greylist(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", undef,
664 $net, $host, $sender, $rcpt, $instance,
665 $ctime, $ctime + $greylist_lifetime, 0, 1, 0, $ctime, $self->{lcid});
666
667 $res = $defer_res;
668 $self->log (3, "defer greylisted mail");
669 } else {
670 my $age = $ctime - $ref->{rctime};
671
672 if ($age < $greylist_delay) {
673 # defer (resent within greylist_delay window)
674 $res = $defer_res;
675 $self->log (3, "defer greylisted mail");
676 $dbh->do ("UPDATE CGreylist " .
677 "SET Blocked = Blocked + 1, Host = ?, MTime = ? " .
678 "WHERE IPNet = ? AND Sender = ? AND Receiver = ?", undef,
679 $host, $ctime, $net, $sender, $rcpt);
680 } else {
681 if ($ctime < $ref->{extime}) {
682 # accept (not expired)
683 my $lifetime = $sender eq "" ? 0 : $greylist_awlifetime;
684 my $delay = $ref->{passed} ? "" : "Delay = $age, ";
685 $dbh->do ("UPDATE CGreylist " .
686 "SET Passed = Passed + 1, $delay Host = ?, ExTime = ?, MTime = ? " .
687 "WHERE IPNet = ? AND Sender = ? AND Receiver = ?", undef,
688 $host, $ctime + $lifetime, $ctime, $net, $sender, $rcpt);
689 } else {
690 # defer (record is expired)
691 $res = $defer_res;
692 $dbh->do ("UPDATE CGreylist " .
693 "SET Host = ?, RCTime = ?, ExTime = ?, MTime = ?, Instance = ?, " .
694 "Blocked = 1, Passed = 0 " .
695 "WHERE IPNet = ? AND Sender = ? AND Receiver = ?", undef,
696 $host, $ctime, $ctime + $greylist_lifetime, $ctime, $instance,
697 $net, $sender, $rcpt);
698 }
699 }
700 }
701
702 $dbh->commit;
703 };
704
705 if ($@) {
706 $dbh->rollback;
707 $self->log (0, msgquote ($@));
708 }
709
710 return $res;
711}
712
713# shutdown connections: we need this - else file handles are
714# not closed and we run out of handles
715sub mux_eof {
716 my ($self, $mux, $fh) = @_;
717
718 $mux->shutdown($fh, 1);
719}
720
721sub mux_input {
722 my ($self, $mux, $fh, $dataref) = @_;
723 my $prop = $self->{server};
724
725 my $attribute = {};
726
727 eval {
728 $self->load_config() if $self->{reload_config};
729
730 while ($$dataref =~ s/^([^\r\n]*)\r?\n//) {
731 my $line = $1;
732 next if !defined ($line);
733
734 if ($line =~ m/([^=]+)=(.*)/) {
735 $attribute->{substr($1, 0, 255)} = substr($2, 0, 255);
736 } elsif ($line eq '') {
737 my $res = 'dunno';
738 my $ctime = time;
739
740 if ($opt_testmode) {
741 die "undefined test time :ERROR" if !defined $attribute->{testtime};
742 $ctime = $attribute->{testtime};
743 }
744
745 if ($attribute->{instance} && $attribute->{recipient} &&
746 $attribute->{client_address} && $attribute->{request} &&
747 $attribute->{request} eq 'smtpd_access_policy') {
748
749 eval {
750
751 $res = $self->greylist_value ($ctime,
752 lc ($attribute->{helo_name}),
753 lc ($attribute->{client_address}),
754 lc ($attribute->{sender}),
755 lc ($attribute->{recipient}),
756 lc ($attribute->{instance}));
757 };
758
759 $self->log (0, msgquote ($@)) if $@;
760 }
761
762 print $fh "action=$res\n\n";
763
764 $attribute = {};
765 } else {
766 $self->log (0, "greylist policy protocol error - got '%s'", $line);
767 }
768 }
769 };
770
771 # remove remaining data, if any
772 if ($$dataref ne '') {
773 $self->log (0, "greylist policy protocol error - unused data '%s'", $$dataref);
774 $$dataref = '';
775 }
776
777 $self->log (0, msgquote ($@)) if $@;
778}
779
780sub restart_close_hook {
781 my $self = shift;
782
783 my $sig_set = POSIX::SigSet->new;
784 $sig_set->addset (&POSIX::SIGHUP);
785 $sig_set->addset (&POSIX::SIGCHLD); # to avoid zombies
786 my $old_sig_set = POSIX::SigSet->new();
787
788 sigprocmask (SIG_BLOCK, $sig_set, $old_sig_set);
789}
790
791sub pre_server_close_hook {
792 my $self = shift;
793 my $prop = $self->{server};
794
795 if (defined $prop->{_HUP}) {
796 undef $prop->{pid_file_unlink};
797 }
798}
799
800sub setup_fork_signal_mask {
801 my $block = shift;
802
803 my $sig_set = POSIX::SigSet->new;
804 $sig_set->addset (&POSIX::SIGINT);
805 $sig_set->addset (&POSIX::SIGTERM);
806 $sig_set->addset (&POSIX::SIGQUIT);
807 $sig_set->addset (&POSIX::SIGHUP);
808 my $old_sig_set = POSIX::SigSet->new();
809
810 if ($block) {
811 sigprocmask (SIG_BLOCK, $sig_set, $old_sig_set);
812 } else {
813 sigprocmask (SIG_UNBLOCK, $sig_set, $old_sig_set);
814 }
815}
816
817# subroutine to start up a specified number of children.
818# We need to block signals until handlers are set up correctly.
819# Else its possible that HUP occurs after fork, which triggers
820# singal TERM at childs and calls server_close() instead of
821# simply exit the child.
822# Note: on server startup signals are setup to trigger
823# asynchronously for a short period of time (in PreForkSimple]::loop,
824# run_n_children is called before run_parent)
825# Net::Server::PreFork does not have this problem, because it is using
826# signal HUP stop children
827sub run_n_children {
828 my $self = shift;
829 my $prop = $self->{server};
830 my $n = shift;
831
832 setup_fork_signal_mask (1); # block signals
833
834 $self->SUPER::run_n_children ($n);
835
836 setup_fork_signal_mask (0); # unblocking signals for parent
837}
838
839# test sig_hup with: for ((;;)) ;do kill -HUP `cat /var/run/proxpolicy.pid`; done;
840# wrapper to avoid multiple calls to sig_hup
841sub sig_hup {
842 my $self = shift;
843 my $prop = $self->{server};
844
845 return if defined ($prop->{_HUP}); # do not call twice
846
847 $self->SUPER::sig_hup();
848}
849
850### child process which will accept on the port
851sub run_child {
852 my $self = shift;
853 my $prop = $self->{server};
854
855 $self->log(4,"Child Preforked ($$)\n");
856
857 # set correct signal handlers before enabling signals again
858 $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub {
859 $self->child_finish_hook;
860 exit;
861 };
862
863 delete $prop->{children};
864
865 $self->child_init_hook;
866
867 # accept connections
868
869 my $sock = $prop->{sock}->[0];
870
871 # make sure we got a good sock
872 if (!defined ($sock)){
873 $self->log (0, "ERROR: Received a bad socket");
874 exit (-1);
875 }
876
877 # sometimes the socket is not usable, don't know why
878 my $flags = fcntl($sock, F_GETFL, 0);
879 if (!$flags) {
880 $self->log (0, msgquote ("socket not ready - $!"));
881 exit (-1);
882 }
883
884 # cache is limited, because postfix does max. 100 queries
885 $self->{cache} = {};
886
887 eval {
888 my $mux = $self->{mux};
889 $mux->listen ($sock);
890 $mux->loop;
891 };
892
893 $self->log (0, msgquote ("ERROR: $@")) if $@;
894
895 $self->child_finish_hook;
896
897 exit;
898}
899
900sub log {
901 my ($self, $level, $msg, @therest) = @_;
902 my $prop = $self->{server};
903
904 return if $level =~ /^\d+$/ && $level > $prop->{log_level};
905
906 $level = $Proxmox::SafeSyslog::syslog_map->{$level} || $level;
907 if (@therest) {
908 syslog($level, $msg, @therest);
909 } else {
910 syslog ($level, $msg);
911 }
912}
913
914my $server = bless {
915 server => $server_attr,
916};
917
918$server->sig_chld(); # avoid zombies after restart
919
920$server->run ();
921
922exit (0);
923
924__END__
925
926=head1 NAME
927
928proxpolicy - the Proxmox policy daemon
929
930=head1 SYNOPSIS
931
932proxpolicy
933
934=head1 DESCRIPTION
935
936Documentation is available at www.proxmox.com
937
938