chaowen.← Back to archive

Networking / routing security deep dive

BGP hijacks and RPKI: why a more-specific route wins until a router rejects it

BGP chooses paths by policy, and the first decisive rule is usually longest prefix match. That lets an unauthorized /24 attract traffic away from a legitimate /16. RPKI adds a separate authorization check: a signed Route Origin Authorization can tell a router that the route’s origin AS, prefix length, or both are not allowed.

Networking / routing security deep dive · BGP path selection, ROAs, VRPs, RPKI-RTR, FRR, failure modes, and a reproducible validator lab

2026-09-02

The concrete problem

Suppose a company legitimately serves 162.55.0.0/16. The Internet does not have one central router. Thousands of autonomous systems (ASes) exchange reachability announcements using the Border Gateway Protocol, or BGP. Each receiving router builds its own routing information base and chooses a best route for each destination prefix.

Now an unrelated network announces 162.55.80.0/24. That /24 is contained inside the legitimate /16, but it is more specific: it describes 256 addresses instead of 65,536. For a destination such as 162.55.80.42, ordinary forwarding prefers the /24 wherever that route exists. The attacker does not need to make the whole Internet believe that it owns the /16; it only needs a more-specific announcement to reach enough networks.

Legitimate announcement Unauthorized announcement AS24940 → 162.55.0.0/16 AS62390 → 162.55.80.0/24 (possibly through AS6204) Destination: 162.55.80.42 162.55.0.0/16 matches, prefix length 16 ─┐ 162.55.80.0/24 matches, prefix length 24 ─┼─► /24 wins by specificity │ attacker-controlled next hop

This is a routing decision, not a cryptographic decision. Classic BGP authenticates the transport session between neighbors, but it does not by itself prove that the origin AS is authorized to announce the address space. RPKI supplies that missing statement of authority.

A recent incident makes the boundary visible

Virtualizor’s official incident report describes a real event between August 28 and August 30, 2026. AS62390, through AS6204, announced 162.55.80.0/24, while the legitimate infrastructure normally announced the surrounding 162.55.0.0/16. The report says the unauthorized route was accepted by the networks that received it, and that traffic to affected Softaculous systems was diverted intermittently for about 33 hours.

The attack was more serious than packet loss. The diverted endpoint served a technically valid Let’s Encrypt certificate because the certificate authority’s HTTP-based domain-control validation also followed the hijacked route. Clients therefore did not necessarily see a TLS warning. A malicious Virtualizor update reached a small number of installations; because those responses never reached the vendor’s servers, the vendor could not reconstruct a definitive list of clients from its own logs.

The lesson: TLS authenticates the endpoint that completed certificate validation. It does not tell the client whether BGP delivered the connection to the network that the client intended. Routing authorization and application authentication are separate control planes.

RPKI could have helped, but only if the address holder had published an appropriate ROA and the relevant networks were enforcing invalid-route policy. A ROA for 162.55.0.0/16 with maxLength=16 would make the unauthorized /24 invalid even if its BGP path ended with the legitimate origin AS. A permissive maxLength=24, or no covering ROA at all, changes that result. The exact protection depends on the signed policy, not on the word “RPKI” appearing in a network diagram.

What BGP actually compares

BGP carries a route to a prefix, a next hop, and path attributes. The full decision process is implementation- and policy-dependent, but the forwarding lookup has a simple invariant: among installed routes, the route with the longest matching prefix wins. BGP best-path selection then chooses one path for a given prefix, often considering local preference, locally originated routes, AS-path length, origin type, MED, eBGP/iBGP status, and router ID.

RPKI does not replace this selection process. It annotates a received BGP route with an origin-validation state. An operator’s policy decides what that state does to local preference or acceptance.

Input routeLongest-prefix resultOrigin-validation question
162.55.0.0/16, origin AS24940Matches broad destinations in the /16.Does a covering ROA authorize AS24940 for /16?
162.55.80.0/24, origin AS62390Wins for addresses inside that /24 if accepted.Does a covering ROA authorize AS62390, with a max length that permits /24?
162.55.80.0/24, origin AS24940Still wins by specificity.Is /24 within the ROA’s allowed maxLength? A correct origin alone is not enough.

That last row is the common misconception. “The origin AS is right” and “the announced prefix length is authorized” are two different predicates. ROA maxLength is what prevents an authorized AS from turning a broad allocation into arbitrary more-specific routes.

ROAs become VRPs, then route states

A Route Origin Authorization (ROA) is a signed RPKI object that binds an address prefix to an origin AS and a maximum prefix length. A relying-party validator retrieves certificates and signed objects from the RPKI repository, checks the cryptographic chain, and reduces the result to a set of Validated ROA Payloads (VRPs):

VRP = (authorized_prefix, max_length, origin_as)

(162.55.0.0/16, 16, AS24940)
(203.0.113.0/24, 24, AS64500)

A router does not normally verify every X.509 object during every BGP UPDATE. A validator or cache does the expensive repository and signature work. The router consumes the compact VRP set over the RPKI-RTR protocol, then evaluates each route as it arrives or as the VRP set changes.

RPKI repository validator / cache router certificates + ROAs ──fetch/verify──► VRP set ──RPKI-RTR──► route policy signed objects (prefix, max, AS) │ ├─ VALID BGP UPDATE: /24, AS62390 ─────────────────────────────────────────┤ ├─ INVALID → reject / lower preference └─ NOT FOUND → local policy decides

For a BGP route R = (prefix P, origin AS A), RFC 6811’s essential logic is:

  1. Find VRPs whose authorized prefix contains P.
  2. The route is Valid if at least one matching VRP has origin_as = A and prefix_length(P) ≤ max_length.
  3. If covering VRPs exist but none authorize that origin and length, the route is Invalid.
  4. If no covering VRP exists, the route is NotFound.

“Invalid” and “NotFound” must not be conflated. Invalid means signed routing policy contradicts the route. NotFound means there is no signed policy covering it. A deployment that rejects Invalid but accepts NotFound still leaves every unprotected prefix exposed to ordinary BGP mistakes and hijacks.

The data and control flow in an implementation

The system has three clocks and three kinds of state: repository publication, validator refresh, and router BGP convergence. They are loosely synchronized. A new ROA may be correctly published while a cache still has the old VRP set, or a router may have the new VRP set while a route UPDATE is still converging.

1. Resource holder signs ROA prefix=162.55.0.0/16, maxLength=16, origin=AS24940 │ 2. RPKI repository publishes object and certificate chain │ 3. Routinator / rpki-client validates chain and emits VRPs │ 4. RTR session sends serial/refresh + IPv4/IPv6 Prefix PDUs │ 5. FRR / another router joins route + VRP state │ 6. Route-map or policy rejects Invalid before best-path installation

The RPKI-RTR protocol is intentionally small. A router asks for a full or incremental cache response, receives IPv4/IPv6 prefix records carrying the VRP fields, and gets an End of Data marker with serial and timing information. The router can then detect a stale or reset cache. The protocol transfers validated authorization data; it is not a BGP UPDATE channel.

Open-source implementation landmarks

There is a useful separation of concerns in open-source stacks. A relying-party validator implements RPKI repository retrieval, certificate validation, manifest/CRL processing, and VRP generation. A routing daemon maintains the BGP Adj-RIB-In and Loc-RIB, receives VRPs over RTR, classifies routes, and applies operator policy.

ComponentImportant implementation boundaryWhat to inspect when debugging
RoutinatorRelying-party validator and RTR server. It turns distributed signed objects into a validated VRP set.Repository refresh time, validation errors, serial number, VRP count, and whether the expected ROA is present.
rpki-clientSmall relying-party implementation that validates RPKI objects and can emit validated data for downstream use.Trust-anchor configuration, object expiry, rejected manifests, and output for the exact prefix.
FRR bgpd/bgp_rpki.cMaintains the router-side RPKI/RTR integration and updates route validation state as VRPs change.RTR session state, serial/reset handling, cache freshness, and route-state transitions.
FRR BGP/RPKI policyExposes validation state to route maps and lets operators reject or prefer routes based on Valid, Invalid, or NotFound.Whether the route map is applied on the inbound edge where the UPDATE enters, before it can become the best path.
RPKI-RTRDefines cache-to-router PDUs such as Serial Notify, Cache Response, Prefix, End of Data, and Cache Reset.Transport security, session resets, serial mismatch, and stale-data timers.

FRR’s route object has to retain more than a next hop: it needs the BGP path attributes used by best-path selection and a validation state derived from the current VRP table. The important operational detail is that a VRP update can invalidate an already learned route without a new BGP UPDATE. Implementations therefore need a re-evaluation path from “VRP changed” to “route policy and best path changed.”

What RPKI does not solve

ProblemWhy origin validation is insufficientAdditional control
Forged or replayed application contentA Valid route can still lead to a compromised origin server.Signed software updates, package transparency, end-to-end application authentication, and rollback.
Path manipulation after the originRPKI validates the origin AS and prefix length, not every AS in AS_PATH.Peer filtering, AS-path monitoring, and, where appropriate, BGPsec or private authenticated overlays.
Route leaks by an authorized originA correctly authorized route can still be exported to the wrong peers.Import/export policy, prefix limits, BGP communities, and leak detection.
No ROA existsThe route is NotFound, not Invalid.Publish accurate ROAs and decide an explicit local policy for NotFound.
Bad ROA publicationAn overly broad or incorrect ROA can make a legitimate route Invalid, creating an outage.Staged publication, pre-checks, alerting, and a documented emergency rollback.

RPKI is therefore an authorization layer, not a complete replacement for BGP security. It is strongest when combined with strict prefix filters and when update endpoints verify signed artifacts independently of the network path.

Failure modes and security implications

How to debug a suspected hijack

  1. Start with the destination. Resolve the exact IP and compare its covering routes. A /24 may be hijacked while the surrounding /16 looks healthy.
  2. Compare vantage points. Query multiple independent route collectors and your own upstreams. BGP is a distributed system; one router’s table is not “the Internet.”
  3. Inspect the AS path and prefix length. Record the first-seen time, origin AS, neighboring ASes, and whether the route is more specific than the legitimate announcement.
  4. Ask the validator for the exact VRP decision. Capture the covering ROA prefix, origin, maxLength, validator serial, and route state. Do not infer “Invalid” from an unfamiliar AS path alone.
  5. Check enforcement. On the receiving router, confirm that the UPDATE was classified Invalid and that an inbound route policy rejected it before best-path installation. A dashboard showing Invalid is not evidence of rejection.
  6. Check application evidence separately. Review certificate issuance, update-client logs, package signatures, credential use, and outbound connections. Routing telemetry cannot identify every client that received an attacker’s response.

Useful production metrics include the age of the newest successful validator refresh, VRP serial age, RTR session resets, counts of Valid/Invalid/NotFound routes, rejected Invalid updates, and more-specific announcements inside your own allocations. Alert on a new more-specific route and on a Valid-to-Invalid transition, not only on total BGP session loss.

Compact reproducible lab

Goal and prerequisites

This lab models the two decisions that are easy to confuse: longest-prefix forwarding and RPKI origin validation. It uses only Python’s standard library, so it does not create real BGP sessions or cryptographically validate an RPKI repository. That makes the semantic boundary visible without requiring FRR, a validator, or a lab network.

mkdir -p /tmp/bgp-rpki-lab
cd /tmp/bgp-rpki-lab
python3 --version

Run this exact program:

python3 - <<'PY'
from dataclasses import dataclass
from ipaddress import ip_address, ip_network

@dataclass(frozen=True)
class VRP:
    prefix: object
    max_length: int
    origin: int

@dataclass(frozen=True)
class Route:
    prefix: object
    origin: int
    name: str

def validation_state(route, vrps):
    covering = [v for v in vrps if route.prefix.subnet_of(v.prefix)]
    if not covering:
        return "NotFound"
    valid = any(route.origin == v.origin
                and route.prefix.prefixlen <= v.max_length
                for v in covering)
    return "Valid" if valid else "Invalid"

def best_route(destination, routes, vrps, reject_invalid=False):
    candidates = [r for r in routes if destination in r.prefix]
    states = {r.name: validation_state(r, vrps) for r in candidates}
    if reject_invalid:
        candidates = [r for r in candidates if states[r.name] != "Invalid"]
    winner = max(candidates, key=lambda r: r.prefix.prefixlen) if candidates else None
    return winner, states

destination = ip_address("162.55.80.42")
routes = [
    Route(ip_network("162.55.0.0/16"), 24940, "legitimate /16"),
    Route(ip_network("162.55.80.0/24"), 62390, "hijack /24"),
]
vrps = [VRP(ip_network("162.55.0.0/16"), 16, 24940)]

for reject in (False, True):
    winner, states = best_route(destination, routes, vrps, reject_invalid=reject)
    print("states:", states)
    print("reject-invalid:", reject, "winner:", winner.name if winner else None)
PY

Expected result:

states: {'legitimate /16': 'Valid', 'hijack /24': 'Invalid'}
reject-invalid: False winner: hijack /24
states: {'legitimate /16': 'Valid', 'hijack /24': 'Invalid'}
reject-invalid: True winner: legitimate /16

The first pass proves that validation does not automatically alter route selection. The /24 is Invalid, but it still wins when policy merely observes the state. The second pass models an inbound “reject Invalid” policy; the /24 disappears before longest-prefix selection, so traffic falls back to the legitimate /16.

Variation 1 — allow the /24 for the real origin: change the VRP line to vrps = [VRP(ip_network("162.55.0.0/16"), 24, 24940)], and change the hijack route’s origin from 62390 to 24940. Both routes become Valid, and the /24 still wins. A permissive maxLength authorizes more-specific announcements.

Variation 2 — remove the ROA: change vrps = [...] to vrps = []. Both routes become NotFound. The reject-Invalid policy no longer removes the hijack, demonstrating why “we reject Invalid” is not equivalent to “we reject every unauthorized route.”

To extend this lab into a protocol test, run FRR with a local Routinator instance and configure an RTR session. Keep the Python model beside that test: it is a small oracle for the expected Valid/Invalid/NotFound state and for the route-policy ordering.

The durable mental model

Think of BGP and RPKI as two layers with different jobs. BGP answers, “Which reachable path should I use for this destination?” RPKI answers, “Is this origin AS authorized to announce this prefix at this length?” The router must join the answers through explicit policy.

For a suspected hijack, ask in order: What exact prefix was announced? Is it more specific? What origin AS does the route claim? Which covering ROAs exist, and what maxLength do they permit? Did the router reject Invalid before best-path selection? That sequence explains both the outage mechanics and why a valid TLS certificate or a healthy BGP session is not enough.

Primary sources