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)
class _traceHopFormat(registry.OnlySomeStrings):
validStrings = ('ip', 'ptr', 'both')
BirdLGGo = conf.registerPlugin('BirdLGGo')
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',
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:

View File

@ -5,9 +5,14 @@ import subprocess
from dataclasses import dataclass, field
from typing import List
@dataclass
class TraceHop:
ip: str
ptr: str
@dataclass
class TraceResult:
ips: List[str]
hops: List[TraceHop]
latency: str = field(default=None)
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
# 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):
lines = text.strip().splitlines()
if len(lines) < 2 or not lines[1].lstrip().startswith("1"):
# Assume error condition if 2nd line doesn't start with "1" (first hop)
raise TraceParseError(' '.join(lines) or "traceroute returned empty output")
else:
ips = []
hops = []
notes = []
latency = None
for line in lines[1:]:
@ -33,14 +38,16 @@ def parse_traceroute(text):
if not m:
notes.append(line)
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")
# bird-lg-go specific truncation
if "hops not responding" in ''.join(notes):
latency = None
return TraceResult(ips, latency, notes)
return TraceResult(hops, latency, notes)
if __name__ == '__main__':
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
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):
maxDiff = None
@ -14,14 +18,14 @@ class ParseTraceTestCase(unittest.TestCase):
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
"""
self.assertEqual(TraceResult(ips=[
"2605:4840:3::1",
"2604:6600:2700:11::1",
"2001:418:0:5000::ec0",
"2001:504:0:4:0:1:5169:1",
"*",
"2001:4860:0:1::5737",
"2607:f8b0:4009:809::200e"
self.assertEqual(TraceResult(hops=[
_bareip("2605:4840:3::1"),
_bareip("2604:6600:2700:11::1"),
TraceHop("2001:418:0:5000::ec0", "ce-0-7-0-2.r07.chcgil09.us.bb.gin.ntt.net"),
TraceHop("2001:504:0:4:0:1:5169:1", "eqix-ch-200g-1.google.com"),
_bareip("*"),
_bareip("2001:4860:0:1::5737"),
TraceHop("2607:f8b0:4009:809::200e", "ord37s33-in-x0e.1e100.net")
], latency="46.306 ms"), parse_traceroute(s))
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
5 one.one.one.one (1.1.1.1) 8.567 ms 8.688 ms 8.764 ms
"""
self.assertEqual(TraceResult(ips=[
'205.185.112.1',
'172.18.0.29',
'184.104.194.81',
'149.112.13.27',
'1.1.1.1'
self.assertEqual(TraceResult(hops=[
_bareip('205.185.112.1'),
_bareip('172.18.0.29'),
TraceHop('184.104.194.81', '100ge3-2.core1.slc1.he.net'),
TraceHop('149.112.13.27', 'cloudflare.slix.net'),
TraceHop('1.1.1.1', 'one.one.one.one')
], latency="8.567 ms"), parse_traceroute(s))
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
13 fra16s51-in-f14.1e100.net (142.250.185.174) 3.895 ms 3.871 ms 3.945 ms
"""
self.assertEqual(TraceResult(ips=[
'172.31.1.1',
'49.12.142.82',
'*',
'78.47.3.237',
'85.10.239.169',
'85.10.228.85',
'213.239.245.250',
'213.239.224.217',
'142.250.169.172',
'*',
'142.250.226.148',
'142.250.210.209',
'142.250.185.174'], latency="3.895 ms"), parse_traceroute(s))
self.assertEqual(TraceResult(hops=[
TraceHop(ip='172.31.1.1', ptr='172.31.1.1'),
TraceHop(ip='49.12.142.82', ptr='17476.your-cloud.host'),
TraceHop(ip='*', ptr='*'),
TraceHop(ip='78.47.3.237', ptr='static.237.3.47.78.clients.your-server.de'),
TraceHop(ip='85.10.239.169', ptr='static.85.10.239.169.clients.your-server.de'),
TraceHop(ip='85.10.228.85', ptr='static.85-10-228-85.clients.your-server.de'),
TraceHop(ip='213.239.245.250', ptr='core1.fra.hetzner.com'),
TraceHop(ip='213.239.224.217', ptr='core8.fra.hetzner.com'),
TraceHop(ip='142.250.169.172', ptr='142.250.169.172'),
TraceHop(ip='*', ptr='*'),
TraceHop(ip='142.250.226.148', ptr='142.250.226.148'),
TraceHop(ip='142.250.210.209', ptr='142.250.210.209'),
TraceHop(ip='142.250.185.174', ptr='fra16s51-in-f14.1e100.net')],
latency='3.895 ms'), parse_traceroute(s))
def testTracerouteTimedOut(self):
s = """
@ -105,15 +110,16 @@ traceroute to azure.microsoft.com (13.107.42.16), 30 hops max, 60 byte packets
29 * * *
30 * * *
"""
self.assertEqual(TraceResult(ips=[
'172.31.1.1',
'49.12.142.82',
'*',
'78.47.3.237',
'85.10.248.221',
'213.239.208.221',
'213.239.224.238',
'104.44.37.193'] + ["*"]*(30-9+1)), parse_traceroute(s))
self.assertEqual(TraceResult(hops=[
TraceHop(ip='172.31.1.1', ptr='172.31.1.1'),
TraceHop(ip='49.12.142.82', ptr='17476.your-cloud.host'),
TraceHop(ip='*', ptr='*'),
TraceHop(ip='78.47.3.237', ptr='static.237.3.47.78.clients.your-server.de'),
TraceHop(ip='85.10.248.221', ptr='static.85.10.248.221.clients.your-server.de'),
TraceHop(ip='213.239.208.221', ptr='core11.nbg1.hetzner.com'),
TraceHop(ip='213.239.224.238', ptr='core5.fra.hetzner.com'),
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):
@ -131,14 +137,15 @@ traceroute to azure.microsoft.com (13.107.42.16), 30 hops max, 60 byte packets
23 hops not responding.
"""
self.assertEqual(TraceResult(ips=[
'172.31.1.1',
'49.12.142.82',
'78.47.3.237',
'85.10.248.217',
'85.10.250.209',
'213.239.245.254',
'104.44.197.103'], notes=["23 hops not responding."]), parse_traceroute(s))
self.assertEqual(TraceResult(hops=[
TraceHop(ip='172.31.1.1', ptr='172.31.1.1'),
TraceHop(ip='49.12.142.82', ptr='17476.your-cloud.host'),
TraceHop(ip='78.47.3.237', ptr='static.237.3.47.78.clients.your-server.de'),
TraceHop(ip='85.10.248.217', ptr='static.85.10.248.217.clients.your-server.de'),
TraceHop(ip='85.10.250.209', ptr='core11.nbg1.hetzner.com'),
TraceHop(ip='213.239.245.254', ptr='core1.fra.hetzner.com'),
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):
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
8 172.20.66.67 182.695 ms 167.117 ms 187.094 ms"""
self.assertEqual(TraceResult(ips=[
'172.20.229.114',
'172.20.229.113',
'172.20.229.123',
'*',
'*',
'172.20.129.169',
'172.23.96.1',
'172.20.66.67'], latency="182.695 ms"), parse_traceroute(s))
self.assertEqual(TraceResult(hops=[
_bareip('172.20.229.114'),
_bareip('172.20.229.113'),
_bareip('172.20.229.123'),
_bareip('*'),
_bareip('*'),
_bareip('172.20.129.169'),
_bareip('172.23.96.1'),
_bareip('172.20.66.67')], latency="182.695 ms"), parse_traceroute(s))
def testTracerouteNoDNSv6(self):
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
"""
self.assertEqual(TraceResult(ips=[
'fd86:bad:11b7:34::1',
'fd86:bad:11b7:22::1',
'fd42:4242:2189:ef::1',
'fd42:4242:2189:e9::1'], latency="208.400 ms"), parse_traceroute(s))
self.assertEqual(TraceResult(hops=[
_bareip('fd86:bad:11b7:34::1'),
_bareip('fd86:bad:11b7:22::1'),
_bareip('fd42:4242:2189:ef::1'),
_bareip('fd42:4242:2189:e9::1')], latency="208.400 ms"), parse_traceroute(s))
def testTracerouteError(self):
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
"""
self.assertEqual(TraceResult(ips=[
'192.168.123.1'], latency="3079.329 ms !H"), parse_traceroute(s))
self.assertEqual(TraceResult(hops=[
_bareip('192.168.123.1')], latency="3079.329 ms !H"), parse_traceroute(s))
if __name__ == '__main__':
unittest.main()

View File

@ -65,6 +65,7 @@ class BirdLGGo(callbacks.Plugin):
else:
return resp["result"]
@wrap(['something'])
def traceroute(self, irc, msg, args, target):
"""<target>
@ -78,18 +79,28 @@ class BirdLGGo(callbacks.Plugin):
"args": target
}
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:
parsed_result = parsetrace.parse_traceroute(result["data"])
server = result["server"]
ips = " ".join(parsed_result.ips)
hops = " ".join(map(_format_tracehop, parsed_result.hops))
latency = parsed_result.latency or "(timed out)"
notes = ""
if 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'])
def showroute(self, irc, msg, args, target):