Skip to content

Instantly share code, notes, and snippets.

@dmarzzz
Created October 13, 2025 06:32
Show Gist options
  • Select an option

  • Save dmarzzz/e3acfeb5a73fcb0f44ae38d8ecd04dab to your computer and use it in GitHub Desktop.

Select an option

Save dmarzzz/e3acfeb5a73fcb0f44ae38d8ecd04dab to your computer and use it in GitHub Desktop.
Traceroute AS parsing
# Run "traceroute -n google.com | tee traceroute_output.txt" in your terminal
# Paste your traceroute/tracert output between the triple quotes and run this cell.
TR_TEXT = """
<PASTE YOUR TRACEROUTE OUTPUT HERE>
"""
import re, requests, ipaddress, sys
def extract_hops(text: str):
"""
Works for macOS/Linux `traceroute` and Windows `tracert`.
Returns an ordered list of hop IPs (one per hop), skipping '*' lines.
"""
hops = []
for line in text.splitlines():
line = line.strip()
if not line:
continue
# Grab the first IPv4 address on the line (good proxy for the hop's responding router)
m = re.search(r'(\d{1,3}(?:\.\d{1,3}){3})', line)
if not m:
continue
ip = m.group(1)
# Skip placeholder '*' hops
if ip == '*':
continue
# Keep order by hop (don't dedupe globally yet; traceroute already has 1 IP per hop line typically)
hops.append(ip)
# Some traceroute variants may echo the same IP on multiple adjacent lines; collapse consecutive duplicates.
deduped = []
for ip in hops:
if not deduped or deduped[-1] != ip:
deduped.append(ip)
return deduped
def ip_class(ip):
try:
addr = ipaddress.ip_address(ip)
if addr.is_private: return "Private"
if addr.is_loopback: return "Loopback"
if addr.is_link_local: return "Link-local"
if addr.is_reserved: return "Reserved"
if addr.is_multicast: return "Multicast"
if ip.startswith("100.64.") or ip.startswith("100.65.") or ip.startswith("100."): # CGNAT (approx)
# ipaddress has .is_private True for CGNAT; keep label explicit for readability:
return "CGNAT/Private"
return "Public"
except ValueError:
return "Unknown"
def ripe_asn_lookup(ip):
"""
Query RIPEstat for announcing ASN and prefix for an IP.
Returns dict with asn, holder, prefix, rir (or minimal info if not found).
"""
try:
r = requests.get(
"https://stat.ripe.net/data/prefix-overview/data.json",
params={"resource": ip},
timeout=10,
)
r.raise_for_status()
d = r.json().get("data", {})
asns = d.get("asns", []) or []
if asns:
a = asns[0]
return {
"asn": f"AS{a.get('asn')}",
"holder": a.get("holder") or "",
"prefix": d.get("prefix") or "",
"rir": d.get("rir") or "",
}
# Fallback: still return prefix if present
return {
"asn": "",
"holder": "",
"prefix": d.get("prefix") or "",
"rir": d.get("rir") or "",
}
except Exception:
return {"asn": "", "holder": "", "prefix": "", "rir": ""}
# Process
hops = extract_hops(TR_TEXT)
# Build rows with ASN mapping (cache lookups)
cache = {}
rows = []
for i, ip in enumerate(hops, start=1):
kind = ip_class(ip)
if kind != "Public":
rows.append({"hop": i, "ip": ip, "type": kind, "asn": "", "holder": "", "prefix": "", "rir": ""})
continue
if ip not in cache:
cache[ip] = ripe_asn_lookup(ip)
info = cache[ip]
rows.append({
"hop": i, "ip": ip, "type": kind,
"asn": info.get("asn",""), "holder": info.get("holder",""),
"prefix": info.get("prefix",""), "rir": info.get("rir","")
})
# Pretty print as Markdown table
def to_markdown_table(rows):
headers = ["Hop", "IP", "Type", "ASN", "Holder", "Prefix", "RIR"]
out = ["| " + " | ".join(headers) + " |", "|" + "|".join(["---"]*len(headers)) + "|"]
for r in rows:
out.append("| " + " | ".join(str(r.get(k.lower(), r.get(k, ""))) for k in ["hop","ip","type","asn","holder","prefix","rir"]) + " |")
return "\n".join(out)
md = to_markdown_table(rows)
print(md)
# If pandas is available, also show a nice table
try:
import pandas as pd
df = pd.DataFrame(rows, columns=["hop","ip","type","asn","holder","prefix","rir"])
from IPython.display import display
display(df)
except Exception as e:
print("\n(pandas not available or display failed; Markdown table printed above.)")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment