-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathpgl4rbl.py
executable file
·252 lines (182 loc) · 6.13 KB
/
pgl4rbl.py
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010-2014 Develer S.r.L
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
import argparse
import netaddr
import os
import os.path
import re
import signal
import socket
import stat
import sys
import syslog
import time
RE_IP = re.compile(r"\[(\d+)\.(\d+)\.(\d+)\.(\d+)\]")
def main():
# Allow SIGPIPE to kill our program
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
args = parse_args()
load_config_file(args.config)
# Configure syslog support
syslog.openlog("pgl4rbl", syslog.LOG_PID, getattr(syslog, SYSLOG_FACILITY))
sanity_check()
if args.clean:
os.system("find '%s' -type f -mmin +%d -delete" %
(GREYLIST_DB, MAX_GREYLIST_TIME))
else:
process_one()
def parse_args():
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("-c", "--config", type=str, default="/etc/pgl4rbl.conf", help="path to the configuration file")
arg_parser.add_argument("-d", "--clean", action="store_true", help="clean the greylist db")
return arg_parser.parse_args()
def load_config_file(config):
try:
execfile(config, globals())
except Exception, e:
# We can't use die() here
syslog.openlog("pgl4rbl", syslog.LOG_PID)
error("Error parsing configuration: %s" % e)
sys.exit(2)
def sanity_check():
# Check that we can access the DB directory
if not os.path.isdir(GREYLIST_DB):
die("DB directory does not exist: " + GREYLIST_DB)
# Check that permissions allow access to the DB directory
try:
test_fn = ".test.%s" % os.getpid()
add_db(test_fn)
check_db(test_fn)
clean_db(test_fn)
except (OSError, IOError):
die("Wrong permissions for DB directory: " + GREYLIST_DB)
def log(s):
syslog.syslog(syslog.LOG_INFO, s)
def die(s):
error(s)
sys.exit(2)
def error(s):
syslog.syslog(syslog.LOG_ERR, s)
def process_one():
d = {}
while 1:
L = sys.stdin.readline()
L = L.strip()
if not L:
break
try:
k, v = L.split('=', 1)
except ValueError:
die("invalid input line: %r" % L)
d[k.strip()] = v.strip()
try:
ip = d['client_address']
helo = d['helo_name']
except KeyError:
die("client_address/helo_name field not found in input data, aborting")
if not ip:
die("client_address empty in input data, aborting")
log("Processing client: S:%s H:%s" % (ip, helo))
action = process_ip(ip, helo)
log("Action for IP %s: %s" % (ip, action))
sys.stdout.write('action=%s\n\n' % action)
def process_ip(ip, helo):
if check_whitelist(ip):
log("%s is whitelisted" % ip)
return "ok You are cleared to land"
if not check_rbls(ip) and not check_badhelo(helo):
return "ok You are cleared to land"
t = check_db(ip)
if t < 0:
log("%s not in greylist DB, adding it" % ip)
add_db(ip)
return "defer Are you a spammer? If not, just retry!"
elif t < MIN_GREYLIST_TIME * 60:
log("%s too young in greylist DB" % ip)
return "defer Are you a spammer? If not, just retry!"
else:
log("%s already present greylist DB" % ip)
return "ok Greylisting OK"
def check_rbls(ip):
"""True if the IP is listed in RBLs"""
return any(query_rbl(ip, r) for r in RBLS)
def query_rbl(ip, rbl_root):
addr_parts = list(reversed(ip.split('.'))) + [rbl_root]
check_name = ".".join(addr_parts)
try:
ip = socket.gethostbyname(check_name)
except socket.error:
return None
else:
log("Found in blacklist %s (resolved to %s)" % (rbl_root, ip))
return ip
def check_whitelist(ip):
"""True if the IP is whitelisted"""
if len(GREYLIST_WHITELIST) > 0:
wl = open(GREYLIST_WHITELIST)
nip = netaddr.IPAddress(ip)
for subnet in wl:
if nip in netaddr.IPNetwork(subnet):
wl.close()
return True
wl.close()
return False
def check_badhelo(helo):
"""True if the HELO string violates the RFC"""
if not CHECK_BAD_HELO:
return False
if helo.startswith('['):
m = RE_IP.match(helo)
if m is not None:
octs = map(int, (m.group(1), m.group(2), m.group(3), m.group(4)))
if max(octs) < 256:
return False
log("HELO string begins with '[' but does not contain a valid IPv4 address")
return True
if '.' not in helo:
log("HELO string does not look like a FQDN")
return True
return False
def check_db(ip):
"""
Check if ip is in the GL database.
Returns -1 if not present, or the number of seconds
since it has been added.
"""
fn = os.path.join(GREYLIST_DB, ip)
try:
s = os.stat(fn)
except OSError:
return -1
return time.time() - s.st_mtime
def add_db(ip):
"""Add the specified IP to the GL database"""
open(os.path.join(GREYLIST_DB, ip), "w").close()
def clean_db(ip):
os.remove(os.path.join(GREYLIST_DB, ip))
if __name__ == "__main__":
main()