File: dns_whitelist_soft

package info (click to toggle)
qpsmtpd 0.84-9
  • links: PTS
  • area: main
  • in suites: wheezy
  • size: 1,376 kB
  • sloc: perl: 8,012; sh: 382; makefile: 61
file content (157 lines) | stat: -rw-r--r-- 4,512 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
=head1 NAME

dns_whitelist_soft - dns-based whitelist override for other qpsmtpd plugins

=head1 DESCRIPTION

The dns_whitelist_soft plugin allows selected host to be whitelisted as
exceptions to later plugin processing.  It is strongly based on the original
dnsbl plugin as well as Gavin Carr's original whitelist_soft plugin.  It is
most suitable for multisite installations, so that the whitelist is stored
in one location and available from all.

=head1 CONFIGURATION

To enable the plugin, add it to the ~qpsmtpd/config/plugins file as usual.
It should precede any plugins whose rejections you wish to override.  You may
have to alter those plugins to check the appropriate notes field.

Several configuration files are supported, corresponding to different
parts of the SMTP conversation:

=over 4

=item whitelist_zones

Any IP address listed in the whitelist_zones file is queried using
the connecting MTA's IP address.  Any A or TXT answer means that the
remote HOST address can be selectively exempted at other stages by plugins
testing for a 'whitelisthost' connection note.

=back

NOTE: other 'connect' hooks will continue to fire (e.g. dnsbl), since the DNS
queries happen in the background.  This plugin's 'rcpt_handler' retrieves
the results of the query and sets the connection note if found.

If you switch to qpsmtpd-async and to the async version of this plugin, then
the 'whitelisthost' connection note will be available to the other 'connect'
hooks, see the documentation of the async plugin.

=head1 AUTHOR

John Peacock <jpeacock@rowman.com>

Based on the 'whitelist_soft' plugin by Gavin Carr <gavin@openfusion.com.au>,
based on the 'whitelist' plugin by Devin Carraway <qpsmtpd@devin.com>.

=cut

sub hook_connect {
  my ($self, $transaction) = @_;

  my $remote_ip = $self->qp->connection->remote_ip;

  my %whitelist_zones = map { (split /\s+/, $_, 2)[0,1] }
				$self->qp->config('whitelist_zones');

  return DECLINED unless %whitelist_zones;

  my $reversed_ip = join(".", reverse(split(/\./, $remote_ip)));

  # we queue these lookups in the background and just fetch the
  # results in the first rcpt handler

  my $res = new Net::DNS::Resolver;
  my $sel = IO::Select->new();

  for my $dnsbl (keys %whitelist_zones) {
    $self->log(LOGDEBUG, "Checking $reversed_ip.$dnsbl in the background");
    $sel->add($res->bgsend("$reversed_ip.$dnsbl", "TXT"));
  }

  $self->qp->connection->notes('whitelist_sockets', $sel);

  return DECLINED;
}

sub process_sockets {
  my ($self) = @_;

  my $conn = $self->qp->connection;

  return $conn->notes('whitelisthost') 
    if $conn->notes('whitelisthost');

  my $res = new Net::DNS::Resolver;
  my $sel = $conn->notes('whitelist_sockets') or return "";

  my $result; 

  $self->log(LOGDEBUG, "waiting for whitelist dns");

  # don't wait more than 4 seconds here
  my @ready = $sel->can_read(4);

  $self->log(LOGDEBUG, "DONE waiting for whitelist dns, got ",
  	scalar @ready, " answers ...") ;
  return '' unless @ready;

  for my $socket (@ready) {
    my $query = $res->bgread($socket);
    $sel->remove($socket);
    undef $socket;

    my $whitelist;

    if ($query) {
      my $a_record = 0;
      foreach my $rr ($query->answer) {
	$a_record = 1 if $rr->type eq "A";
	my $name = $rr->name;
	($whitelist) = ($name =~ m/(?:\d+\.){4}(.*)/) unless $whitelist;
	$whitelist = $name unless $whitelist;
	$self->log(LOGDEBUG, "name ", $rr->name);
	next unless $rr->type eq "TXT";
	$self->log(LOGDEBUG, "got txt record");
	$result = $rr->txtdata and last;
      }
      $a_record and $result = "Blocked by $whitelist";
    }
    else {
      $self->log(LOGERROR, "$whitelist query failed: ", $res->errorstring)
	unless $res->errorstring eq "NXDOMAIN";
    }

    if ($result) {
      #kill any other pending I/O
      $conn->notes('whitelist_sockets', undef);
      return $conn->notes('whitelisthost', $result);
    }
  }

  if ($sel->count) {
    # loop around if we have dns blacklists left to see results from
    return $self->process_sockets();
  }

  # er, the following code doesn't make much sense anymore...

  # if there was more to read; then forget it
  $conn->notes('whitelist_sockets', undef);

  return $conn->notes('whitelisthost', $result);

}

sub hook_rcpt {
  my ($self, $transaction, $rcpt, %param) = @_;
  my $ip = $self->qp->connection->remote_ip || return (DECLINED);
  my $note = $self->process_sockets;
  if ( $note ) {
    $self->log(LOGNOTICE,"Host $ip is whitelisted: $note");
  }
  return DECLINED;
}

1;