]>
Commit | Line | Data |
---|---|---|
88ef7e9c MT |
1 | #!/usr/bin/python3 |
2 | ############################################################################### | |
3 | # # | |
4 | # libloc - A library to determine the location of someone on the Internet # | |
5 | # # | |
e17e804e | 6 | # Copyright (C) 2020-2021 IPFire Development Team <info@ipfire.org> # |
88ef7e9c MT |
7 | # # |
8 | # This library is free software; you can redistribute it and/or # | |
9 | # modify it under the terms of the GNU Lesser General Public # | |
10 | # License as published by the Free Software Foundation; either # | |
11 | # version 2.1 of the License, or (at your option) any later version. # | |
12 | # # | |
13 | # This library is distributed in the hope that it will be useful, # | |
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of # | |
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # | |
16 | # Lesser General Public License for more details. # | |
17 | # # | |
18 | ############################################################################### | |
19 | ||
ce1e53c7 | 20 | import functools |
88ef7e9c MT |
21 | import io |
22 | import ipaddress | |
23 | import logging | |
47de14b0 | 24 | import math |
88ef7e9c MT |
25 | import os |
26 | import socket | |
58f0922d | 27 | import sys |
88ef7e9c | 28 | |
58f0922d | 29 | from .i18n import _ |
fae36e81 MT |
30 | import _location |
31 | ||
88ef7e9c MT |
32 | # Initialise logging |
33 | log = logging.getLogger("location.export") | |
34 | log.propagate = 1 | |
35 | ||
bd1dc6bf | 36 | FLAGS = { |
fae36e81 MT |
37 | _location.NETWORK_FLAG_ANONYMOUS_PROXY : "A1", |
38 | _location.NETWORK_FLAG_SATELLITE_PROVIDER : "A2", | |
39 | _location.NETWORK_FLAG_ANYCAST : "A3", | |
e17e804e | 40 | _location.NETWORK_FLAG_DROP : "XD", |
fae36e81 MT |
41 | } |
42 | ||
88ef7e9c MT |
43 | class OutputWriter(object): |
44 | suffix = "networks" | |
45 | mode = "w" | |
46 | ||
f1fb21bd | 47 | def __init__(self, name, family=None, directory=None, f=None): |
ce1e53c7 | 48 | self.name = name |
27dc4fa5 | 49 | self.family = family |
ce1e53c7 MT |
50 | self.directory = directory |
51 | ||
52 | # Open output file | |
f1fb21bd MT |
53 | if f: |
54 | self.f = f | |
55 | elif self.directory: | |
ce1e53c7 MT |
56 | self.f = open(self.filename, self.mode) |
57 | elif "b" in self.mode: | |
58 | self.f = io.BytesIO() | |
59 | else: | |
60 | self.f = io.StringIO() | |
88ef7e9c | 61 | |
47de14b0 MT |
62 | # Call any custom initialization |
63 | self.init() | |
64 | ||
88ef7e9c MT |
65 | # Immediately write the header |
66 | self._write_header() | |
67 | ||
47de14b0 MT |
68 | def init(self): |
69 | """ | |
70 | To be overwritten by anything that inherits from this | |
71 | """ | |
72 | pass | |
73 | ||
ce1e53c7 MT |
74 | def __repr__(self): |
75 | return "<%s %s f=%s>" % (self.__class__.__name__, self, self.f) | |
88ef7e9c | 76 | |
ce1e53c7 MT |
77 | @functools.cached_property |
78 | def tag(self): | |
79 | families = { | |
80 | socket.AF_INET6 : "6", | |
81 | socket.AF_INET : "4", | |
82 | } | |
88ef7e9c | 83 | |
ce1e53c7 MT |
84 | return "%sv%s" % (self.name, families.get(self.family, "?")) |
85 | ||
86 | @functools.cached_property | |
87 | def filename(self): | |
88 | if self.directory: | |
89 | return os.path.join(self.directory, "%s.%s" % (self.tag, self.suffix)) | |
88ef7e9c MT |
90 | |
91 | def _write_header(self): | |
92 | """ | |
93 | The header of the file | |
94 | """ | |
95 | pass | |
96 | ||
97 | def _write_footer(self): | |
98 | """ | |
99 | The footer of the file | |
100 | """ | |
101 | pass | |
102 | ||
c242f732 | 103 | def write(self, network): |
90d2194a | 104 | self.f.write("%s\n" % network) |
43554dc4 | 105 | |
88ef7e9c MT |
106 | def finish(self): |
107 | """ | |
108 | Called when all data has been written | |
109 | """ | |
110 | self._write_footer() | |
111 | ||
58f0922d MT |
112 | # Flush all output |
113 | self.f.flush() | |
114 | ||
115 | def print(self): | |
116 | """ | |
117 | Prints the entire output line by line | |
118 | """ | |
119 | if isinstance(self.f, io.BytesIO): | |
120 | raise TypeError(_("Won't write binary output to stdout")) | |
121 | ||
122 | # Go back to the beginning | |
123 | self.f.seek(0) | |
124 | ||
125 | # Iterate over everything line by line | |
126 | for line in self.f: | |
127 | sys.stdout.write(line) | |
88ef7e9c MT |
128 | |
129 | ||
130 | class IpsetOutputWriter(OutputWriter): | |
131 | """ | |
132 | For ipset | |
133 | """ | |
134 | suffix = "ipset" | |
135 | ||
47de14b0 MT |
136 | # The value is being used if we don't know any better |
137 | DEFAULT_HASHSIZE = 64 | |
138 | ||
139 | # We aim for this many networks in a bucket on average. This allows us to choose | |
140 | # how much memory we want to sacrifice to gain better performance. The lower the | |
141 | # factor, the faster a lookup will be, but it will use more memory. | |
142 | # We will aim for only using three quarters of all buckets to avoid any searches | |
143 | # through the linked lists. | |
144 | HASHSIZE_FACTOR = 0.75 | |
145 | ||
146 | def init(self): | |
147 | # Count all networks | |
148 | self.networks = 0 | |
149 | ||
150 | @property | |
151 | def hashsize(self): | |
152 | """ | |
153 | Calculates an optimized hashsize | |
154 | """ | |
155 | # Return the default value if we don't know the size of the set | |
156 | if not self.networks: | |
157 | return self.DEFAULT_HASHSIZE | |
158 | ||
159 | # Find the nearest power of two that is larger than the number of networks | |
160 | # divided by the hashsize factor. | |
161 | exponent = math.log(self.networks / self.HASHSIZE_FACTOR, 2) | |
162 | ||
f1f8927a MT |
163 | # Return the size of the hash (the minimum is 64) |
164 | return max(2 ** math.ceil(exponent), 64) | |
47de14b0 | 165 | |
88ef7e9c | 166 | def _write_header(self): |
47de14b0 | 167 | # This must have a fixed size, because we will write the header again in the end |
27dc4fa5 | 168 | self.f.write("create %s hash:net family inet%s" % ( |
ce1e53c7 | 169 | self.tag, |
27dc4fa5 MT |
170 | "6" if self.family == socket.AF_INET6 else "" |
171 | )) | |
52176cc7 | 172 | self.f.write(" hashsize %8d maxelem 1048576 -exist\n" % self.hashsize) |
ce1e53c7 | 173 | self.f.write("flush %s\n" % self.tag) |
88ef7e9c | 174 | |
90d2194a | 175 | def write(self, network): |
ce1e53c7 | 176 | self.f.write("add %s %s\n" % (self.tag, network)) |
88ef7e9c | 177 | |
47de14b0 MT |
178 | # Increment network counter |
179 | self.networks += 1 | |
180 | ||
181 | def _write_footer(self): | |
182 | # Jump back to the beginning of the file | |
183 | self.f.seek(0) | |
184 | ||
185 | # Rewrite the header with better configuration | |
186 | self._write_header() | |
187 | ||
88ef7e9c MT |
188 | |
189 | class NftablesOutputWriter(OutputWriter): | |
190 | """ | |
191 | For nftables | |
192 | """ | |
193 | suffix = "set" | |
194 | ||
195 | def _write_header(self): | |
ce1e53c7 | 196 | self.f.write("define %s = {\n" % self.tag) |
88ef7e9c MT |
197 | |
198 | def _write_footer(self): | |
199 | self.f.write("}\n") | |
200 | ||
90d2194a | 201 | def write(self, network): |
88ef7e9c MT |
202 | self.f.write(" %s,\n" % network) |
203 | ||
204 | ||
205 | class XTGeoIPOutputWriter(OutputWriter): | |
206 | """ | |
207 | Formats the output in that way, that it can be loaded by | |
208 | the xt_geoip kernel module from xtables-addons. | |
209 | """ | |
88ef7e9c MT |
210 | mode = "wb" |
211 | ||
ce1e53c7 MT |
212 | @property |
213 | def tag(self): | |
214 | return self.name | |
215 | ||
216 | @property | |
217 | def suffix(self): | |
218 | return "iv%s" % ("6" if self.family == socket.AF_INET6 else "4") | |
219 | ||
90d2194a | 220 | def write(self, network): |
90188dad MT |
221 | self.f.write(network._first_address) |
222 | self.f.write(network._last_address) | |
88ef7e9c MT |
223 | |
224 | ||
225 | formats = { | |
226 | "ipset" : IpsetOutputWriter, | |
227 | "list" : OutputWriter, | |
228 | "nftables" : NftablesOutputWriter, | |
229 | "xt_geoip" : XTGeoIPOutputWriter, | |
230 | } | |
231 | ||
232 | class Exporter(object): | |
233 | def __init__(self, db, writer): | |
234 | self.db, self.writer = db, writer | |
235 | ||
236 | def export(self, directory, families, countries, asns): | |
237 | for family in families: | |
238 | log.debug("Exporting family %s" % family) | |
239 | ||
240 | writers = {} | |
241 | ||
242 | # Create writers for countries | |
243 | for country_code in countries: | |
ce1e53c7 | 244 | writers[country_code] = self.writer(country_code, family=family, directory=directory) |
88ef7e9c MT |
245 | |
246 | # Create writers for ASNs | |
247 | for asn in asns: | |
ce1e53c7 | 248 | writers[asn] = self.writer("AS%s" % asn, family=family, directory=directory) |
88ef7e9c | 249 | |
7af51f8a MT |
250 | # Filter countries from special country codes |
251 | country_codes = [ | |
bd1dc6bf | 252 | country_code for country_code in countries if not country_code in FLAGS.values() |
7af51f8a MT |
253 | ] |
254 | ||
88ef7e9c | 255 | # Get all networks that match the family |
7af51f8a | 256 | networks = self.db.search_networks(family=family, |
a3140aa9 | 257 | country_codes=country_codes, asns=asns, flatten=True) |
28c73fa3 | 258 | |
88ef7e9c | 259 | # Walk through all networks |
bbed1fd2 | 260 | for network in networks: |
88ef7e9c | 261 | # Write matching countries |
c242f732 MT |
262 | try: |
263 | writers[network.country_code].write(network) | |
264 | except KeyError: | |
265 | pass | |
88ef7e9c MT |
266 | |
267 | # Write matching ASNs | |
c242f732 MT |
268 | try: |
269 | writers[network.asn].write(network) | |
270 | except KeyError: | |
271 | pass | |
88ef7e9c | 272 | |
fae36e81 | 273 | # Handle flags |
bd1dc6bf | 274 | for flag in FLAGS: |
fae36e81 MT |
275 | if network.has_flag(flag): |
276 | # Fetch the "fake" country code | |
bd1dc6bf | 277 | country = FLAGS[flag] |
fae36e81 | 278 | |
c242f732 MT |
279 | try: |
280 | writers[country].write(network) | |
281 | except KeyError: | |
282 | pass | |
fae36e81 | 283 | |
88ef7e9c MT |
284 | # Write everything to the filesystem |
285 | for writer in writers.values(): | |
286 | writer.finish() | |
287 | ||
58f0922d MT |
288 | # Print to stdout |
289 | if not directory: | |
290 | for writer in writers.values(): | |
291 | writer.print() |