DEV Community

Cover image for The ASN Pivot Playbook: Routes, Upstreams, Downstreams
ABDULLAH AFZAL
ABDULLAH AFZAL

Posted on

The ASN Pivot Playbook: Routes, Upstreams, Downstreams

An ASN pivot is the cheapest move in an IP investigation and the easiest one to over-read. One call turns a single address into a network, a list of announced prefixes, a transit chain, and a set of customer networks. None of that tells you who owns anything.

That gap is the whole problem. The data is easy to get and hard to weigh, so investigations into C2 infrastructure either ignore it or lean on it far past what it supports. This is the version I'd hand a new analyst: five pivots, in the order I'd run them, each with the false positive it hands you.

TL;DR

  • An ASN pivot maps one IP to the network announcing it, then walks outward through announced routes, downstream customers, upstream transit, and WHOIS.
  • downstreams and routes carry most of the signal. peers carries the least, and may not mean what the name suggests.
  • Upstream, downstream, and peer labels are not declared in BGP. They are inferred from observed AS paths, so treat them as a good guess rather than ground truth.
  • Routing adjacency alone is low-confidence evidence, always. It tells you where to look next, not who did anything.
  • False positive risk depends almost entirely on the AS type. Shared cloud and residential ISP networks produce near-worthless clustering; small dedicated networks produce useful clustering.
  • Everything below runs on one paid endpoint call plus local set math, in curl and Python.

The short version: use routing data to generate leads and non-routing evidence to confirm them. An analyst who can say what a pivot does not prove is more useful than one who can run more pivots.

The five pivots, ranked by what they prove

Run them in this order. Each one costs a single lookup, and you can pull all of the data in one request and slice it locally rather than making five calls.

# Pivot What you get What it proves Confidence
1 IP to origin ASN AS number, org, type, RIR, allocation date This address is routed by this network High
2 ASN to announced routes Every IPv4 and IPv6 prefix observed as originated by the AS The address space currently announced with this AS as origin High
3 ASN to downstreams Customer networks routing through this AS The data infers these networks as customers of this AS Medium
4 ASN to upstreams Transit providers carrying this AS Who to escalate to, and how well connected the AS is Medium
5 ASN to WHOIS Registrant org, abuse contacts, handles Who registered the number, which is not always who uses it Low to medium

Nothing in that table is attribution. Pivot 1 is close to a fact. Everything below it is an inference with a shrinking amount of evidence behind it, which is why the order matters more than the coverage.

What upstreams, downstreams, and peers actually mean

Three relationships between autonomous systems, and the money decides which is which.

Upstreams are transit providers. You pay them, they carry your traffic to the rest of the internet and announce your prefixes onward. A network with multiple upstreams is multi-homed, which provides routing redundancy. The number of upstreams alone does not determine whether an AS is a stub.

Downstreams are customers. They route through you to reach everyone else, and you announce their prefixes on their behalf. Networks that provide transit have downstream customers; a stub AS does not provide transit between other ASes.

Peers sit level with you. Two networks exchange traffic directly, usually settlement-free, usually at an internet exchange. In the conventional valley-free routing model, peer-learned routes are generally exported to customers rather than to other peers or providers. A typical path therefore goes up through providers, may cross a peer link, and then goes down through customers. Real-world routing policies can have exceptions, so treat valley-free behavior as a useful model rather than a protocol guarantee.

Here's the part that changes how much you should trust any of it. BGP does not carry a field that says "this neighbour is my provider." The protocol announces reachability, not commercial terms, and the commercial terms are confidential. In datasets that infer AS relationships from observed BGP paths, upstream, downstream, and peer labels are algorithmic classifications rather than declarations carried in BGP itself. CAIDA, whose relationship dataset underpins a large share of this tooling, says as much directly in the paper describing their AS relationship inference algorithm: the business relationships are confidential, and the algorithm infers them from BGP paths.

So the labels are good. They are not ground truth, and an investigation that treats them as ground truth is building on sand.

Why peers may already contain your upstreams and downstreams

Worth checking before you write set logic against these arrays.

In the documented full response for AS12, every one of the 8 upstreams and both of the downstreams also appears in the peers array. peers also contains AS286 twice, once with country NL and once with US. On that response, peers behaves as "every adjacency we observed," not "settlement-free peers only."

I could only confirm this on one AS, so treat it as a property to check rather than a rule. Either way the defensive version costs two lines: deduplicate on AS number across the union, and treat the remaining peers entries after subtracting known upstreams and downstreams as peer candidates rather than proven settlement-free peers. Code that assumes the three sets are disjoint will double-count adjacencies and inflate whatever score sits on top.

Pivot 1: IP to origin ASN

Start here, always. An IP on its own is a poor unit of analysis, because bulletproof hosting operators rotate them deliberately. The Australian Cyber Security Centre's advisory on bulletproof hosting describes the technique plainly: these providers frequently change the internet-facing identifiers tied to a customer, including assigned IP addresses and domain names, which makes it harder to link an incident to whoever was using the address at the time. The AS number churns far more slowly than the address does.

curl -sS --max-time 3 \
  "https://api.ipgeolocation.io/v3/asn?apiKey=$IPGEO_API_KEY&ip=49.12.0.0"
Enter fullscreen mode Exit fullscreen mode
{
  "ip": "49.12.0.0",
  "asn": {
    "as_number": "AS24940",
    "organization": "Hetzner Online GmbH",
    "country": "DE",
    "type": "HOSTING",
    "domain": "hetzner.com",
    "date_allocated": "2002-06-03",
    "asn_name": "HETZNER-AS",
    "allocation_status": "ASSIGNED",
    "num_of_ipv4_routes": "84",
    "num_of_ipv6_routes": "6",
    "rir": "RIPE"
  }
}
Enter fullscreen mode Exit fullscreen mode

To be explicit, since this is an article about investigations: Hetzner is a legitimate hosting company and appears here because it is a well-documented example of a HOSTING-type AS, not because of anything else. That distinction matters throughout, and the false-positive table later on is mostly about not making that mistake.

Two field quirks that will bite you. as_number carries the AS prefix, so it is a string, not an integer. num_of_ipv4_routes and num_of_ipv6_routes are also strings despite being counts, so cast before comparing. Full field reference is in the ASN API documentation.

import os
import requests

API_KEY = os.environ.get("IPGEO_API_KEY")
BASE_URL = "https://api.ipgeolocation.io/v3/asn"


def resolve_origin(ip_address):
    """Map an IP to the AS announcing it. Returns None on any failure."""
    if not API_KEY:
        raise RuntimeError("IPGEO_API_KEY is not set")

    try:
        resp = requests.get(
            BASE_URL,
            params={"apiKey": API_KEY, "ip": ip_address},
            timeout=(2.0, 5.0),
        )
        # 423 means bogon or private. That is an input problem, not an outage.
        if resp.status_code == 423:
            return None
        resp.raise_for_status()
    except requests.exceptions.RequestException as exc:
        # Enrichment is not a gate. Log and let the caller proceed without it.
        print(f"ASN lookup failed for {ip_address}: {exc}")
        return None

    asn = resp.json().get("asn") or {}
    return {
        "as_number": asn.get("as_number"),
        "organization": asn.get("organization"),
        "type": asn.get("type"),
        "rir": asn.get("rir"),
        # Counts come back as strings. Cast, and survive an empty value.
        "ipv4_routes": int(asn.get("num_of_ipv4_routes") or 0),
    }
Enter fullscreen mode Exit fullscreen mode

That returns None rather than raising, which is a deliberate choice. An ASN lookup is enrichment on a case note, so it should fail open. If you wire this into enforcement, choose fail-open or fail-closed explicitly based on the control's role, your risk tolerance, and your availability requirements.

Pivot 2: ASN to announced routes

The routes array is the address space currently observed as originated by the AS, and it is your enumeration surface. If your one indicator sits in a /24 inside a network announcing 84 prefixes, you now have a defined perimeter to scan, correlate against logs, or check for previously-seen addresses.

curl -sS --max-time 5 \
  "https://api.ipgeolocation.io/v3/asn?apiKey=$IPGEO_API_KEY&asn=AS12&include=routes"
Enter fullscreen mode Exit fullscreen mode

The gotcha is small and annoying: IPv4 and IPv6 prefixes arrive mixed in the same array, as plain CIDR strings. Split them before you do anything numeric.

import ipaddress


def split_routes(routes):
    """Separate a mixed v4/v6 prefix list. Skips anything unparseable."""
    v4, v6 = [], []
    for prefix in routes or []:
        try:
            net = ipaddress.ip_network(prefix, strict=False)
        except ValueError:
            continue  # Malformed entry, not worth failing the whole pivot over
        (v4 if net.version == 4 else v6).append(net)
    return v4, v6
Enter fullscreen mode Exit fullscreen mode

One caution before you scan everything you just enumerated. A prefix announced with the AS as origin does not make it interesting, and on a large host the announced space is mostly other people's servers.

Pivot 3: ASN to downstreams

This is the pivot that earns its place, and the one most write-ups skip.

The joint guidance CISA published with the NSA, FBI, DC3 and international partners in November 2025, Bulletproof Defense, makes the structural point: bulletproof hosting (BPH) operators increasingly resell stolen or leased infrastructure from legitimate hosting providers, data centres, ISPs and cloud providers, who may have no idea what they are carrying. If a reseller operates its own ASN and buys transit from another provider, it may appear as that provider's downstream. If it only leases IP addresses or servers inside the provider's network, no separate downstream ASN relationship may exist.

So when your indicator lands in a small or mid-sized AS, the customer list is where the interesting structure lives.

curl -sS --max-time 5 \
  "https://api.ipgeolocation.io/v3/asn?apiKey=$IPGEO_API_KEY&asn=25139&include=downstreams"
Enter fullscreen mode Exit fullscreen mode
{
  "asn": {
    "organization": "TVCABO Comunicacoes Multimedia, Lda",
    "country": "MZ",
    "downstreams": [
      { "as_number": "AS328162", "description": "Icolo Ltd", "country": "KE" },
      { "as_number": "AS10798", "description": "The Standard Bank of South Africa Proprietary Limited", "country": "ZA" },
      { "as_number": "AS37477", "description": "Mozabanco", "country": "MZ" },
      { "as_number": "AS329394", "description": "OneCloud LDA", "country": "MZ" }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Read that list the way an investigator should: a regional operator with a bank, a data centre company and a cloud provider as customers is a normal regional operator. Nothing here is suspicious. On a genuinely suspect network the shape is the signal, not any individual name. Many small downstreams registered in quick succession, opaque or recently created organisation names, and customers announcing far more space than the business behind the name would plausibly need.

A downstream relationship suggests a customer-provider relationship based on observed routing data. It does not prove a commercial agreement, common ownership, or shared operation. Plenty of resellers are exactly what they claim to be.

Pivot 4: ASN to upstreams

Upstreams answer two different questions, and it is worth being clear about which one you are asking.

The investigative question is how well connected the network is. A single upstream means the network depends on one inferred transit provider in this dataset. Several upstreams indicate multihoming and routing redundancy, but they do not tell you whether the operator is legitimate or well funded.

The operational question is who to talk to. The ACSC advisory makes the point that upstream providers may be unaware they are carrying downstream criminal infrastructure, and the CISA guidance is addressed largely to those providers. If you are filing an abuse report that has gone nowhere at the hosting layer, the transit provider one hop up is the next escalation, and it is often the one with both the standing and the incentive to act.

curl -sS --max-time 5 \
  "https://api.ipgeolocation.io/v3/asn?apiKey=$IPGEO_API_KEY&asn=AS12&include=upstreams"
Enter fullscreen mode Exit fullscreen mode

There is a ceiling on this. Once you climb into tier-1 territory the concept stops being informative, because those networks carry effectively everything and their presence says nothing about any particular customer. When your upstream list is a set of global carriers, you have reached the top of the useful part of the pivot.

Pivot 5: ASN WHOIS, and the registrant trap

include=whois_response returns the raw registry record: registrant organisation, admin and tech handles, abuse contacts, allocation dates.

The trap is in the name of the organization field, and it catches people constantly. That field is the entity registered with the RIR as holding the AS number. It is not necessarily the entity using the address space. Subleasing, resale, acquisitions that never got reflected in the registry, and shell registrations all break the link, and this is precisely the pattern the CISA guidance describes when it talks about resold infrastructure. If you have ever wondered why looking up who owns an IP address gives you a cloud provider's name when the actual tenant is a startup you have never heard of, this is why.

Useful things in the record anyway: the abuse handle and email for reporting, RegDate and Updated for age, the RDAP references, and the maintainer objects, which occasionally tie several ASNs to one operator more reliably than any routing relationship does. Field names differ by registry, so an ARIN record and a RIPE record will not parse the same way. Recent allocation plus sparse contact detail plus a large customer list is a shape worth a second look.

Pull it once, slice it locally

All five pivots come out of one request. The include parameter takes all five datasets and adds no extra credit cost over the single credit the lookup already costs, so making five separate calls is just five times the spend for the same data.

# Continues the module from Pivot 1: requests, API_KEY and BASE_URL are already set.


def fetch_asn_profile(as_number):
    """One call, everything. Returns the asn object or None."""
    try:
        resp = requests.get(
            BASE_URL,
            params={
                "apiKey": API_KEY,
                "asn": as_number,
                "include": "routes,peers,upstreams,downstreams,whois_response",
            },
            timeout=(2.0, 10.0),  # Larger payload, so a longer read timeout
        )
        resp.raise_for_status()
    except requests.exceptions.RequestException as exc:
        print(f"Profile lookup failed for {as_number}: {exc}")
        return None

    return resp.json().get("asn") or {}


def relationship_sets(asn):
    """Dedupe adjacencies and separate unclassified peer candidates."""
    def numbers(key):
        return {n.get("as_number") for n in (asn.get(key) or []) if n.get("as_number")}

    upstreams = numbers("upstreams")
    downstreams = numbers("downstreams")
    peer_candidates = numbers("peers") - upstreams - downstreams

    return {
        "upstreams": upstreams,
        "downstreams": downstreams,
        "peer_candidates": peer_candidates,
        "all_adjacent": upstreams | downstreams | peer_candidates,
    }
Enter fullscreen mode Exit fullscreen mode

Note that a lookup by AS number returns no top-level ip field, since there is no address to echo back. Do not assume it is there.

Cache relatively stable ASN metadata aggressively, but keep a timestamp. Organizations, allocations, and routing data can change. If route accuracy matters to your workflow, refresh routes more frequently than basic ASN metadata or fetch them per case.

What each signal actually proves

The table that should sit next to the pivot table in your head.

Signal Confidence on its own What raises it
Same IP observed across related events Medium to high Corroborating timing, service, account, or behavioral evidence
Same announced prefix Medium to high Matching services, banners, or TLS certificates on both hosts
Same origin ASN, small dedicated network Medium Consistent behaviour and timing across the hosts
Same origin ASN, large host or cloud Very low Effectively needs independent evidence to mean anything
Downstream relationship Low to medium Shared registrant, shared maintainer object, correlated registration dates
Upstream relationship Very low Almost nothing. Transit is a commercial arrangement, not an endorsement
Peer relationship Very low Same
Shared WHOIS registrant or maintainer Medium Corroborating DNS, certificate, or malware-family overlap

The pattern is consistent. Routing data is excellent at generating candidates and poor at confirming them. Certificates, passive DNS, service fingerprints and behavioural timing are what turn a candidate into a finding. Anyone who tells you an upstream relationship implicates a provider is selling something.

False positives by AS type

The type field is the single most useful thing for calibrating how much a shared ASN means. Same data, opposite conclusions.

type What a shared ASN means What to do
ISP Almost nothing. Residential networks mix real users with compromised devices at scale Never cluster on ASN alone. Score the host, not the network
HOSTING Depends entirely on size. Meaningless on a large provider, meaningful on a small one Check the routing footprint and other context first. Route count can help distinguish a broad network from a narrow one, but it is not a reliable measure of organization size by itself.
BUSINESS Potentially useful, but may still contain shared, managed, or resold infrastructure Confirm the operator and hosting model before clustering
EDUCATION Often shared infrastructure with many unrelated users and systems Avoid ASN-wide conclusions; investigate the individual host and context
GOVERNMENT Shared organizational infrastructure where ASN-wide attribution is especially risky Investigate narrowly and use appropriate escalation channels

The num_of_ipv4_routes count is a useful routing-footprint signal, which is the practical reason to bother casting that string. Do not treat it as a direct measure of organization size.

Stop rules

Over-pivoting is how a clean case turns into a pile of loosely related infrastructure nobody can act on. Stop when any of these is true.

  1. You have reached a tier-1 network. Upstream relationships stop carrying information at the top of the transit hierarchy.
  2. The AS type is ISP or a large HOSTING network. the shared-network signal is usually too weak on its own; further expansion just adds noise.
  3. You are two relationship hops out with no corroborating evidence. Adjacency to adjacency is not a chain of evidence.
  4. The candidate set is growing faster than your evidence. If each pivot adds twenty addresses and zero confirmations, the pivot is not working.
  5. You cannot state what the next pivot would change. If the answer does not alter a decision, do not make the call.

Write the stop reason into the case note. The next analyst needs to know you stopped deliberately, not that you ran out of time.

Wiring it into a case note

Attach the routing profile to the indicator, not the case. Store as_number, organization, type, rir and the route counts alongside the address with a timestamp, because routing changes and an undated profile is one you cannot reason about six months later.

Keep the confidence label with the data. A field that records asn_match_confidence: low next to a cluster of addresses is worth more than the cluster, because it stops the next person treating your leads as your findings.

One credit per lookup, cached for a day, is cheap enough that IP enrichment at SIEM ingest is usually the right call rather than looking things up by hand mid-investigation. Just make sure the enrichment step cannot take the pipeline down with it.

Run the two-ASN check on the peers behaviour against your own data before you build scoring on those arrays. And if a pivot gives you a lead you cannot confirm with something outside BGP, write it down as a lead and move on. The discipline is the deliverable here, not the API call.

Top comments (0)