]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/geoip.py
configure: Fail if tools are missing
[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
9fdf4fb7 45 async def query(self, name, type=pycares.QUERY_TYPE_A):
d8f64b59
MT
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
9fdf4fb7 53 result, error = await fut
d8f64b59
MT
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
9068dba1 74class GeoIP(Object):
d8f64b59
MT
75 @lazy_property
76 def resolver(self):
c75e27d3 77 return Resolver(tries=2, timeout=2, domains=[])
d8f64b59
MT
78
79 def lookup(self, address):
80 return Address(self.backend, address)
81
9068dba1
MT
82 def guess_address_family(self, addr):
83 if ":" in addr:
84 return 6
940227cb 85
9068dba1 86 return 4
65afea2f 87
9068dba1
MT
88 def get_country(self, addr):
89 ret = self.get_all(addr)
940227cb 90
9068dba1
MT
91 if ret:
92 return ret.country
119f55d7 93
9068dba1 94 def get_location(self, addr):
5488a9f4
MT
95 query = "SELECT * FROM geoip \
96 WHERE %s BETWEEN start_ip AND end_ip LIMIT 1"
0673d1b0 97
c89084ce 98 return self.db.get(query, addr)
0673d1b0 99
9068dba1 100 def get_asn(self, addr):
5488a9f4
MT
101 query = "SELECT asn FROM geoip_asn \
102 WHERE %s BETWEEN start_ip AND end_ip LIMIT 1"
0673d1b0 103
9068dba1 104 ret = self.db.get(query, addr)
0673d1b0 105
9068dba1
MT
106 if ret:
107 return ret.asn
0673d1b0 108
9068dba1
MT
109 def get_all(self, addr):
110 location = self.get_location(addr)
0673d1b0 111
9068dba1
MT
112 if location:
113 location["asn"] = self.get_asn(addr)
940227cb 114
9068dba1 115 return location
119f55d7 116
9068dba1
MT
117 _countries = {
118 "A1" : "Anonymous Proxy",
119 "A2" : "Satellite Provider",
120 "AP" : "Asia/Pacific Region",
121 "EU" : "Europe",
122 }
119f55d7 123
9068dba1 124 def get_country_name(self, code):
df731215 125 return countries.get_name(code)
d8f64b59 126
9fdf4fb7 127 async def test_blacklist(self, address):
8f94e19f
MT
128 address = self.lookup(address)
129
130 # Determne blacklist status
9fdf4fb7 131 status = await address.is_blacklisted()
8f94e19f
MT
132
133 print("Blacklist status for %s: %s" % (address, status))
134
d8f64b59
MT
135
136class 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
d8f64b59
MT
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):
f1364f23 153 if self.family == socket.AF_INET6:
66ac234b 154 octets = list(self.address.exploded.replace(":", ""))
f1364f23
MT
155 elif self.family == socket.AF_INET:
156 octets = str(self.address).split(".")
157 else:
158 raise NotImplementedError("Unknown IP protocol")
d8f64b59 159
f1364f23
MT
160 # Reverse the list
161 octets.reverse()
d8f64b59 162
f1364f23
MT
163 # Append suffix
164 octets.append(blacklist)
d8f64b59 165
f1364f23 166 return ".".join(octets)
d8f64b59 167
9fdf4fb7 168 async def _resolve_blacklist(self, blacklist):
8f94e19f
MT
169 return_code = None
170
d8f64b59
MT
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:
9fdf4fb7 184 res = await self.backend.geoip.resolver.query(rr, type=type)
d8f64b59
MT
185 except IOError as e:
186 logging.warning(e)
187
8f94e19f 188 return return_code, "%s" % e
d8f64b59
MT
189
190 # Not found
191 if not res:
cfe7d74c 192 logging.debug("%s is not blacklisted on %s" % (self, blacklist))
8f94e19f
MT
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
d8f64b59
MT
199
200 # If the IP address is on a blacklist, we will try to fetch the TXT record
9fdf4fb7 201 reason = await self.backend.geoip.resolver.query(rr, type=pycares.QUERY_TYPE_TXT)
d8f64b59 202
cfe7d74c
MT
203 # Log result
204 logging.debug("%s is blacklisted on %s: %s" % (self, blacklist, reason or "N/A"))
205
d8f64b59
MT
206 # Take the first reason
207 if reason:
208 for i in reason:
8f94e19f 209 return return_code, i.text
d8f64b59
MT
210
211 # Blocked, but no reason
8f94e19f 212 return return_code, None
d8f64b59 213
9fdf4fb7
MT
214 async def get_blacklists(self):
215 blacklists = { bl : self._resolve_blacklist(bl) for bl in BLACKLISTS }
d8f64b59
MT
216
217 return blacklists
2517822e 218
9fdf4fb7 219 async def is_blacklisted(self):
cfe7d74c
MT
220 logging.debug("Checking if %s is blacklisted..." % self)
221
222 # Perform checks
9fdf4fb7 223 blacklists = { bl : self._resolve_blacklist(bl) for bl in BLOCKLISTS }
2517822e
MT
224
225 # If we are blacklisted on one list, this one is screwed
8f94e19f 226 for bl in blacklists:
9fdf4fb7 227 code, message = await blacklists[bl]
8f94e19f
MT
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
2517822e
MT
236 if code:
237 return True