Skip to content

Commit 6abe772

Browse files
fjahrsipa
andcommitted
contrib: Add asmap-tool
Co-authored-by: Pieter Wuille <pieter@wuille.net>
1 parent 8da62a1 commit 6abe772

File tree

4 files changed

+176
-3
lines changed

4 files changed

+176
-3
lines changed

contrib/asmap/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# ASMap Tool
2+
3+
Tool for performing various operations on textual and binary asmap files,
4+
particularly encoding/compressing the raw data to the binary format that can
5+
be used in Bitcoin Core with the `-asmap` option.
6+
7+
Example usage:
8+
```
9+
python3 asmap-tool.py encode /path/to/input.file /path/to/output.file
10+
python3 asmap-tool.py decode /path/to/input.file /path/to/output.file
11+
python3 asmap-tool.py diff /path/to/first.file /path/to/second.file
12+
```

contrib/asmap/asmap-tool.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2022 Pieter Wuille
3+
# Distributed under the MIT software license, see the accompanying
4+
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
5+
6+
import argparse
7+
import sys
8+
import ipaddress
9+
import math
10+
11+
import asmap
12+
13+
def load_file(input_file):
14+
try:
15+
contents = input_file.read()
16+
except OSError as err:
17+
sys.exit(f"Input file '{input_file.name}' cannot be read: {err.strerror}.")
18+
try:
19+
bin_asmap = asmap.ASMap.from_binary(contents)
20+
except ValueError:
21+
bin_asmap = None
22+
txt_error = None
23+
entries = None
24+
try:
25+
txt_contents = str(contents, encoding="utf-8")
26+
except UnicodeError:
27+
txt_error = "invalid UTF-8"
28+
txt_contents = None
29+
if txt_contents is not None:
30+
entries = []
31+
for line in txt_contents.split("\n"):
32+
idx = line.find('#')
33+
if idx >= 0:
34+
line = line[:idx]
35+
line = line.lstrip(' ').rstrip(' \t\r\n')
36+
if len(line) == 0:
37+
continue
38+
fields = line.split(' ')
39+
if len(fields) != 2:
40+
txt_error = f"unparseable line '{line}'"
41+
entries = None
42+
break
43+
prefix, asn = fields
44+
if len(asn) <= 2 or asn[:2] != "AS" or any(c < '0' or c > '9' for c in asn[2:]):
45+
txt_error = f"invalid ASN '{asn}'"
46+
entries = None
47+
break
48+
try:
49+
net = ipaddress.ip_network(prefix)
50+
except ValueError:
51+
txt_error = f"invalid network '{prefix}'"
52+
entries = None
53+
break
54+
entries.append((asmap.net_to_prefix(net), int(asn[2:])))
55+
if entries is not None and bin_asmap is not None and len(contents) > 0:
56+
sys.exit(f"Input file '{input_file.name}' is ambiguous.")
57+
if entries is not None:
58+
state = asmap.ASMap()
59+
state.update_multi(entries)
60+
return state
61+
if bin_asmap is not None:
62+
return bin_asmap
63+
sys.exit(f"Input file '{input_file.name}' is neither a valid binary asmap file nor valid text input ({txt_error}).")
64+
65+
66+
def save_binary(output_file, state, fill):
67+
contents = state.to_binary(fill=fill)
68+
try:
69+
output_file.write(contents)
70+
output_file.close()
71+
except OSError as err:
72+
sys.exit(f"Output file '{output_file.name}' cannot be written to: {err.strerror}.")
73+
74+
def save_text(output_file, state, fill, overlapping):
75+
for prefix, asn in state.to_entries(fill=fill, overlapping=overlapping):
76+
net = asmap.prefix_to_net(prefix)
77+
try:
78+
print(f"{net} AS{asn}", file=output_file)
79+
except OSError as err:
80+
sys.exit(f"Output file '{output_file.name}' cannot be written to: {err.strerror}.")
81+
try:
82+
output_file.close()
83+
except OSError as err:
84+
sys.exit(f"Output file '{output_file.name}' cannot be written to: {err.strerror}.")
85+
86+
def main():
87+
parser = argparse.ArgumentParser(description="Tool for performing various operations on textual and binary asmap files.")
88+
subparsers = parser.add_subparsers(title="valid subcommands", dest="subcommand")
89+
90+
parser_encode = subparsers.add_parser("encode", help="convert asmap data to binary format")
91+
parser_encode.add_argument('-f', '--fill', dest="fill", default=False, action="store_true",
92+
help="permit reassigning undefined network ranges arbitrarily to reduce size")
93+
parser_encode.add_argument('infile', nargs='?', type=argparse.FileType('rb'), default=sys.stdin.buffer,
94+
help="input asmap file (text or binary); default is stdin")
95+
parser_encode.add_argument('outfile', nargs='?', type=argparse.FileType('wb'), default=sys.stdout.buffer,
96+
help="output binary asmap file; default is stdout")
97+
98+
parser_decode = subparsers.add_parser("decode", help="convert asmap data to text format")
99+
parser_decode.add_argument('-f', '--fill', dest="fill", default=False, action="store_true",
100+
help="permit reassigning undefined network ranges arbitrarily to reduce length")
101+
parser_decode.add_argument('-n', '--nonoverlapping', dest="overlapping", default=True, action="store_false",
102+
help="output strictly non-overall ping network ranges (increases output size)")
103+
parser_decode.add_argument('infile', nargs='?', type=argparse.FileType('rb'), default=sys.stdin.buffer,
104+
help="input asmap file (text or binary); default is stdin")
105+
parser_decode.add_argument('outfile', nargs='?', type=argparse.FileType('w'), default=sys.stdout,
106+
help="output text file; default is stdout")
107+
108+
parser_diff = subparsers.add_parser("diff", help="compute the difference between two asmap files")
109+
parser_diff.add_argument('-i', '--ignore-unassigned', dest="ignore_unassigned", default=False, action="store_true",
110+
help="ignore unassigned ranges in the first input (useful when second input is filled)")
111+
parser_diff.add_argument('infile1', type=argparse.FileType('rb'),
112+
help="first file to compare (text or binary)")
113+
parser_diff.add_argument('infile2', type=argparse.FileType('rb'),
114+
help="second file to compare (text or binary)")
115+
116+
args = parser.parse_args()
117+
if args.subcommand is None:
118+
parser.print_help()
119+
elif args.subcommand == "encode":
120+
state = load_file(args.infile)
121+
save_binary(args.outfile, state, fill=args.fill)
122+
elif args.subcommand == "decode":
123+
state = load_file(args.infile)
124+
save_text(args.outfile, state, fill=args.fill, overlapping=args.overlapping)
125+
elif args.subcommand == "diff":
126+
state1 = load_file(args.infile1)
127+
state2 = load_file(args.infile2)
128+
ipv4_changed = 0
129+
ipv6_changed = 0
130+
for prefix, old_asn, new_asn in state1.diff(state2):
131+
if args.ignore_unassigned and old_asn == 0:
132+
continue
133+
net = asmap.prefix_to_net(prefix)
134+
if isinstance(net, ipaddress.IPv4Network):
135+
ipv4_changed += 1 << (32 - net.prefixlen)
136+
elif isinstance(net, ipaddress.IPv6Network):
137+
ipv6_changed += 1 << (128 - net.prefixlen)
138+
if new_asn == 0:
139+
print(f"# {net} was AS{old_asn}")
140+
elif old_asn == 0:
141+
print(f"{net} AS{new_asn} # was unassigned")
142+
else:
143+
print(f"{net} AS{new_asn} # was AS{old_asn}")
144+
ipv4_change_str = "" if ipv4_changed == 0 else f" (2^{math.log2(ipv4_changed):.2f})"
145+
ipv6_change_str = "" if ipv6_changed == 0 else f" (2^{math.log2(ipv6_changed):.2f})"
146+
147+
print(
148+
f"# {ipv4_changed}{ipv4_change_str} IPv4 addresses changed; "
149+
f"{ipv6_changed}{ipv6_change_str} IPv6 addresses changed"
150+
)
151+
else:
152+
parser.print_help()
153+
sys.exit("No command provided.")
154+
155+
if __name__ == '__main__':
156+
main()

contrib/seeds/asmap.py renamed to contrib/asmap/asmap.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -489,12 +489,14 @@ def candidate(ctx: Optional[int], arg1, arg2, func: Callable):
489489
if ctx not in ret or cand.size < ret[ctx].size:
490490
ret[ctx] = cand
491491

492-
for ctx in set(left) | set(right):
492+
union = set(left) | set(right)
493+
sorted_union = sorted(union, key=lambda x: (x is None, x))
494+
for ctx in sorted_union:
493495
candidate(ctx, left.get(ctx), right.get(ctx), _BinNode.make_branch)
494496
candidate(ctx, left.get(None), right.get(ctx), _BinNode.make_branch)
495497
candidate(ctx, left.get(ctx), right.get(None), _BinNode.make_branch)
496498
if not hole:
497-
for ctx in set(ret) - set([None]):
499+
for ctx in sorted(set(ret) - set([None])):
498500
candidate(None, ctx, ret[ctx], _BinNode.make_default)
499501
if None in ret:
500502
ret = {ctx:enc for ctx, enc in ret.items()

contrib/seeds/makeseeds.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
import argparse
1010
import collections
1111
import ipaddress
12+
from pathlib import Path
1213
import re
1314
import sys
1415
from typing import Union
1516

16-
from asmap import ASMap, net_to_prefix
17+
asmap_dir = Path(__file__).parent.parent / "asmap"
18+
sys.path.append(str(asmap_dir))
19+
from asmap import ASMap, net_to_prefix # noqa: E402
1720

1821
NSEEDS=512
1922

0 commit comments

Comments
 (0)