]> git.ipfire.org Git - people/ms/libloc.git/blame - src/python/location-importer.in
Revert "Revert "Revert "Revert "importer: Import raw sources for inetnum's again""""
[people/ms/libloc.git] / src / python / location-importer.in
CommitLineData
78ff0cf2
MT
1#!/usr/bin/python3
2###############################################################################
3# #
4# libloc - A library to determine the location of someone on the Internet #
5# #
6# Copyright (C) 2020 IPFire Development Team <info@ipfire.org> #
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
6ffd06b5 21import ipaddress
78ff0cf2 22import logging
6ffd06b5
MT
23import math
24import re
22d8d199 25import socket
78ff0cf2 26import sys
83d61c46 27import telnetlib
78ff0cf2
MT
28
29# Load our location module
30import location
29c6fa22 31import location.database
3192b66c 32import location.importer
78ff0cf2
MT
33from location.i18n import _
34
35# Initialise logging
36log = logging.getLogger("location.importer")
37log.propagate = 1
38
39class CLI(object):
40 def parse_cli(self):
41 parser = argparse.ArgumentParser(
42 description=_("Location Importer Command Line Interface"),
43 )
6ffd06b5 44 subparsers = parser.add_subparsers()
78ff0cf2
MT
45
46 # Global configuration flags
47 parser.add_argument("--debug", action="store_true",
48 help=_("Enable debug output"))
bc1f5f53
MT
49 parser.add_argument("--quiet", action="store_true",
50 help=_("Enable quiet mode"))
78ff0cf2
MT
51
52 # version
53 parser.add_argument("--version", action="version",
54 version="%(prog)s @VERSION@")
55
29c6fa22
MT
56 # Database
57 parser.add_argument("--database-host", required=True,
58 help=_("Database Hostname"), metavar=_("HOST"))
59 parser.add_argument("--database-name", required=True,
60 help=_("Database Name"), metavar=_("NAME"))
61 parser.add_argument("--database-username", required=True,
62 help=_("Database Username"), metavar=_("USERNAME"))
63 parser.add_argument("--database-password", required=True,
64 help=_("Database Password"), metavar=_("PASSWORD"))
65
0983f3dd
MT
66 # Write Database
67 write = subparsers.add_parser("write", help=_("Write database to file"))
68 write.set_defaults(func=self.handle_write)
69 write.add_argument("file", nargs=1, help=_("Database File"))
70 write.add_argument("--signing-key", nargs="?", type=open, help=_("Signing Key"))
1164d876 71 write.add_argument("--backup-signing-key", nargs="?", type=open, help=_("Backup Signing Key"))
0983f3dd
MT
72 write.add_argument("--vendor", nargs="?", help=_("Sets the vendor"))
73 write.add_argument("--description", nargs="?", help=_("Sets a description"))
74 write.add_argument("--license", nargs="?", help=_("Sets the license"))
b904896a 75 write.add_argument("--version", type=int, help=_("Database Format Version"))
0983f3dd 76
6ffd06b5
MT
77 # Update WHOIS
78 update_whois = subparsers.add_parser("update-whois", help=_("Update WHOIS Information"))
79 update_whois.set_defaults(func=self.handle_update_whois)
80
83d61c46
MT
81 # Update announcements
82 update_announcements = subparsers.add_parser("update-announcements",
83 help=_("Update BGP Annoucements"))
84 update_announcements.set_defaults(func=self.handle_update_announcements)
85 update_announcements.add_argument("server", nargs=1,
86 help=_("Route Server to connect to"), metavar=_("SERVER"))
87
d7fc3057
MT
88 # Update overrides
89 update_overrides = subparsers.add_parser("update-overrides",
90 help=_("Update overrides"),
91 )
92 update_overrides.add_argument(
93 "files", nargs="+", help=_("Files to import"),
94 )
95 update_overrides.set_defaults(func=self.handle_update_overrides)
96
8084b33a
MT
97 # Import countries
98 import_countries = subparsers.add_parser("import-countries",
99 help=_("Import countries"),
100 )
101 import_countries.add_argument("file", nargs=1, type=argparse.FileType("r"),
102 help=_("File to import"))
103 import_countries.set_defaults(func=self.handle_import_countries)
104
78ff0cf2
MT
105 args = parser.parse_args()
106
bc1f5f53 107 # Configure logging
78ff0cf2 108 if args.debug:
f9de5e61 109 location.logger.set_level(logging.DEBUG)
bc1f5f53
MT
110 elif args.quiet:
111 location.logger.set_level(logging.WARNING)
78ff0cf2 112
6ffd06b5
MT
113 # Print usage if no action was given
114 if not "func" in args:
115 parser.print_usage()
116 sys.exit(2)
117
78ff0cf2
MT
118 return args
119
120 def run(self):
121 # Parse command line arguments
122 args = self.parse_cli()
123
29c6fa22 124 # Initialise database
6ffd06b5 125 self.db = self._setup_database(args)
29c6fa22 126
78ff0cf2 127 # Call function
6ffd06b5 128 ret = args.func(args)
78ff0cf2
MT
129
130 # Return with exit code
131 if ret:
132 sys.exit(ret)
133
134 # Otherwise just exit
135 sys.exit(0)
136
29c6fa22
MT
137 def _setup_database(self, ns):
138 """
139 Initialise the database
140 """
141 # Connect to database
142 db = location.database.Connection(
143 host=ns.database_host, database=ns.database_name,
144 user=ns.database_username, password=ns.database_password,
145 )
146
147 with db.transaction():
148 db.execute("""
83d61c46
MT
149 -- announcements
150 CREATE TABLE IF NOT EXISTS announcements(network inet, autnum bigint,
151 first_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
152 last_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP);
153 CREATE UNIQUE INDEX IF NOT EXISTS announcements_networks ON announcements(network);
154 CREATE INDEX IF NOT EXISTS announcements_family ON announcements(family(network));
a1707d89 155 CREATE INDEX IF NOT EXISTS announcements_search ON announcements USING GIST(network inet_ops);
83d61c46 156
6ffd06b5 157 -- autnums
0983f3dd 158 CREATE TABLE IF NOT EXISTS autnums(number bigint, name text NOT NULL);
6ffd06b5
MT
159 CREATE UNIQUE INDEX IF NOT EXISTS autnums_number ON autnums(number);
160
8084b33a
MT
161 -- countries
162 CREATE TABLE IF NOT EXISTS countries(
163 country_code text NOT NULL, name text NOT NULL, continent_code text NOT NULL);
164 CREATE UNIQUE INDEX IF NOT EXISTS countries_country_code ON countries(country_code);
165
429a43d1 166 -- networks
83d61c46 167 CREATE TABLE IF NOT EXISTS networks(network inet, country text);
429a43d1 168 CREATE UNIQUE INDEX IF NOT EXISTS networks_network ON networks(network);
83d61c46 169 CREATE INDEX IF NOT EXISTS networks_search ON networks USING GIST(network inet_ops);
d7fc3057
MT
170
171 -- overrides
172 CREATE TABLE IF NOT EXISTS autnum_overrides(
173 number bigint NOT NULL,
174 name text,
bd1aa6a1 175 country text,
b8e25b71
MT
176 is_anonymous_proxy boolean,
177 is_satellite_provider boolean,
178 is_anycast boolean
d7fc3057
MT
179 );
180 CREATE UNIQUE INDEX IF NOT EXISTS autnum_overrides_number
181 ON autnum_overrides(number);
182
183 CREATE TABLE IF NOT EXISTS network_overrides(
184 network inet NOT NULL,
185 country text,
b8e25b71
MT
186 is_anonymous_proxy boolean,
187 is_satellite_provider boolean,
188 is_anycast boolean
d7fc3057
MT
189 );
190 CREATE UNIQUE INDEX IF NOT EXISTS network_overrides_network
191 ON network_overrides(network);
991baf53
MT
192 CREATE INDEX IF NOT EXISTS network_overrides_search
193 ON network_overrides USING GIST(network inet_ops);
29c6fa22
MT
194 """)
195
196 return db
197
0983f3dd
MT
198 def handle_write(self, ns):
199 """
200 Compiles a database in libloc format out of what is in the database
201 """
0983f3dd 202 # Allocate a writer
1164d876 203 writer = location.Writer(ns.signing_key, ns.backup_signing_key)
0983f3dd
MT
204
205 # Set all metadata
206 if ns.vendor:
207 writer.vendor = ns.vendor
208
209 if ns.description:
210 writer.description = ns.description
211
212 if ns.license:
213 writer.license = ns.license
214
215 # Add all Autonomous Systems
216 log.info("Writing Autonomous Systems...")
217
218 # Select all ASes with a name
6e97c44b
MT
219 rows = self.db.query("""
220 SELECT
221 autnums.number AS number,
222 COALESCE(
223 (SELECT overrides.name FROM autnum_overrides overrides
224 WHERE overrides.number = autnums.number),
225 autnums.name
226 ) AS name
227 FROM autnums
228 WHERE name <> %s ORDER BY number
229 """, "")
0983f3dd
MT
230
231 for row in rows:
232 a = writer.add_as(row.number)
233 a.name = row.name
234
235 # Add all networks
236 log.info("Writing networks...")
237
238 # Select all known networks
239 rows = self.db.query("""
5372d9c7
MT
240 -- Return a list of those networks enriched with all
241 -- other information that we store in the database
0983f3dd 242 SELECT
bbea93a7
MT
243 DISTINCT ON (network)
244 network,
245 autnum,
bd1aa6a1
MT
246
247 -- Country
248 COALESCE(
249 (
250 SELECT country FROM network_overrides overrides
bbea93a7 251 WHERE networks.network <<= overrides.network
bd1aa6a1
MT
252 ORDER BY masklen(overrides.network) DESC
253 LIMIT 1
254 ),
255 (
256 SELECT country FROM autnum_overrides overrides
bbea93a7 257 WHERE networks.autnum = overrides.number
bd1aa6a1
MT
258 ),
259 networks.country
260 ) AS country,
8e8555bb 261
0983f3dd 262 -- Flags
1422b5d4
MT
263 COALESCE(
264 (
265 SELECT is_anonymous_proxy FROM network_overrides overrides
bbea93a7 266 WHERE networks.network <<= overrides.network
1422b5d4
MT
267 ORDER BY masklen(overrides.network) DESC
268 LIMIT 1
269 ),
270 (
271 SELECT is_anonymous_proxy FROM autnum_overrides overrides
bbea93a7 272 WHERE networks.autnum = overrides.number
b8e25b71
MT
273 ),
274 FALSE
1422b5d4
MT
275 ) AS is_anonymous_proxy,
276 COALESCE(
277 (
278 SELECT is_satellite_provider FROM network_overrides overrides
bbea93a7 279 WHERE networks.network <<= overrides.network
1422b5d4
MT
280 ORDER BY masklen(overrides.network) DESC
281 LIMIT 1
282 ),
283 (
284 SELECT is_satellite_provider FROM autnum_overrides overrides
bbea93a7 285 WHERE networks.autnum = overrides.number
b8e25b71
MT
286 ),
287 FALSE
1422b5d4
MT
288 ) AS is_satellite_provider,
289 COALESCE(
290 (
291 SELECT is_anycast FROM network_overrides overrides
bbea93a7 292 WHERE networks.network <<= overrides.network
1422b5d4
MT
293 ORDER BY masklen(overrides.network) DESC
294 LIMIT 1
295 ),
296 (
297 SELECT is_anycast FROM autnum_overrides overrides
bbea93a7 298 WHERE networks.autnum = overrides.number
b8e25b71
MT
299 ),
300 FALSE
bbea93a7
MT
301 ) AS is_anycast
302 FROM (
303 SELECT
304 known_networks.network AS network,
305 announcements.autnum AS autnum,
306 networks.country AS country,
307
308 -- Must be part of returned values for ORDER BY clause
309 masklen(announcements.network) AS sort_a,
310 masklen(networks.network) AS sort_b
311 FROM (
312 SELECT network FROM announcements
313 UNION ALL
314 SELECT network FROM networks
315 UNION ALL
316 SELECT network FROM network_overrides
317 ) known_networks
318 LEFT JOIN
319 announcements ON known_networks.network <<= announcements.network
320 LEFT JOIN
321 networks ON known_networks.network <<= networks.network
322 ORDER BY
323 known_networks.network,
324 sort_a DESC,
325 sort_b DESC
326 ) networks
0983f3dd
MT
327 """)
328
329 for row in rows:
330 network = writer.add_network(row.network)
331
5372d9c7
MT
332 # Save country
333 if row.country:
334 network.country_code = row.country
335
336 # Save ASN
337 if row.autnum:
338 network.asn = row.autnum
0983f3dd
MT
339
340 # Set flags
341 if row.is_anonymous_proxy:
342 network.set_flag(location.NETWORK_FLAG_ANONYMOUS_PROXY)
343
344 if row.is_satellite_provider:
345 network.set_flag(location.NETWORK_FLAG_SATELLITE_PROVIDER)
346
347 if row.is_anycast:
348 network.set_flag(location.NETWORK_FLAG_ANYCAST)
349
8084b33a
MT
350 # Add all countries
351 log.info("Writing countries...")
352 rows = self.db.query("SELECT * FROM countries ORDER BY country_code")
353
354 for row in rows:
355 c = writer.add_country(row.country_code)
356 c.continent_code = row.continent_code
357 c.name = row.name
358
0983f3dd
MT
359 # Write everything to file
360 log.info("Writing database to file...")
361 for file in ns.file:
362 writer.write(file)
363
6ffd06b5
MT
364 def handle_update_whois(self, ns):
365 downloader = location.importer.Downloader()
366
367 # Download all sources
0365119d
MT
368 with self.db.transaction():
369 # Create some temporary tables to store parsed data
370 self.db.execute("""
371 CREATE TEMPORARY TABLE _autnums(number integer, organization text)
372 ON COMMIT DROP;
373 CREATE UNIQUE INDEX _autnums_number ON _autnums(number);
374
2cd2e342 375 CREATE TEMPORARY TABLE _organizations(handle text, name text NOT NULL)
0365119d
MT
376 ON COMMIT DROP;
377 CREATE UNIQUE INDEX _organizations_handle ON _organizations(handle);
378 """)
379
380 for source in location.importer.WHOIS_SOURCES:
6ffd06b5
MT
381 with downloader.request(source, return_blocks=True) as f:
382 for block in f:
383 self._parse_block(block)
384
0365119d
MT
385 self.db.execute("""
386 INSERT INTO autnums(number, name)
387 SELECT _autnums.number, _organizations.name FROM _autnums
2cd2e342 388 JOIN _organizations ON _autnums.organization = _organizations.handle
ee6ea398 389 ON CONFLICT (number) DO UPDATE SET name = excluded.name;
0365119d
MT
390 """)
391
429a43d1
MT
392 # Download all extended sources
393 for source in location.importer.EXTENDED_SOURCES:
394 with self.db.transaction():
429a43d1
MT
395 # Download data
396 with downloader.request(source) as f:
397 for line in f:
398 self._parse_line(line)
399
6ffd06b5
MT
400 def _parse_block(self, block):
401 # Get first line to find out what type of block this is
402 line = block[0]
403
6ffd06b5 404 # aut-num
429a43d1 405 if line.startswith("aut-num:"):
6ffd06b5
MT
406 return self._parse_autnum_block(block)
407
aadac4c5
PM
408 # inetnum
409 if line.startswith("inet6num:") or line.startswith("inetnum:"):
410 return self._parse_inetnum_block(block)
411
6ffd06b5
MT
412 # organisation
413 elif line.startswith("organisation:"):
414 return self._parse_org_block(block)
415
6ffd06b5 416 def _parse_autnum_block(self, block):
6ffd06b5
MT
417 autnum = {}
418 for line in block:
419 # Split line
420 key, val = split_line(line)
421
422 if key == "aut-num":
423 m = re.match(r"^(AS|as)(\d+)", val)
424 if m:
425 autnum["asn"] = m.group(2)
426
0365119d 427 elif key == "org":
6ffd06b5
MT
428 autnum[key] = val
429
430 # Skip empty objects
431 if not autnum:
432 return
433
434 # Insert into database
0365119d
MT
435 self.db.execute("INSERT INTO _autnums(number, organization) \
436 VALUES(%s, %s) ON CONFLICT (number) DO UPDATE SET \
437 organization = excluded.organization",
438 autnum.get("asn"), autnum.get("org"),
6ffd06b5
MT
439 )
440
aadac4c5
PM
441 def _parse_inetnum_block(self, block):
442 logging.debug("Parsing inetnum block:")
443
444 inetnum = {}
445 for line in block:
446 logging.debug(line)
447
448 # Split line
449 key, val = split_line(line)
450
451 if key == "inetnum":
452 start_address, delim, end_address = val.partition("-")
453
454 # Strip any excess space
455 start_address, end_address = start_address.rstrip(), end_address.strip()
456
457 # Convert to IP address
458 try:
459 start_address = ipaddress.ip_address(start_address)
460 end_address = ipaddress.ip_address(end_address)
461 except ValueError:
462 logging.warning("Could not parse line: %s" % line)
463 return
464
465 # Set prefix to default
466 prefix = 32
467
468 # Count number of addresses in this subnet
469 num_addresses = int(end_address) - int(start_address)
470 if num_addresses:
471 prefix -= math.log(num_addresses, 2)
472
473 inetnum["inetnum"] = "%s/%.0f" % (start_address, prefix)
474
475 elif key == "inet6num":
476 inetnum[key] = val
477
478 elif key == "country":
479 if val == "UNITED STATES":
480 val = "US"
481
482 inetnum[key] = val.upper()
483
484 # Skip empty objects
485 if not inetnum:
486 return
487
488 network = ipaddress.ip_network(inetnum.get("inet6num") or inetnum.get("inetnum"), strict=False)
489
490 # Bail out in case we have processed a non-public IP network
491 if network.is_private:
492 logging.warning("Skipping non-globally routable network: %s" % network)
493 return
494
495 self.db.execute("INSERT INTO networks(network, country) \
496 VALUES(%s, %s) ON CONFLICT (network) DO UPDATE SET country = excluded.country",
497 "%s" % network, inetnum.get("country"),
498 )
499
6ffd06b5
MT
500 def _parse_org_block(self, block):
501 org = {}
502 for line in block:
503 # Split line
504 key, val = split_line(line)
505
0365119d 506 if key in ("organisation", "org-name"):
6ffd06b5
MT
507 org[key] = val
508
509 # Skip empty objects
510 if not org:
511 return
512
0365119d
MT
513 self.db.execute("INSERT INTO _organizations(handle, name) \
514 VALUES(%s, %s) ON CONFLICT (handle) DO \
515 UPDATE SET name = excluded.name",
516 org.get("organisation"), org.get("org-name"),
6ffd06b5
MT
517 )
518
429a43d1
MT
519 def _parse_line(self, line):
520 # Skip version line
521 if line.startswith("2"):
522 return
6ffd06b5 523
429a43d1
MT
524 # Skip comments
525 if line.startswith("#"):
526 return
6ffd06b5 527
429a43d1
MT
528 try:
529 registry, country_code, type, line = line.split("|", 3)
530 except:
531 log.warning("Could not parse line: %s" % line)
532 return
6ffd06b5 533
429a43d1
MT
534 # Skip any lines that are for stats only
535 if country_code == "*":
6ffd06b5
MT
536 return
537
429a43d1
MT
538 if type in ("ipv6", "ipv4"):
539 return self._parse_ip_line(country_code, type, line)
540
429a43d1
MT
541 def _parse_ip_line(self, country, type, line):
542 try:
543 address, prefix, date, status, organization = line.split("|")
544 except ValueError:
545 organization = None
546
547 # Try parsing the line without organization
548 try:
549 address, prefix, date, status = line.split("|")
550 except ValueError:
551 log.warning("Unhandled line format: %s" % line)
552 return
553
554 # Skip anything that isn't properly assigned
555 if not status in ("assigned", "allocated"):
556 return
557
558 # Cast prefix into an integer
559 try:
560 prefix = int(prefix)
561 except:
562 log.warning("Invalid prefix: %s" % prefix)
7177031f 563 return
429a43d1
MT
564
565 # Fix prefix length for IPv4
566 if type == "ipv4":
567 prefix = 32 - int(math.log(prefix, 2))
568
569 # Try to parse the address
570 try:
571 network = ipaddress.ip_network("%s/%s" % (address, prefix), strict=False)
572 except ValueError:
573 log.warning("Invalid IP address: %s" % address)
574 return
575
87b3e102
MT
576 self.db.execute("INSERT INTO networks(network, country) \
577 VALUES(%s, %s) ON CONFLICT (network) DO \
578 UPDATE SET country = excluded.country",
579 "%s" % network, country,
6ffd06b5
MT
580 )
581
83d61c46
MT
582 def handle_update_announcements(self, ns):
583 server = ns.server[0]
584
22d8d199
MT
585 with self.db.transaction():
586 if server.startswith("/"):
587 self._handle_update_announcements_from_bird(server)
588 else:
589 self._handle_update_announcements_from_telnet(server)
590
591 # Purge anything we never want here
592 self.db.execute("""
593 -- Delete default routes
594 DELETE FROM announcements WHERE network = '::/0' OR network = '0.0.0.0/0';
595
596 -- Delete anything that is not global unicast address space
597 DELETE FROM announcements WHERE family(network) = 6 AND NOT network <<= '2000::/3';
598
599 -- DELETE "current network" address space
600 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '0.0.0.0/8';
601
602 -- DELETE local loopback address space
603 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '127.0.0.0/8';
604
605 -- DELETE RFC 1918 address space
606 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '10.0.0.0/8';
607 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '172.16.0.0/12';
608 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.168.0.0/16';
609
610 -- DELETE test, benchmark and documentation address space
611 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.0.0/24';
612 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.2.0/24';
613 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.18.0.0/15';
614 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.51.100.0/24';
615 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '203.0.113.0/24';
616
617 -- DELETE CGNAT address space (RFC 6598)
618 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '100.64.0.0/10';
619
620 -- DELETE link local address space
621 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '169.254.0.0/16';
622
623 -- DELETE IPv6 to IPv4 (6to4) address space
624 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.88.99.0/24';
625
626 -- DELETE multicast and reserved address space
627 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '224.0.0.0/4';
628 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '240.0.0.0/4';
629
630 -- Delete networks that are too small to be in the global routing table
631 DELETE FROM announcements WHERE family(network) = 6 AND masklen(network) > 48;
632 DELETE FROM announcements WHERE family(network) = 4 AND masklen(network) > 24;
633
634 -- Delete any non-public or reserved ASNs
635 DELETE FROM announcements WHERE NOT (
636 (autnum >= 1 AND autnum <= 23455)
637 OR
638 (autnum >= 23457 AND autnum <= 64495)
639 OR
640 (autnum >= 131072 AND autnum <= 4199999999)
641 );
642
643 -- Delete everything that we have not seen for 14 days
644 DELETE FROM announcements WHERE last_seen_at <= CURRENT_TIMESTAMP - INTERVAL '14 days';
645 """)
646
647 def _handle_update_announcements_from_bird(self, server):
648 # Pre-compile the regular expression for faster searching
dc0be5c5 649 route = re.compile(b"^\s(.+?)\s+.+?\[AS(.*?).\]$")
22d8d199
MT
650
651 log.info("Requesting routing table from Bird (%s)" % server)
652
653 # Send command to list all routes
654 for line in self._bird_cmd(server, "show route"):
655 m = route.match(line)
656 if not m:
657 log.debug("Could not parse line: %s" % line.decode())
658 continue
659
660 # Fetch the extracted network and ASN
661 network, autnum = m.groups()
662
663 # Insert it into the database
664 self.db.execute("INSERT INTO announcements(network, autnum) \
665 VALUES(%s, %s) ON CONFLICT (network) DO \
666 UPDATE SET autnum = excluded.autnum, last_seen_at = CURRENT_TIMESTAMP",
667 network.decode(), autnum.decode(),
668 )
669
670 def _handle_update_announcements_from_telnet(self, server):
83d61c46 671 # Pre-compile regular expression for routes
83d61c46
MT
672 route = re.compile(b"^\*[\s\>]i([^\s]+).+?(\d+)\si\r\n", re.MULTILINE|re.DOTALL)
673
674 with telnetlib.Telnet(server) as t:
675 # Enable debug mode
676 #if ns.debug:
677 # t.set_debuglevel(10)
678
679 # Wait for console greeting
fcd5b8b2
MT
680 greeting = t.read_until(b"> ", timeout=30)
681 if not greeting:
682 log.error("Could not get a console prompt")
683 return 1
83d61c46
MT
684
685 # Disable pagination
686 t.write(b"terminal length 0\n")
687
688 # Wait for the prompt to return
689 t.read_until(b"> ")
690
691 # Fetch the routing tables
22d8d199
MT
692 for protocol in ("ipv6", "ipv4"):
693 log.info("Requesting %s routing table" % protocol)
83d61c46 694
22d8d199
MT
695 # Request the full unicast routing table
696 t.write(b"show bgp %s unicast\n" % protocol.encode())
83d61c46 697
22d8d199
MT
698 # Read entire header which ends with "Path"
699 t.read_until(b"Path\r\n")
83d61c46 700
22d8d199
MT
701 while True:
702 # Try reading a full entry
703 # Those might be broken across multiple lines but ends with i
704 line = t.read_until(b"i\r\n", timeout=5)
705 if not line:
706 break
83d61c46 707
22d8d199
MT
708 # Show line for debugging
709 #log.debug(repr(line))
d773c1bc 710
22d8d199
MT
711 # Try finding a route in here
712 m = route.match(line)
713 if m:
714 network, autnum = m.groups()
83d61c46 715
22d8d199
MT
716 # Convert network to string
717 network = network.decode()
83d61c46 718
22d8d199
MT
719 # Append /24 for IPv4 addresses
720 if not "/" in network and not ":" in network:
721 network = "%s/24" % network
83d61c46 722
22d8d199
MT
723 # Convert AS number to integer
724 autnum = int(autnum)
83d61c46 725
22d8d199 726 log.info("Found announcement for %s by %s" % (network, autnum))
83d61c46 727
22d8d199
MT
728 self.db.execute("INSERT INTO announcements(network, autnum) \
729 VALUES(%s, %s) ON CONFLICT (network) DO \
730 UPDATE SET autnum = excluded.autnum, last_seen_at = CURRENT_TIMESTAMP",
731 network, autnum,
732 )
83d61c46 733
22d8d199 734 log.info("Finished reading the %s routing table" % protocol)
1d4e4e8f 735
22d8d199
MT
736 def _bird_cmd(self, socket_path, command):
737 # Connect to the socket
738 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
739 s.connect(socket_path)
cedee656 740
22d8d199
MT
741 # Allocate some buffer
742 buffer = b""
83d61c46 743
22d8d199
MT
744 # Send the command
745 s.send(b"%s\n" % command.encode())
209c04b6 746
22d8d199
MT
747 while True:
748 # Fill up the buffer
749 buffer += s.recv(4096)
209c04b6 750
22d8d199
MT
751 while True:
752 # Search for the next newline
753 pos = buffer.find(b"\n")
209c04b6 754
22d8d199
MT
755 # If we cannot find one, we go back and read more data
756 if pos <= 0:
757 break
209c04b6 758
22d8d199
MT
759 # Cut after the newline character
760 pos += 1
b89cee80 761
22d8d199
MT
762 # Split the line we want and keep the rest in buffer
763 line, buffer = buffer[:pos], buffer[pos:]
83d61c46 764
22d8d199
MT
765 # Look for the end-of-output indicator
766 if line == b"0000 \n":
767 return
83d61c46 768
22d8d199
MT
769 # Otherwise return the line
770 yield line
83d61c46 771
d7fc3057
MT
772 def handle_update_overrides(self, ns):
773 with self.db.transaction():
774 # Drop all data that we have
775 self.db.execute("""
776 TRUNCATE TABLE autnum_overrides;
777 TRUNCATE TABLE network_overrides;
778 """)
779
780 for file in ns.files:
781 log.info("Reading %s..." % file)
782
783 with open(file, "rb") as f:
784 for type, block in location.importer.read_blocks(f):
785 if type == "net":
786 network = block.get("net")
787 # Try to parse and normalise the network
788 try:
789 network = ipaddress.ip_network(network, strict=False)
790 except ValueError as e:
791 log.warning("Invalid IP network: %s: %s" % (network, e))
792 continue
793
94dfab8c
MT
794 # Prevent that we overwrite all networks
795 if network.prefixlen == 0:
796 log.warning("Skipping %s: You cannot overwrite default" % network)
797 continue
798
d7fc3057
MT
799 self.db.execute("""
800 INSERT INTO network_overrides(
801 network,
802 country,
803 is_anonymous_proxy,
804 is_satellite_provider,
805 is_anycast
56f6587a 806 ) VALUES (%s, %s, %s, %s, %s)
d7fc3057
MT
807 ON CONFLICT (network) DO NOTHING""",
808 "%s" % network,
809 block.get("country"),
28d29b7c
MT
810 self._parse_bool(block, "is-anonymous-proxy"),
811 self._parse_bool(block, "is-satellite-provider"),
812 self._parse_bool(block, "is-anycast"),
d7fc3057
MT
813 )
814
f476cdfd
MT
815 elif type == "aut-num":
816 autnum = block.get("aut-num")
d7fc3057
MT
817
818 # Check if AS number begins with "AS"
819 if not autnum.startswith("AS"):
820 log.warning("Invalid AS number: %s" % autnum)
821 continue
822
823 # Strip "AS"
824 autnum = autnum[2:]
825
826 self.db.execute("""
827 INSERT INTO autnum_overrides(
828 number,
829 name,
bd1aa6a1 830 country,
d7fc3057
MT
831 is_anonymous_proxy,
832 is_satellite_provider,
833 is_anycast
bd1aa6a1 834 ) VALUES(%s, %s, %s, %s, %s, %s)
d7fc3057 835 ON CONFLICT DO NOTHING""",
bd1aa6a1
MT
836 autnum,
837 block.get("name"),
838 block.get("country"),
28d29b7c
MT
839 self._parse_bool(block, "is-anonymous-proxy"),
840 self._parse_bool(block, "is-satellite-provider"),
841 self._parse_bool(block, "is-anycast"),
d7fc3057
MT
842 )
843
844 else:
845 log.warning("Unsupport type: %s" % type)
846
28d29b7c
MT
847 @staticmethod
848 def _parse_bool(block, key):
849 val = block.get(key)
850
851 # There is no point to proceed when we got None
852 if val is None:
853 return
854
855 # Convert to lowercase
856 val = val.lower()
857
858 # True
859 if val in ("yes", "1"):
860 return True
861
862 # False
863 if val in ("no", "0"):
864 return False
865
866 # Default to None
867 return None
868
8084b33a
MT
869 def handle_import_countries(self, ns):
870 with self.db.transaction():
871 # Drop all data that we have
872 self.db.execute("TRUNCATE TABLE countries")
873
874 for file in ns.file:
875 for line in file:
876 line = line.rstrip()
877
878 # Ignore any comments
879 if line.startswith("#"):
880 continue
881
882 try:
883 country_code, continent_code, name = line.split(maxsplit=2)
884 except:
885 log.warning("Could not parse line: %s" % line)
886 continue
887
888 self.db.execute("INSERT INTO countries(country_code, name, continent_code) \
889 VALUES(%s, %s, %s) ON CONFLICT DO NOTHING", country_code, name, continent_code)
890
6ffd06b5
MT
891
892def split_line(line):
893 key, colon, val = line.partition(":")
894
895 # Strip any excess space
896 key = key.strip()
897 val = val.strip()
78ff0cf2 898
6ffd06b5 899 return key, val
78ff0cf2
MT
900
901def main():
902 # Run the command line interface
903 c = CLI()
904 c.run()
905
906main()