]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/geoip.py
Shorten list of blacklists
[ipfire.org.git] / src / backend / geoip.py
CommitLineData
940227cb
MT
1#!/usr/bin/python
2
d8f64b59
MT
3import ipaddress
4import logging
5import pycares
638e9782 6import re
d8f64b59
MT
7import socket
8import tornado.gen
9import tornado.platform.caresresolver
638e9782 10
11347e46 11from . import countries
940227cb 12
d8f64b59 13from .decorators import *
11347e46 14from .misc import Object
940227cb 15
8f94e19f
MT
16# These lists are used to block access to the webapp
17BLOCKLISTS = (
18 "sbl.spamhaus.org",
19 "xbl.spamhaus.org",
20)
21
1058acb8 22BLACKLISTS = (
1058acb8 23 "b.barracudacentral.org",
1058acb8 24 "bl.spamcop.net",
3204bb7f 25 "bl.blocklist.de",
1058acb8 26 "cbl.abuseat.org",
1058acb8
MT
27 "dnsbl-1.uceprotect.net",
28 "dnsbl-2.uceprotect.net",
29 "dnsbl-3.uceprotect.net",
30 "dnsbl.abuse.ch",
1058acb8 31 "ix.dnsbl.manitu.net",
1058acb8 32 "pbl.spamhaus.org",
1058acb8 33 "sbl.spamhaus.org",
1058acb8
MT
34 "xbl.spamhaus.org",
35 "zen.spamhaus.org",
1058acb8 36)
d8f64b59
MT
37
38class Resolver(tornado.platform.caresresolver.CaresResolver):
39 def initialize(self, **kwargs):
40 super().initialize()
41
42 # Overwrite Channel
43 self.channel = pycares.Channel(sock_state_cb=self._sock_state_cb, **kwargs)
44
45 @tornado.gen.coroutine
46 def query(self, name, type=pycares.QUERY_TYPE_A):
47 # Create a new Future
48 fut = tornado.gen.Future()
49
50 # Perform the query
51 self.channel.query(name, type, lambda result, error: fut.set_result((result, error)))
52
53 # Wait for the response
54 result, error = yield fut
55
56 # Handle any errors
57 if error:
58 # NXDOMAIN
59 if error == pycares.errno.ARES_ENOTFOUND:
60 return
61
62 # Ignore responses with no data
63 elif error == pycares.errno.ARES_ENODATA:
64 return
65
66 raise IOError(
67 "C-Ares returned error %s: %s while resolving %s"
68 % (error, pycares.errno.strerror(error), name)
69 )
70
71 # Return the result
72 return result
73
74
9068dba1 75class GeoIP(Object):
d8f64b59
MT
76 @lazy_property
77 def resolver(self):
c75e27d3 78 return Resolver(tries=2, timeout=2, domains=[])
d8f64b59
MT
79
80 def lookup(self, address):
81 return Address(self.backend, address)
82
9068dba1
MT
83 def guess_address_family(self, addr):
84 if ":" in addr:
85 return 6
940227cb 86
9068dba1 87 return 4
65afea2f 88
9068dba1
MT
89 def get_country(self, addr):
90 ret = self.get_all(addr)
940227cb 91
9068dba1
MT
92 if ret:
93 return ret.country
119f55d7 94
9068dba1 95 def get_location(self, addr):
5488a9f4
MT
96 query = "SELECT * FROM geoip \
97 WHERE %s BETWEEN start_ip AND end_ip LIMIT 1"
0673d1b0 98
c89084ce 99 return self.db.get(query, addr)
0673d1b0 100
9068dba1 101 def get_asn(self, addr):
5488a9f4
MT
102 query = "SELECT asn FROM geoip_asn \
103 WHERE %s BETWEEN start_ip AND end_ip LIMIT 1"
0673d1b0 104
9068dba1 105 ret = self.db.get(query, addr)
0673d1b0 106
9068dba1
MT
107 if ret:
108 return ret.asn
0673d1b0 109
9068dba1
MT
110 def get_all(self, addr):
111 location = self.get_location(addr)
0673d1b0 112
9068dba1
MT
113 if location:
114 location["asn"] = self.get_asn(addr)
940227cb 115
9068dba1 116 return location
119f55d7 117
9068dba1
MT
118 _countries = {
119 "A1" : "Anonymous Proxy",
120 "A2" : "Satellite Provider",
121 "AP" : "Asia/Pacific Region",
122 "EU" : "Europe",
123 }
119f55d7 124
9068dba1 125 def get_country_name(self, code):
df731215 126 return countries.get_name(code)
d8f64b59 127
8f94e19f
MT
128 @tornado.gen.coroutine
129 def test_blacklist(self, address):
130 address = self.lookup(address)
131
132 # Determne blacklist status
133 status = yield address.is_blacklisted()
134
135 print("Blacklist status for %s: %s" % (address, status))
136
d8f64b59
MT
137
138class Address(Object):
139 def init(self, address):
140 self.address = ipaddress.ip_address(address)
141
142 def __str__(self):
143 return "%s" % self.address
144
d8f64b59
MT
145 @property
146 def family(self):
147 if isinstance(self.address, ipaddress.IPv6Address):
148 return socket.AF_INET6
149 elif isinstance(self.address, ipaddress.IPv4Address):
150 return socket.AF_INET
151
152 # Blacklist
153
154 def _make_blacklist_rr(self, blacklist):
f1364f23 155 if self.family == socket.AF_INET6:
66ac234b 156 octets = list(self.address.exploded.replace(":", ""))
f1364f23
MT
157 elif self.family == socket.AF_INET:
158 octets = str(self.address).split(".")
159 else:
160 raise NotImplementedError("Unknown IP protocol")
d8f64b59 161
f1364f23
MT
162 # Reverse the list
163 octets.reverse()
d8f64b59 164
f1364f23
MT
165 # Append suffix
166 octets.append(blacklist)
d8f64b59 167
f1364f23 168 return ".".join(octets)
d8f64b59
MT
169
170 @tornado.gen.coroutine
171 def _resolve_blacklist(self, blacklist):
8f94e19f
MT
172 return_code = None
173
d8f64b59
MT
174 # Get resource record name
175 rr = self._make_blacklist_rr(blacklist)
176
177 # Get query type from IP protocol version
178 if self.family == socket.AF_INET6:
179 type = pycares.QUERY_TYPE_AAAA
180 elif self.family == socket.AF_INET:
181 type = pycares.QUERY_TYPE_A
182 else:
183 raise NotImplementedError("Unknown IP protocol")
184
185 # Run query
186 try:
187 res = yield self.backend.geoip.resolver.query(rr, type=type)
188 except IOError as e:
189 logging.warning(e)
190
8f94e19f 191 return return_code, "%s" % e
d8f64b59
MT
192
193 # Not found
194 if not res:
cfe7d74c 195 logging.debug("%s is not blacklisted on %s" % (self, blacklist))
8f94e19f
MT
196 return return_code, None
197
198 # Extract return code from DNS response
199 for row in res:
200 return_code = row.host
201 break
d8f64b59
MT
202
203 # If the IP address is on a blacklist, we will try to fetch the TXT record
204 reason = yield self.backend.geoip.resolver.query(rr, type=pycares.QUERY_TYPE_TXT)
205
cfe7d74c
MT
206 # Log result
207 logging.debug("%s is blacklisted on %s: %s" % (self, blacklist, reason or "N/A"))
208
d8f64b59
MT
209 # Take the first reason
210 if reason:
211 for i in reason:
8f94e19f 212 return return_code, i.text
d8f64b59
MT
213
214 # Blocked, but no reason
8f94e19f 215 return return_code, None
d8f64b59
MT
216
217 @tornado.gen.coroutine
1058acb8
MT
218 def get_blacklists(self):
219 blacklists = yield { bl : self._resolve_blacklist(bl) for bl in BLACKLISTS }
d8f64b59
MT
220
221 return blacklists
2517822e
MT
222
223 @tornado.gen.coroutine
224 def is_blacklisted(self):
cfe7d74c
MT
225 logging.debug("Checking if %s is blacklisted..." % self)
226
227 # Perform checks
8f94e19f 228 blacklists = yield { bl : self._resolve_blacklist(bl) for bl in BLOCKLISTS }
2517822e
MT
229
230 # If we are blacklisted on one list, this one is screwed
8f94e19f
MT
231 for bl in blacklists:
232 code, message = blacklists[bl]
233
234 logging.debug("Response from %s is: %s (%s)" % (bl, code, message))
235
236 # Exclude matches on SBLCSS
237 if bl == "sbl.spamhaus.org" and code == "127.0.0.3":
238 continue
239
240 # Consider the host blocked for any non-zero return code
2517822e
MT
241 if code:
242 return True