BirdLGGo: support optionally printing trace hop PTR

This commit is contained in:
James Lu 2021-10-29 00:20:32 -07:00
parent ebcbf5a92d
commit f6932586fa
4 changed files with 101 additions and 72 deletions

View File

@ -47,11 +47,15 @@ def configure(advanced):
conf.registerPlugin('BirdLGGo', True) conf.registerPlugin('BirdLGGo', True)
class _traceHopFormat(registry.OnlySomeStrings):
validStrings = ('ip', 'ptr', 'both')
BirdLGGo = conf.registerPlugin('BirdLGGo') BirdLGGo = conf.registerPlugin('BirdLGGo')
conf.registerChannelValue(BirdLGGo, 'lgServer', conf.registerChannelValue(BirdLGGo, 'lgServer',
registry.String("", _("""Looking glass server to query, including the scheme (http(s)://)"""))) registry.String("", _("""Looking glass server to query, including the scheme (http(s)://).""")))
conf.registerChannelValue(BirdLGGo, 'nodes', conf.registerChannelValue(BirdLGGo, 'nodes',
registry.SpaceSeparatedListOfStrings([], _("""List of nodes to query (space separated list)"""))) registry.SpaceSeparatedListOfStrings([], _("""List of nodes to query (space separated list).""")))
conf.registerChannelValue(BirdLGGo, 'traceHopFormat',
_traceHopFormat(_traceHopFormat.validStrings[0], _("""Whether the plugin should show IPs, reverse DNS (PTR records), or both for each hop in a traceroute.""")))
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=120: # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=120:

View File

@ -5,9 +5,14 @@ import subprocess
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List from typing import List
@dataclass
class TraceHop:
ip: str
ptr: str
@dataclass @dataclass
class TraceResult: class TraceResult:
ips: List[str] hops: List[TraceHop]
latency: str = field(default=None) latency: str = field(default=None)
notes: List[str] = field(default_factory=list) notes: List[str] = field(default_factory=list)
@ -16,14 +21,14 @@ class TraceParseError(ValueError):
# bird-lg-go enables DNS lookups and sends one query by default, but we should be a bit more flexible with what we accept # bird-lg-go enables DNS lookups and sends one query by default, but we should be a bit more flexible with what we accept
# This will grab IPs from whichever format is used, and report the first latency / error code of the last line if there are multiple queries # This will grab IPs from whichever format is used, and report the first latency / error code of the last line if there are multiple queries
_TRACEROUTE_RE = re.compile(r'\s*\d+\s+(?P<line>(?:[^() ]+ \((?P<IPdns>[0-9a-fA-F.:]+)\)|(?P<IPbare>[0-9a-fA-F.:]+)).*? (?P<latency>[0-9.]+ ms( \![A-Za-z0-9]+)?)|\*)') _TRACEROUTE_RE = re.compile(r'\s*\d+\s+(?P<line>(?:(?P<ptr>[^() ]+) \((?P<IPdns>[0-9a-fA-F.:]+)\)|(?P<IPbare>[0-9a-fA-F.:]+)).*? (?P<latency>[0-9.]+ ms( \![A-Za-z0-9]+)?)|\*)')
def parse_traceroute(text): def parse_traceroute(text):
lines = text.strip().splitlines() lines = text.strip().splitlines()
if len(lines) < 2 or not lines[1].lstrip().startswith("1"): if len(lines) < 2 or not lines[1].lstrip().startswith("1"):
# Assume error condition if 2nd line doesn't start with "1" (first hop) # Assume error condition if 2nd line doesn't start with "1" (first hop)
raise TraceParseError(' '.join(lines) or "traceroute returned empty output") raise TraceParseError(' '.join(lines) or "traceroute returned empty output")
else: else:
ips = [] hops = []
notes = [] notes = []
latency = None latency = None
for line in lines[1:]: for line in lines[1:]:
@ -33,14 +38,16 @@ def parse_traceroute(text):
if not m: if not m:
notes.append(line) notes.append(line)
continue continue
ips.append(m.group("IPdns") or m.group("IPbare") or m.group("line")) ip = m.group("IPdns") or m.group("IPbare") or m.group("line")
ptr = m.group("ptr") or ip
hops.append(TraceHop(ip, ptr))
latency = m.group("latency") latency = m.group("latency")
# bird-lg-go specific truncation # bird-lg-go specific truncation
if "hops not responding" in ''.join(notes): if "hops not responding" in ''.join(notes):
latency = None latency = None
return TraceResult(ips, latency, notes) return TraceResult(hops, latency, notes)
if __name__ == '__main__': if __name__ == '__main__':
proc = subprocess.run(['traceroute', *sys.argv[1:]], encoding='utf-8', stdout=subprocess.PIPE) proc = subprocess.run(['traceroute', *sys.argv[1:]], encoding='utf-8', stdout=subprocess.PIPE)

View File

@ -1,5 +1,9 @@
from pprint import pprint
import unittest import unittest
from parsetrace import parse_traceroute, TraceResult from parsetrace import parse_traceroute, TraceResult, TraceHop
def _bareip(ip: str):
return TraceHop(ip=ip, ptr=ip)
class ParseTraceTestCase(unittest.TestCase): class ParseTraceTestCase(unittest.TestCase):
maxDiff = None maxDiff = None
@ -14,14 +18,14 @@ class ParseTraceTestCase(unittest.TestCase):
6 2001:4860:0:1::5737 (2001:4860:0:1::5737) 0.870 ms 6 2001:4860:0:1::5737 (2001:4860:0:1::5737) 0.870 ms
7 ord37s33-in-x0e.1e100.net (2607:f8b0:4009:809::200e) 46.306 ms 7 ord37s33-in-x0e.1e100.net (2607:f8b0:4009:809::200e) 46.306 ms
""" """
self.assertEqual(TraceResult(ips=[ self.assertEqual(TraceResult(hops=[
"2605:4840:3::1", _bareip("2605:4840:3::1"),
"2604:6600:2700:11::1", _bareip("2604:6600:2700:11::1"),
"2001:418:0:5000::ec0", TraceHop("2001:418:0:5000::ec0", "ce-0-7-0-2.r07.chcgil09.us.bb.gin.ntt.net"),
"2001:504:0:4:0:1:5169:1", TraceHop("2001:504:0:4:0:1:5169:1", "eqix-ch-200g-1.google.com"),
"*", _bareip("*"),
"2001:4860:0:1::5737", _bareip("2001:4860:0:1::5737"),
"2607:f8b0:4009:809::200e" TraceHop("2607:f8b0:4009:809::200e", "ord37s33-in-x0e.1e100.net")
], latency="46.306 ms"), parse_traceroute(s)) ], latency="46.306 ms"), parse_traceroute(s))
def testTracerouteMultiQuery(self): def testTracerouteMultiQuery(self):
@ -32,12 +36,12 @@ class ParseTraceTestCase(unittest.TestCase):
4 cloudflare.slix.net (149.112.13.27) 9.057 ms 9.060 ms 9.504 ms 4 cloudflare.slix.net (149.112.13.27) 9.057 ms 9.060 ms 9.504 ms
5 one.one.one.one (1.1.1.1) 8.567 ms 8.688 ms 8.764 ms 5 one.one.one.one (1.1.1.1) 8.567 ms 8.688 ms 8.764 ms
""" """
self.assertEqual(TraceResult(ips=[ self.assertEqual(TraceResult(hops=[
'205.185.112.1', _bareip('205.185.112.1'),
'172.18.0.29', _bareip('172.18.0.29'),
'184.104.194.81', TraceHop('184.104.194.81', '100ge3-2.core1.slc1.he.net'),
'149.112.13.27', TraceHop('149.112.13.27', 'cloudflare.slix.net'),
'1.1.1.1' TraceHop('1.1.1.1', 'one.one.one.one')
], latency="8.567 ms"), parse_traceroute(s)) ], latency="8.567 ms"), parse_traceroute(s))
def testTracerouteMultiQueryDifferentPaths(self): def testTracerouteMultiQueryDifferentPaths(self):
@ -56,20 +60,21 @@ class ParseTraceTestCase(unittest.TestCase):
12 142.250.210.209 (142.250.210.209) 3.812 ms 108.170.252.18 (108.170.252.18) 3.763 ms 142.250.210.209 (142.250.210.209) 3.685 ms 12 142.250.210.209 (142.250.210.209) 3.812 ms 108.170.252.18 (108.170.252.18) 3.763 ms 142.250.210.209 (142.250.210.209) 3.685 ms
13 fra16s51-in-f14.1e100.net (142.250.185.174) 3.895 ms 3.871 ms 3.945 ms 13 fra16s51-in-f14.1e100.net (142.250.185.174) 3.895 ms 3.871 ms 3.945 ms
""" """
self.assertEqual(TraceResult(ips=[ self.assertEqual(TraceResult(hops=[
'172.31.1.1', TraceHop(ip='172.31.1.1', ptr='172.31.1.1'),
'49.12.142.82', TraceHop(ip='49.12.142.82', ptr='17476.your-cloud.host'),
'*', TraceHop(ip='*', ptr='*'),
'78.47.3.237', TraceHop(ip='78.47.3.237', ptr='static.237.3.47.78.clients.your-server.de'),
'85.10.239.169', TraceHop(ip='85.10.239.169', ptr='static.85.10.239.169.clients.your-server.de'),
'85.10.228.85', TraceHop(ip='85.10.228.85', ptr='static.85-10-228-85.clients.your-server.de'),
'213.239.245.250', TraceHop(ip='213.239.245.250', ptr='core1.fra.hetzner.com'),
'213.239.224.217', TraceHop(ip='213.239.224.217', ptr='core8.fra.hetzner.com'),
'142.250.169.172', TraceHop(ip='142.250.169.172', ptr='142.250.169.172'),
'*', TraceHop(ip='*', ptr='*'),
'142.250.226.148', TraceHop(ip='142.250.226.148', ptr='142.250.226.148'),
'142.250.210.209', TraceHop(ip='142.250.210.209', ptr='142.250.210.209'),
'142.250.185.174'], latency="3.895 ms"), parse_traceroute(s)) TraceHop(ip='142.250.185.174', ptr='fra16s51-in-f14.1e100.net')],
latency='3.895 ms'), parse_traceroute(s))
def testTracerouteTimedOut(self): def testTracerouteTimedOut(self):
s = """ s = """
@ -105,15 +110,16 @@ traceroute to azure.microsoft.com (13.107.42.16), 30 hops max, 60 byte packets
29 * * * 29 * * *
30 * * * 30 * * *
""" """
self.assertEqual(TraceResult(ips=[ self.assertEqual(TraceResult(hops=[
'172.31.1.1', TraceHop(ip='172.31.1.1', ptr='172.31.1.1'),
'49.12.142.82', TraceHop(ip='49.12.142.82', ptr='17476.your-cloud.host'),
'*', TraceHop(ip='*', ptr='*'),
'78.47.3.237', TraceHop(ip='78.47.3.237', ptr='static.237.3.47.78.clients.your-server.de'),
'85.10.248.221', TraceHop(ip='85.10.248.221', ptr='static.85.10.248.221.clients.your-server.de'),
'213.239.208.221', TraceHop(ip='213.239.208.221', ptr='core11.nbg1.hetzner.com'),
'213.239.224.238', TraceHop(ip='213.239.224.238', ptr='core5.fra.hetzner.com'),
'104.44.37.193'] + ["*"]*(30-9+1)), parse_traceroute(s)) TraceHop(ip='104.44.37.193', ptr='ae72-0.fra-96cbe-1b.ntwk.msn.net')] +
[_bareip("*")]*(30-9+1)), parse_traceroute(s))
def testTracerouteTimedOutTruncated(self): def testTracerouteTimedOutTruncated(self):
@ -131,14 +137,15 @@ traceroute to azure.microsoft.com (13.107.42.16), 30 hops max, 60 byte packets
23 hops not responding. 23 hops not responding.
""" """
self.assertEqual(TraceResult(ips=[ self.assertEqual(TraceResult(hops=[
'172.31.1.1', TraceHop(ip='172.31.1.1', ptr='172.31.1.1'),
'49.12.142.82', TraceHop(ip='49.12.142.82', ptr='17476.your-cloud.host'),
'78.47.3.237', TraceHop(ip='78.47.3.237', ptr='static.237.3.47.78.clients.your-server.de'),
'85.10.248.217', TraceHop(ip='85.10.248.217', ptr='static.85.10.248.217.clients.your-server.de'),
'85.10.250.209', TraceHop(ip='85.10.250.209', ptr='core11.nbg1.hetzner.com'),
'213.239.245.254', TraceHop(ip='213.239.245.254', ptr='core1.fra.hetzner.com'),
'104.44.197.103'], notes=["23 hops not responding."]), parse_traceroute(s)) TraceHop(ip='104.44.197.103', ptr='hetzner.fra-96cbe-1a.ntwk.msn.net')],
notes=["23 hops not responding."]), parse_traceroute(s))
def testTracerouteNoDNS(self): def testTracerouteNoDNS(self):
s = """ s = """
@ -152,15 +159,15 @@ traceroute to irc.hackint.dn42 (172.20.66.67), 30 hops max, 60 byte packets
7 172.23.96.1 165.631 ms 182.696 ms 182.695 ms 7 172.23.96.1 165.631 ms 182.696 ms 182.695 ms
8 172.20.66.67 182.695 ms 167.117 ms 187.094 ms""" 8 172.20.66.67 182.695 ms 167.117 ms 187.094 ms"""
self.assertEqual(TraceResult(ips=[ self.assertEqual(TraceResult(hops=[
'172.20.229.114', _bareip('172.20.229.114'),
'172.20.229.113', _bareip('172.20.229.113'),
'172.20.229.123', _bareip('172.20.229.123'),
'*', _bareip('*'),
'*', _bareip('*'),
'172.20.129.169', _bareip('172.20.129.169'),
'172.23.96.1', _bareip('172.23.96.1'),
'172.20.66.67'], latency="182.695 ms"), parse_traceroute(s)) _bareip('172.20.66.67')], latency="182.695 ms"), parse_traceroute(s))
def testTracerouteNoDNSv6(self): def testTracerouteNoDNSv6(self):
s = """traceroute to map.dn42 (fd42:4242:2189:e9::1), 30 hops max, 80 byte packets s = """traceroute to map.dn42 (fd42:4242:2189:e9::1), 30 hops max, 80 byte packets
@ -170,19 +177,19 @@ traceroute to irc.hackint.dn42 (172.20.66.67), 30 hops max, 60 byte packets
4 fd42:4242:2189:e9::1 208.400 ms 208.398 ms 208.543 ms 4 fd42:4242:2189:e9::1 208.400 ms 208.398 ms 208.543 ms
""" """
self.assertEqual(TraceResult(ips=[ self.assertEqual(TraceResult(hops=[
'fd86:bad:11b7:34::1', _bareip('fd86:bad:11b7:34::1'),
'fd86:bad:11b7:22::1', _bareip('fd86:bad:11b7:22::1'),
'fd42:4242:2189:ef::1', _bareip('fd42:4242:2189:ef::1'),
'fd42:4242:2189:e9::1'], latency="208.400 ms"), parse_traceroute(s)) _bareip('fd42:4242:2189:e9::1')], latency="208.400 ms"), parse_traceroute(s))
def testTracerouteError(self): def testTracerouteError(self):
s = """traceroute to 192.168.123.123 (192.168.123.123), 30 hops max, 60 byte packets s = """traceroute to 192.168.123.123 (192.168.123.123), 30 hops max, 60 byte packets
1 192.168.123.1 (192.168.123.1) 3079.329 ms !H 3079.304 ms !H 3079.299 ms !H 1 192.168.123.1 (192.168.123.1) 3079.329 ms !H 3079.304 ms !H 3079.299 ms !H
""" """
self.assertEqual(TraceResult(ips=[ self.assertEqual(TraceResult(hops=[
'192.168.123.1'], latency="3079.329 ms !H"), parse_traceroute(s)) _bareip('192.168.123.1')], latency="3079.329 ms !H"), parse_traceroute(s))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -65,6 +65,7 @@ class BirdLGGo(callbacks.Plugin):
else: else:
return resp["result"] return resp["result"]
@wrap(['something']) @wrap(['something'])
def traceroute(self, irc, msg, args, target): def traceroute(self, irc, msg, args, target):
"""<target> """<target>
@ -78,18 +79,28 @@ class BirdLGGo(callbacks.Plugin):
"args": target "args": target
} }
results = self.lg_post_request(irc, msg, query) results = self.lg_post_request(irc, msg, query)
hop_display_mode = self.registryValue("traceHopFormat", network=irc.network, channel=msg.channel)
def _format_tracehop(hop):
if hop_display_mode == 'ip' or hop.ip == hop.ptr:
return hop.ip
elif hop_display_mode == 'ptr':
return hop.ptr
elif hop_display_mode == 'both':
return f'{hop.ptr}[{hop.ip}]'
irc.error("Unknown traceHopFormat setting %r" % hop_display_mode)
for result in results: for result in results:
parsed_result = parsetrace.parse_traceroute(result["data"]) parsed_result = parsetrace.parse_traceroute(result["data"])
server = result["server"] server = result["server"]
ips = " ".join(parsed_result.ips) hops = " ".join(map(_format_tracehop, parsed_result.hops))
latency = parsed_result.latency or "(timed out)" latency = parsed_result.latency or "(timed out)"
notes = "" notes = ""
if parsed_result.notes: if parsed_result.notes:
notes = "- " + ", ".join(parsed_result.notes) notes = "- " + ", ".join(parsed_result.notes)
irc.reply(f"{server} -> {target}: {latency} | {ips} {notes}") irc.reply(f"{server} -> {target}: {latency} | {hops} {notes}")
@wrap(['something']) @wrap(['something'])
def showroute(self, irc, msg, args, target): def showroute(self, irc, msg, args, target):