]> git.ipfire.org Git - location/libloc.git/blame - src/python/location.in
Implement an additional flag for hostile networks safe to drop
[location/libloc.git] / src / python / location.in
CommitLineData
5118a4b8
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) 2017-2021 IPFire Development Team <info@ipfire.org> #
5118a4b8
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
20import argparse
a6f1e346 21import datetime
4439e317 22import ipaddress
a6f1e346 23import logging
4439e317 24import os
88ef7e9c 25import re
a6f1e346 26import shutil
4439e317 27import socket
5118a4b8 28import sys
a68a46f5 29import time
5118a4b8
MT
30
31# Load our location module
32import location
a6f1e346 33import location.downloader
88ef7e9c
MT
34import location.export
35
7dccb767 36from location.i18n import _
5118a4b8 37
a6f1e346
MT
38# Setup logging
39log = logging.getLogger("location")
40
4439e317
MT
41# Output formatters
42
5118a4b8 43class CLI(object):
5118a4b8
MT
44 def parse_cli(self):
45 parser = argparse.ArgumentParser(
46 description=_("Location Database Command Line Interface"),
47 )
48 subparsers = parser.add_subparsers()
49
50 # Global configuration flags
51 parser.add_argument("--debug", action="store_true",
52 help=_("Enable debug output"))
bc1f5f53
MT
53 parser.add_argument("--quiet", action="store_true",
54 help=_("Enable quiet mode"))
5118a4b8 55
ddb184be
MT
56 # version
57 parser.add_argument("--version", action="version",
d2714e4a 58 version="%(prog)s @VERSION@")
ddb184be 59
2538ed9a
MT
60 # database
61 parser.add_argument("--database", "-d",
62 default="@databasedir@/database.db", help=_("Path to database"),
63 )
64
726f9984
MT
65 # public key
66 parser.add_argument("--public-key", "-k",
67 default="@databasedir@/signing-key.pem", help=_("Public Signing Key"),
68 )
69
9f64f1eb
MT
70 # Show the database version
71 version = subparsers.add_parser("version",
72 help=_("Show database version"))
73 version.set_defaults(func=self.handle_version)
74
5118a4b8
MT
75 # lookup an IP address
76 lookup = subparsers.add_parser("lookup",
77 help=_("Lookup one or multiple IP addresses"),
78 )
79 lookup.add_argument("address", nargs="+")
80 lookup.set_defaults(func=self.handle_lookup)
81
a68a46f5
MT
82 # Dump the whole database
83 dump = subparsers.add_parser("dump",
84 help=_("Dump the entire database"),
85 )
86 dump.add_argument("output", nargs="?", type=argparse.FileType("w"))
87 dump.set_defaults(func=self.handle_dump)
88
a6f1e346
MT
89 # Update
90 update = subparsers.add_parser("update", help=_("Update database"))
97769987
MT
91 update.add_argument("--cron",
92 help=_("Update the library only once per interval"),
93 choices=("daily", "weekly", "monthly"),
94 )
a6f1e346
MT
95 update.set_defaults(func=self.handle_update)
96
97 # Verify
98 verify = subparsers.add_parser("verify",
99 help=_("Verify the downloaded database"))
100 verify.set_defaults(func=self.handle_verify)
101
fadc1af0
MT
102 # Get AS
103 get_as = subparsers.add_parser("get-as",
104 help=_("Get information about one or multiple Autonomous Systems"),
105 )
106 get_as.add_argument("asn", nargs="+")
107 get_as.set_defaults(func=self.handle_get_as)
108
da3e360e
MT
109 # Search for AS
110 search_as = subparsers.add_parser("search-as",
111 help=_("Search for Autonomous Systems that match the string"),
112 )
113 search_as.add_argument("query", nargs=1)
114 search_as.set_defaults(func=self.handle_search_as)
115
43154ed7
MT
116 # List all networks in an AS
117 list_networks_by_as = subparsers.add_parser("list-networks-by-as",
118 help=_("Lists all networks in an AS"),
119 )
120 list_networks_by_as.add_argument("asn", nargs=1, type=int)
44e5ef71 121 list_networks_by_as.add_argument("--family", choices=("ipv6", "ipv4"))
88ef7e9c
MT
122 list_networks_by_as.add_argument("--format",
123 choices=location.export.formats.keys(), default="list")
43154ed7
MT
124 list_networks_by_as.set_defaults(func=self.handle_list_networks_by_as)
125
ccc7ab4e 126 # List all networks in a country
b5cdfad7 127 list_networks_by_cc = subparsers.add_parser("list-networks-by-cc",
ccc7ab4e
MT
128 help=_("Lists all networks in a country"),
129 )
b5cdfad7 130 list_networks_by_cc.add_argument("country_code", nargs=1)
44e5ef71 131 list_networks_by_cc.add_argument("--family", choices=("ipv6", "ipv4"))
88ef7e9c
MT
132 list_networks_by_cc.add_argument("--format",
133 choices=location.export.formats.keys(), default="list")
b5cdfad7 134 list_networks_by_cc.set_defaults(func=self.handle_list_networks_by_cc)
ccc7ab4e 135
bbdb2e0a
MT
136 # List all networks with flags
137 list_networks_by_flags = subparsers.add_parser("list-networks-by-flags",
138 help=_("Lists all networks with flags"),
139 )
140 list_networks_by_flags.add_argument("--anonymous-proxy",
141 action="store_true", help=_("Anonymous Proxies"),
142 )
143 list_networks_by_flags.add_argument("--satellite-provider",
144 action="store_true", help=_("Satellite Providers"),
145 )
146 list_networks_by_flags.add_argument("--anycast",
147 action="store_true", help=_("Anycasts"),
148 )
e17e804e
PM
149 list_networks_by_flags.add_argument("--drop",
150 action="store_true", help=_("Hostile Networks safe to drop"),
151 )
44e5ef71 152 list_networks_by_flags.add_argument("--family", choices=("ipv6", "ipv4"))
88ef7e9c
MT
153 list_networks_by_flags.add_argument("--format",
154 choices=location.export.formats.keys(), default="list")
bbdb2e0a
MT
155 list_networks_by_flags.set_defaults(func=self.handle_list_networks_by_flags)
156
fa9a3663
MT
157 # List countries
158 list_countries = subparsers.add_parser("list-countries",
159 help=_("Lists all countries"),
160 )
161 list_countries.add_argument("--show-name",
162 action="store_true", help=_("Show the name of the country"),
163 )
164 list_countries.add_argument("--show-continent",
165 action="store_true", help=_("Show the continent"),
166 )
167 list_countries.set_defaults(func=self.handle_list_countries)
168
88ef7e9c
MT
169 # Export
170 export = subparsers.add_parser("export",
171 help=_("Exports data in many formats to load it into packet filters"),
172 )
173 export.add_argument("--format", help=_("Output format"),
174 choices=location.export.formats.keys(), default="list")
175 export.add_argument("--directory", help=_("Output directory"), required=True)
176 export.add_argument("--family",
177 help=_("Specify address family"), choices=("ipv6", "ipv4"),
178 )
10fa313b 179 export.add_argument("objects", nargs="*", help=_("List country codes or ASNs to export"))
88ef7e9c
MT
180 export.set_defaults(func=self.handle_export)
181
78f37815
MT
182 args = parser.parse_args()
183
bc1f5f53 184 # Configure logging
f9de5e61
MT
185 if args.debug:
186 location.logger.set_level(logging.DEBUG)
bc1f5f53
MT
187 elif args.quiet:
188 location.logger.set_level(logging.WARNING)
f9de5e61 189
78f37815
MT
190 # Print usage if no action was given
191 if not "func" in args:
192 parser.print_usage()
193 sys.exit(2)
194
195 return args
5118a4b8
MT
196
197 def run(self):
198 # Parse command line arguments
199 args = self.parse_cli()
200
2538ed9a
MT
201 # Open database
202 try:
203 db = location.Database(args.database)
204 except FileNotFoundError as e:
2201d904
MT
205 # Allow continuing without a database
206 if args.func == self.handle_update:
207 db = None
208
209 else:
210 sys.stderr.write("location: Could not open database %s: %s\n" \
211 % (args.database, e))
212 sys.exit(1)
2538ed9a 213
6961aaf3
MT
214 # Translate family (if present)
215 if "family" in args:
216 if args.family == "ipv6":
217 args.family = socket.AF_INET6
218 elif args.family == "ipv4":
219 args.family = socket.AF_INET
220 else:
221 args.family = 0
44e5ef71 222
5118a4b8 223 # Call function
228d0e74
MT
224 try:
225 ret = args.func(db, args)
226
227 # Catch invalid inputs
228 except ValueError as e:
229 sys.stderr.write("%s\n" % e)
230 ret = 2
5118a4b8
MT
231
232 # Return with exit code
233 if ret:
234 sys.exit(ret)
235
236 # Otherwise just exit
237 sys.exit(0)
238
9f64f1eb
MT
239 def handle_version(self, db, ns):
240 """
241 Print the version of the database
242 """
243 t = time.strftime(
244 "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(db.created_at),
245 )
246
247 print(t)
248
2538ed9a 249 def handle_lookup(self, db, ns):
5118a4b8
MT
250 ret = 0
251
fbf925c8
MT
252 format = " %-24s: %s"
253
5118a4b8
MT
254 for address in ns.address:
255 try:
fbf925c8 256 network = db.lookup(address)
5118a4b8 257 except ValueError:
9f2f5d13 258 print(_("Invalid IP address: %s") % address, file=sys.stderr)
9cb56ac9 259 return 2
5118a4b8
MT
260
261 args = {
262 "address" : address,
fbf925c8 263 "network" : network,
5118a4b8
MT
264 }
265
266 # Nothing found?
fbf925c8 267 if not network:
9f2f5d13 268 print(_("Nothing found for %(address)s") % args, file=sys.stderr)
5118a4b8
MT
269 ret = 1
270 continue
271
fbf925c8
MT
272 print("%s:" % address)
273 print(format % (_("Network"), network))
5118a4b8 274
fbf925c8
MT
275 # Print country
276 if network.country_code:
072ca100
MT
277 country = db.get_country(network.country_code)
278
279 print(format % (
280 _("Country"),
281 country.name if country else network.country_code),
282 )
5118a4b8 283
fbf925c8
MT
284 # Print AS information
285 if network.asn:
286 autonomous_system = db.get_as(network.asn)
5118a4b8 287
fbf925c8
MT
288 print(format % (
289 _("Autonomous System"),
290 autonomous_system or "AS%s" % network.asn),
291 )
5118a4b8 292
ee83fe2e
MT
293 # Anonymous Proxy
294 if network.has_flag(location.NETWORK_FLAG_ANONYMOUS_PROXY):
295 print(format % (
296 _("Anonymous Proxy"), _("yes"),
297 ))
298
299 # Satellite Provider
300 if network.has_flag(location.NETWORK_FLAG_SATELLITE_PROVIDER):
301 print(format % (
302 _("Satellite Provider"), _("yes"),
303 ))
304
305 # Anycast
306 if network.has_flag(location.NETWORK_FLAG_ANYCAST):
307 print(format % (
308 _("Anycast"), _("yes"),
309 ))
310
e17e804e
PM
311 # Hostile Network
312 if network.has_flag(location.NETWORK_FLAG_DROP):
313 print(format % (
314 _("Hostile Network safe to drop"), _("yes"),
315 ))
316
5118a4b8
MT
317 return ret
318
a68a46f5
MT
319 def handle_dump(self, db, ns):
320 # Use output file or write to stdout
321 f = ns.output or sys.stdout
322
d6d06375
MT
323 # Format everything like this
324 format = "%-24s %s\n"
325
a68a46f5
MT
326 # Write metadata
327 f.write("#\n# Location Database Export\n#\n")
328
329 f.write("# Generated: %s\n" % time.strftime(
330 "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(db.created_at),
331 ))
332
333 if db.vendor:
334 f.write("# Vendor: %s\n" % db.vendor)
335
336 if db.license:
337 f.write("# License: %s\n" % db.license)
338
339 f.write("#\n")
340
341 if db.description:
342 for line in db.description.splitlines():
cfbae90f
PM
343 line = "# %s" % line
344 f.write("%s\n" % line.rstrip())
a68a46f5
MT
345
346 f.write("#\n")
347
348 # Iterate over all ASes
349 for a in db.ases:
350 f.write("\n")
d6d06375
MT
351 f.write(format % ("aut-num:", "AS%s" % a.number))
352 f.write(format % ("name:", a.name))
353
354 flags = {
355 location.NETWORK_FLAG_ANONYMOUS_PROXY : "is-anonymous-proxy:",
356 location.NETWORK_FLAG_SATELLITE_PROVIDER : "is-satellite-provider:",
357 location.NETWORK_FLAG_ANYCAST : "is-anycast:",
e17e804e 358 location.NETWORK_FLAG_DROP : "drop:",
d6d06375 359 }
a68a46f5
MT
360
361 # Iterate over all networks
362 for n in db.networks:
363 f.write("\n")
d6d06375 364 f.write(format % ("net:", n))
a68a46f5
MT
365
366 if n.country_code:
d6d06375 367 f.write(format % ("country:", n.country_code))
a68a46f5
MT
368
369 if n.asn:
03cd8096 370 f.write(format % ("aut-num:", n.asn))
d6d06375
MT
371
372 # Print all flags
373 for flag in flags:
374 if n.has_flag(flag):
375 f.write(format % (flags[flag], "yes"))
a68a46f5 376
2538ed9a 377 def handle_get_as(self, db, ns):
fadc1af0
MT
378 """
379 Gets information about Autonomous Systems
380 """
381 ret = 0
382
383 for asn in ns.asn:
384 try:
385 asn = int(asn)
386 except ValueError:
9f2f5d13 387 print(_("Invalid ASN: %s") % asn, file=sys.stderr)
fadc1af0
MT
388 ret = 1
389 continue
390
391 # Fetch AS from database
2538ed9a 392 a = db.get_as(asn)
fadc1af0
MT
393
394 # Nothing found
395 if not a:
9f2f5d13 396 print(_("Could not find AS%s") % asn, file=sys.stderr)
fadc1af0
MT
397 ret = 1
398 continue
399
400 print(_("AS%(asn)s belongs to %(name)s") % { "asn" : a.number, "name" : a.name })
401
402 return ret
5118a4b8 403
2538ed9a 404 def handle_search_as(self, db, ns):
da3e360e
MT
405 for query in ns.query:
406 # Print all matches ASes
2538ed9a 407 for a in db.search_as(query):
da3e360e
MT
408 print(a)
409
a6f1e346 410 def handle_update(self, db, ns):
97769987 411 if ns.cron and db:
60c1ac03 412 now = time.time()
97769987
MT
413
414 if ns.cron == "daily":
415 delta = datetime.timedelta(days=1)
416 elif ns.cron == "weekly":
417 delta = datetime.timedelta(days=7)
418 elif ns.cron == "monthly":
419 delta = datetime.timedelta(days=30)
420
60c1ac03
PM
421 delta = delta.total_seconds()
422
97769987 423 # Check if the database has recently been updated
60c1ac03 424 if db.created_at >= (now - delta):
97769987 425 log.info(
60c1ac03 426 _("The database has been updated recently"),
97769987 427 )
a243587a 428 return 3
97769987 429
a6f1e346
MT
430 # Fetch the timestamp we need from DNS
431 t = location.discover_latest_version()
432
a6f1e346 433 # Check the version of the local database
0c74f6b1 434 if db and t and db.created_at >= t:
a6f1e346
MT
435 log.info("Already on the latest version")
436 return
437
438 # Download the database into the correct directory
439 tmpdir = os.path.dirname(ns.database)
440
441 # Create a downloader
442 d = location.downloader.Downloader()
443
444 # Try downloading a new database
445 try:
2a550d12 446 t = d.download(public_key=ns.public_key, timestamp=t, tmpdir=tmpdir)
a6f1e346
MT
447
448 # If no file could be downloaded, log a message
449 except FileNotFoundError as e:
450 log.error("Could not download a new database")
451 return 1
452
453 # If we have not received a new file, there is nothing to do
454 if not t:
455 return 3
456
457 # Move temporary file to destination
458 shutil.move(t.name, ns.database)
459
460 return 0
461
e99a7226 462 def handle_verify(self, db, ns):
a6f1e346
MT
463 # Verify the database
464 with open(ns.public_key, "r") as f:
465 if not db.verify(f):
466 log.error("Could not verify database")
467 return 1
468
469 # Success
470 log.debug("Database successfully verified")
471 return 0
472
4439e317
MT
473 def __get_output_formatter(self, ns):
474 try:
88ef7e9c 475 cls = location.export.formats[ns.format]
4439e317 476 except KeyError:
88ef7e9c 477 cls = location.export.OutputFormatter
4439e317 478
88ef7e9c 479 return cls
4439e317 480
fa9a3663
MT
481 def handle_list_countries(self, db, ns):
482 for country in db.countries:
483 line = [
484 country.code,
485 ]
486
487 if ns.show_continent:
488 line.append(country.continent_code)
489
490 if ns.show_name:
491 line.append(country.name)
492
493 # Format the output
494 line = " ".join(line)
495
496 # Print the output
497 print(line)
498
43154ed7 499 def handle_list_networks_by_as(self, db, ns):
88ef7e9c
MT
500 writer = self.__get_output_formatter(ns)
501
502 for asn in ns.asn:
503 f = writer(sys.stdout, prefix="AS%s" % asn)
504
505 # Print all matching networks
b178117b 506 for n in db.search_networks(asns=[asn], family=ns.family):
88ef7e9c
MT
507 f.write(n)
508
509 f.finish()
43154ed7 510
ccc7ab4e 511 def handle_list_networks_by_cc(self, db, ns):
88ef7e9c
MT
512 writer = self.__get_output_formatter(ns)
513
514 for country_code in ns.country_code:
515 # Open standard output
516 f = writer(sys.stdout, prefix=country_code)
517
518 # Print all matching networks
0cb1a88c 519 for n in db.search_networks(country_codes=[country_code], family=ns.family):
88ef7e9c
MT
520 f.write(n)
521
522 f.finish()
4439e317 523
bbdb2e0a
MT
524 def handle_list_networks_by_flags(self, db, ns):
525 flags = 0
526
527 if ns.anonymous_proxy:
528 flags |= location.NETWORK_FLAG_ANONYMOUS_PROXY
529
530 if ns.satellite_provider:
531 flags |= location.NETWORK_FLAG_SATELLITE_PROVIDER
532
533 if ns.anycast:
534 flags |= location.NETWORK_FLAG_ANYCAST
535
e17e804e
PM
536 if ns.drop:
537 flags |= location.NETWORK_FLAG_DROP
538
228d0e74
MT
539 if not flags:
540 raise ValueError(_("You must at least pass one flag"))
541
88ef7e9c
MT
542 writer = self.__get_output_formatter(ns)
543 f = writer(sys.stdout, prefix="custom")
544
545 for n in db.search_networks(flags=flags, family=ns.family):
546 f.write(n)
547
548 f.finish()
549
550 def handle_export(self, db, ns):
551 countries, asns = [], []
552
553 # Translate family
df072005
MT
554 if ns.family:
555 families = [ ns.family ]
88ef7e9c
MT
556 else:
557 families = [ socket.AF_INET6, socket.AF_INET ]
558
559 for object in ns.objects:
560 m = re.match("^AS(\d+)$", object)
561 if m:
562 object = int(m.group(1))
563
564 asns.append(object)
565
566 elif location.country_code_is_valid(object) \
e17e804e 567 or object in ("A1", "A2", "A3", "XD"):
88ef7e9c
MT
568 countries.append(object)
569
570 else:
571 log.warning("Invalid argument: %s" % object)
572 continue
573
10fa313b 574 # Default to exporting all countries
88ef7e9c 575 if not countries and not asns:
e17e804e 576 countries = ["A1", "A2", "A3", "XD"] + [country.code for country in db.countries]
88ef7e9c
MT
577
578 # Select the output format
579 writer = self.__get_output_formatter(ns)
580
581 e = location.export.Exporter(db, writer)
582 e.export(ns.directory, countries=countries, asns=asns, families=families)
bbdb2e0a 583
ccc7ab4e 584
97769987
MT
585def format_timedelta(t):
586 s = []
587
588 if t.days:
589 s.append(
590 _("One Day", "%(days)s Days", t.days) % { "days" : t.days, }
591 )
592
593 hours = t.seconds // 3600
594 if hours:
595 s.append(
596 _("One Hour", "%(hours)s Hours", hours) % { "hours" : hours, }
597 )
598
599 minutes = (t.seconds % 3600) // 60
600 if minutes:
601 s.append(
602 _("One Minute", "%(minutes)s Minutes", minutes) % { "minutes" : minutes, }
603 )
604
605 seconds = t.seconds % 60
606 if t.seconds:
607 s.append(
608 _("One Second", "%(seconds)s Seconds", seconds) % { "seconds" : seconds, }
609 )
610
611 if not s:
612 return _("Now")
613
614 return _("%s ago") % ", ".join(s)
615
5118a4b8
MT
616def main():
617 # Run the command line interface
618 c = CLI()
619 c.run()
620
621main()