]> git.ipfire.org Git - people/ms/libloc.git/blame - src/python/export.py
export: Enable flattening for everything
[people/ms/libloc.git] / src / python / export.py
CommitLineData
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 20import functools
88ef7e9c
MT
21import io
22import ipaddress
23import logging
47de14b0 24import math
88ef7e9c
MT
25import os
26import socket
58f0922d 27import sys
88ef7e9c 28
58f0922d 29from .i18n import _
fae36e81
MT
30import _location
31
88ef7e9c
MT
32# Initialise logging
33log = logging.getLogger("location.export")
34log.propagate = 1
35
bd1dc6bf 36FLAGS = {
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
43class 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
130class 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
189class 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
205class 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
225formats = {
226 "ipset" : IpsetOutputWriter,
227 "list" : OutputWriter,
228 "nftables" : NftablesOutputWriter,
229 "xt_geoip" : XTGeoIPOutputWriter,
230}
231
232class 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()