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