-
Notifications
You must be signed in to change notification settings - Fork 0
/
pcps.pl
executable file
·204 lines (165 loc) · 4.65 KB
/
pcps.pl
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#!/usr/bin/perl
#
# pcps.pl - Postfix Country Policy Service
#
# See the README.md for more info.
#
# Chip Rosenthal
#
use strict;
use warnings;
use Getopt::Std;
use File::Basename;
use Sys::Syslog qw(:standard :macros);
use Data::Validate::IP qw(is_ipv4 is_ipv6);
use Geo::IPfree;
use DB_File;
use constant USAGE => "usage: $0 [-dhtq] [-M ACCESS_MAP] (try \"-h\" for help)\n";
# Path to the access map that is keyed by country. Override with -M.
use constant DEFAULT_ACCESS_MAP => "/etc/postfix/access-country.db";
# Result to return when no policy applies to this address.
# Change this by addding a "default" entry in the access map.
use constant DUNNO => "dunno";
sub show_help {
print <<\_EOT_;
NAME
pcps.pl - Postfix Country Policy Service
SYNOPSIS
perl pcps.pl [OPTION ...]
OPTIONS
-d Enable debug logging. Increases syslog logging level from INFO to DEBUG.
-h Display this help.
-q Log only warnings and errors. Decreases syslog logging level from INFO to NOTICE.
-t Also log to stderr. Normally does this if stderr is attached to a tty.
-M ACCESS_MAP
Set path to country access map. Default is /etc/postfix/access-country.db.
See the package README for further details.
AUTHOR
Chip Rosenthal
This package is published at: https://github.com/chip-rosenthal/postfix-country-policy-service
UNLICENSE
This is free and unencumbered software released into the public domain.
See https://github.com/chip-rosenthal/postfix-country-policy-service/LICENSE
_EOT_
exit(0);
}
#
# Parse command line options.
#
my %opts;
if (! getopts('dhqtM:', \%opts)) {
die USAGE;
}
if ($opts{'h'}) {
show_help();
}
if (@ARGV != 0 ) {
die USAGE;
}
#
# Open log.
#
my $logopts = "pid";
if ($opts{'t'} || -t STDERR) {
$logopts .= ",perror";
}
openlog(basename($0), $logopts, LOG_MAIL);
if ($opts{'d'}) {
setlogmask(LOG_UPTO(LOG_DEBUG)); # -d enables debug output
} elsif (! $opts{'q'}) {
setlogmask(LOG_UPTO(LOG_INFO)); # normal logging
} else {
setlogmask(LOG_UPTO(LOG_NOTICE)); # -q suppresses info output
}
##############################################################################
#
# Functions
#
sub fatal {
die "usage: fatal MESSAGE" unless (@_ == 1);
my $mssg = shift;
syslog(LOG_ERR, "fatal: $mssg");
exit(1);
}
# Read in a Postfix access policy request.
# It's a block of name=value lines, terminated with blank line.
#
sub load_request {
die "usage: load_request *FILEHANDLE" unless (@_ == 1);
local *FH = shift;
my %attrs;
while (<FH>) {
chomp();
if (! $_) {
last;
}
syslog(LOG_DEBUG, "debug: READ: $_");
my @a = split(/=/, $_, 2);
$attrs{$a[0]} = $a[1];
}
return %attrs;
}
our $GEO = Geo::IPfree->new;
# Get the two-letter country code for an IP address.
#
sub lookup_country {
die "usage: lookup_country IP" unless (@_ == 1);
my($ip) = @_;
my($code1, $name1) = $GEO->LookUp($ip);
return $code1;
}
our %DB_ACCESS;
our $ACCESS_MAP = $opts{'M'} || DEFAULT_ACCESS_MAP;
syslog(LOG_DEBUG, "debug: opening access map: $ACCESS_MAP");
tie %DB_ACCESS, 'DB_File', $ACCESS_MAP, O_RDONLY;
# Initially, the default access policy is set to DUNNO.
# This can be overridden by specifying a "default" entry in the access map.
our $DEFAULT_ACCESS = DUNNO;
# Lookup a country in the access map. The access map keys are two-letter
# country codes (in lower case) with an appended NUL byte.
#
sub lookup_access {
die "usage: lookup_access COUNTRY" unless (@_ == 1);
my($country) = @_;
my $key = lc($country) . "\0";
return $DB_ACCESS{$key} || $DEFAULT_ACCESS;
}
# Update the default value if one is defined in the access map.
$DEFAULT_ACCESS = lookup_access("default");
# Process a request and determine policy result.
# Returns triple ($client, $country, $result).
#
sub process_request {
my %attrs = @_;
my $client = $attrs{'client_address'} || "";
if (! $client) {
fatal("client_address not set in input request");
}
if (is_ipv6($client)) {
syslog(LOG_WARNING, "warning: cannot process IPv6: client=$client");
return ($client, "", $DEFAULT_ACCESS);
}
if (! is_ipv4($client)) {
fatal("invalid IPv4 address: client=$client");
}
my $country = lookup_country($client) || "";
if (! $country) {
syslog(LOG_WARNING, "warning: country lookup failed: client=$client");
return ($client, "", $DEFAULT_ACCESS);
}
my $result = lookup_access($country);
return ($client, $country, $result);
}
##############################################################################
#
# Execution
#
my %attrs = load_request(*STDIN);
my($client, $country, $result) = process_request(%attrs);
syslog(LOG_INFO, "info: client=$client country=$country result=$result");
print "action=$result\n\n";
untie %DB_ACCESS;
closelog();
exit(0);