Anatomy of the Virtualizor BGP Hijack: Reading the Attack in Raw BGP Bytes
On August 31, 2026, Softaculous disclosed that a BGP hijack against their infrastructure two days earlier was not just a routing outage: during the diversion, a malicious Virtualizor update package was served to hosting providers, compromising hypervisors at the root level [1]. This post reconstructs the routing attack from primary data, the BGP UPDATE messages recorded by public route collectors, and follows the failure chain from RPKI origin validation, through certificate issuance, to an unsigned software-update channel.
Everything below is reproducible from public archives: RIPE RIS and Route Views store every message, and the tools we use to slice them (bgpkit-parser and BGPKIT Broker) are open source. A PDF version of this analysis is available: Full incident report (PDF).
The one-paragraph summary
Between 2026-08-28 20:57:30 UTC and 2026-08-30 ~06:10 UTC, AS62390
(NexonHost) announced 162.55.80.0/24, a slice of Hetzner’s
162.55.0.0/16 that hosts Softaculous’ update and billing servers,
through transit provider AS6204 (ZET.NET). The forged announcement was
more specific than Hetzner’s legitimate /16, so it won route selection
everywhere it reached. Crucially, the operator of AS62390 kept the real origin,
AS24940, at the end of the AS path, which made the hijacked route validate
cleanly against Hetzner’s RPKI ROA. Traffic for the update servers flowed
to the impostor, who used the diversion to pass Let’s Encrypt’s domain
validation and mint a legitimate TLS certificate for 26 Softaculous
domains, cloned the update endpoint, and served a backdoored update to
installations that happened to check during the window.
The hijack path
The path below reads observer → origin, the direction an MRT record stores. Every vantage that received the forged route saw some variant of:
<observer> <upstream...> 6204 62390 24940
Segment by segment, from the origin end:
24940: Hetzner, the real origin. The attacker kept it on the path as camouflage, so RPKI origin validation saw the authorized origin and passed the route.62390: NexonHost, the hijacking AS. It forged the announcement and inserted itself one hop before the origin.6204: ZET.NET, the transit provider that accepted and propagated the forged route to the rest of the Internet.<upstream...>: however many additional ASNs the route accumulated between AS6204 and the recording collector, different at every vantage.
The attack topology in one picture:
graph LR LEG["162.55.0.0/16 | RPKI ROA: AS24940, maxlen 24"] H["AS24940 Hetzner
legitimate /16"] -- "announces /16" --> W["The Internet
(longest prefix match)"] N["AS62390 NexonHost
hijacking AS / apparent source"] -- "announces 162.55.80.0/24
AS path: ... 6204 62390 24940" --> Z["AS6204 ZET.NET
transit that propagated the forged route"] Z -- "more specific wins" --> W W -- "diverted traffic" --> IMP["impostor server
at 162.55.80.8"] style N fill:#fee2e2,stroke:#dc2626 style Z fill:#ffedd5,stroke:#ea580c style IMP fill:#fee2e2,stroke:#dc2626 style H fill:#dcfce7,stroke:#16a34a
Two properties did all the work:
- More-specific beats origin authority. BGP route selection prefers the longer prefix regardless of who announces it. Hetzner’s /16 could not compete with a /24 covering the same space.
- The real origin stayed on the path. This is what separates this incident from a naive origin hijack, and it is why RPKI origin validation, deployed anywhere along the path, rejected nothing. More on that below.
Reading the first hijacked message, byte by byte
The first collector sighting of the forged route is a single MRT record from RIPE RIS rrc00, timestamped 2026-08-28 20:57:30 UTC. Here it is as a BGP4MP-wrapped UPDATE:
BGP4MP|1787950650|A|185.193.84.191|29504|162.55.80.0/24|29504 15935 6204 62390 24940|IGP|...
You can open this exact record, dissected field-by-field down to every byte, in wirescope, our Wireshark-style BGP message dissector. Click any tree row to pin the corresponding bytes. Three fields are worth your attention:
- AS_PATH (
4002 16 02 05 ...): an AS_SEQUENCE of five ASNs,29504 15935 6204 62390 24940. The last two ASNs are the entire attack: 62390 appeared immediately before the real origin; routing data alone cannot say who operated it. - NLRI (
18 a2 37 50): prefix length 24, then 162.55.80.0: the more-specific that wins longest-prefix match. - Communities:
6204:1000,6204:1002stamped inside AS6204, the transit that carried the forged route,15935:100 15935:103and31230:0 31230:2picked up along the way. Communities accumulate hop by hop, which is how a third party can later reconstruct which path the route took.
We extracted those 144 bytes verbatim with
bgpkit-parser’s RawMrtRecord::raw_bytes() API, so what you see in wirescope is the
unaltered wire image from the collector, not a reconstruction.
Timeline, verified against raw MRT where possible
Virtualizor’s disclosure includes a minute-by-minute table reconstructed from RIPEstat. We re-verified the key moments directly against raw update files from two independent collector projects; that is the level of evidence routing claims deserve. Every timestamp below is from the primary data. Throughout the post we separate three evidence classes: verified directly from MRT (routes, paths, propagation, withdrawals); verified from other public data (ROA, ASPA corpus); and reported by Virtualizor or operators (malicious package, impostor host behavior, malware IoCs).
| Moment (UTC) | What happened | Evidence |
|---|---|---|
| 08-28 20:57:30 | First forged /24 seen; 29504 15935 6204 62390 24940 at rrc00, same second at rrc01 via a different transit chain (6830 6204 62390 24940) | RIS rrc00/rrc01 updates.20260828.2055 |
| 08-28 21:01:04 | First Route Views sighting, 57866 5511 6204 62390 24940 | route-views2 updates.20260828.2100 |
| 08-29 08:45-08:50 | Hetzner countermeasures: begins announcing the /24 itself (e.g. 852 3320 3320 3320 24940); diversion drops to zero in minutes | RIS rrc00 updates.20260829.0850 |
| 08-29 ~14:30 | Hetzner withdraws the countermeasure /24; Internet falls back to the /16 | RIPEstat bgp-state table [1] |
| 08-29 19:55:25 | Second wave begins, same path signature, minutes after the clean /24 disappeared | RIS rrc00 updates.20260829.1955 |
| 08-30 05:45:16 | Last forged-route record at rrc00; zero 62390 sightings after 05:50 | RIS rrc00 updates.20260830.0545 |
The two-wave structure is worth pausing on. The forged route disappeared as Hetzner’s countermeasure took effect, then reappeared several hours after the clean /24 was withdrawn. The routing data alone cannot establish whether that timing was deliberate, but the second wave reopened the same diversion path.
| Phase | Window (UTC) | Duration | Diversion |
|---|---|---|---|
| Wave 1 | 08-28 20:57 → 08-29 08:50 | ~12 h | high, flapping (peak ~72% of RIS peers) |
| Suppressed | 08-29 08:50 → 19:55 | ~11 h | ~zero (clean /24, then /16 fallback) |
| Wave 2 | 08-29 19:55 → 08-30 05:45 | ~10 h | high again, same path signature |
Through the waves, the route was exceptionally unstable: roughly 10,600 withdrawals inside the ~33-hour window (per the disclosure’s event reconstruction), with transit route dampening repeatedly suppressing it. Peak diversion reached ~72% of RIS’s 368 collector peers (median 266), and every single RIS peer carried the forged route at some point. Those percentages are routing-visibility measures, not traffic volume; the flapping means any individual network’s exposure was intermittent.
How long the hijack “worked” end to end
The question that matters for a supply-chain attack is not when routes flapped but how long the impostor could actually serve forged content with a valid certificate. Summing only the phases where the forged route was propagated:
- Aggregate attack-wave duration: ~22 of the 33 hours, adding wave 1 (about 12 hours) and wave 2 (about 10 hours). Actual exposure at any individual network was intermittent because of the severe flapping and dampening quantified below.
- ROV exposure: the entire window. No RPKI mechanism would have rejected the route; it validated as Valid from the first announce to the last withdrawal. The only effective countermeasure was Hetzner answering the forged /24 with its own legitimate /24, and it took ~12 hours from onset to take effect (08-28 20:57 → 08-29 08:50), reportedly after escalation by the victim rather than proactive detection.
- Certificate validity outlived the hijack. The fraudulently issued certificate was technically valid the moment Let’s Encrypt’s domain validation completed over the diverted path. Once issued, it kept working for any on-path position the impostor could regain, which is exactly what wave 2 was. Revocation of the misissued cert is the only thing that truly retires it; the disclosure does not state a revocation time, so treat the certificate as live through at least the end of the incident window (08-30 06:10), a ~31-hour worst case from onset; the attack waves themselves span ~22 of those hours, with intermittent exposure as quantified above.
The gap between “route visible” and “operator response” is the real story: public collectors saw the forged route at 20:57:30, the effective countermeasure arrived roughly 12 hours later, and throughout that interval the forged announcement remained RPKI Valid: ROV provided no basis for rejecting it.
What the entire public collector fleet saw
The disclosure’s diversion numbers cover RIPE RIS’s 368 peers. We wanted the whole public picture, so we streamed every update file from both public collector projects (RIPE RIS and Route Views, 79 collectors, 22,613 files, 94,117 route events) through a Rust replay built on bgpkit-broker and bgpkit-parser, classifying every collector peer’s best route for the /24 minute by minute: hijacked (path through AS62390) or on Hetzner’s clean /24. The result:

Reading the curve: at onset the forged route reaches 584 peers within its first minute and peaks at 615 peers simultaneously diverted (about three hours into wave 1). When Hetzner’s countermeasure /24 appears at 08:50, the mix flips almost instantly to green (clean /24 held by 787+ peers), and the forged route stays near-zero through the quiet window. Wave 2 snaps ~609 peers back onto the hijacked path within minutes of the clean /24 disappearing, and the final withdrawal at 05:45 collapses it in one step. One detail the aggregate view exposes that per-collector checks miss: a single collector peer still held the hijacked route for about an hour after everything else had converged (the thin red tail past 05:45), a classic BGP zombie. Raw minute-bucket data: hijack-visibility-buckets.tsv.
The gaps in the curve are real routing states, not missing data. In wave 1 the forged route flapped so violently that transit route dampening repeatedly suppressed it to a single peer for hours at a stretch (the disclosure’s table shows the same near-zero rows). And the blue-shaded mid-day window is genuinely empty: after Hetzner withdrew its countermeasure /24 around 14:30 and before the impostor returned at 19:55, no /24 route existed anywhere in the public view; every network had fallen back to the legitimate /16. Minute-by-minute visibility therefore swung between “hijack everywhere”, “suppressed to almost nothing”, and “no /24 at all”; an operator watching only reachability of the /24 could reasonably have believed the attack was over three separate times.
Where the flapping came from
We also traced the flapping itself. For every wave-1 announcement we extracted the AS immediately upstream of AS6204 (the carrier that relayed the forged route into the transit) and measured the announce/withdraw rhythm per collector peer. Two facts pin the churn to the source rather than to any transit re-computing:
- All carriers cycled together. Sixteen distinct upstream ASes relayed the route into AS6204 (AS5511 with 2,370 observations, AS3257 with 1,065, plus fourteen more). Every one of them shows the same announce/withdraw cycling. The synchronized churn across otherwise independent carrier paths localizes the instability to the common segment at or below AS6204, most plausibly AS62390’s export or the AS62390-AS6204 session, rather than to independent recomputation farther upstream.
- Peers around the world alternate almost one-to-one. Collectors in Singapore, Korea, Brazil, South Africa, the UAE, and the US all saw announce/withdraw ratios near one, a complete up/down cycle roughly every nine minutes on average. That pattern is what a single source switching on and off, with every network faithfully repeating it, looks like; independent local routing decisions would not stay in sync.
The mechanism, end to end: the source export (on the AS62390-AS6204 segment) cycled, each drop triggered a withdrawal avalanche (the largest was 49 withdrawals in a single second, 75 seconds after onset), each recovery re-propagated the route, and the accumulated flapping pushed route-dampening networks along the path into suppressing it for 30-60 minutes at a stretch. Sloppy infrastructure or deliberate on/off hijacking: routing data cannot distinguish intent, but the synchronized churn localizes the instability to the AS62390-AS6204 segment, and the intermittent result was still enough to capture update checks and a certificate validation.
The counts here are collector-peer route states (all RIS + Route Views peers that carried a /24 route), a broader denominator than the disclosure’s 368 RIS peers, so absolute numbers differ; the shape and the instants match the verified timeline exactly.
Why RPKI didn’t stop it
The attacker did not forge the origin; the forged information was the relationship immediately before the origin. A ROA answers “is AS24940 allowed to originate this prefix?” and the answer was genuinely yes. The attack hinged on a different question: “is AS62390 allowed to claim connectivity to AS24940 this way?” Origin validation has no way to ask it. Everything in this section follows from that distinction.
Hetzner’s ROA for the covering block is:
162.55.0.0/16 | origin AS24940 | maxLength 24
(Verifiable at any RPKI viewer; we confirmed the ROA held continuously through the incident window via BGPKIT’s ROA history service, which stores daily snapshots back to 2011.)
Now validate the hijacked route the way a ROV-enforcing router would:
flowchart TD A["Route: 162.55.80.0/24
AS path ... 6204 62390 24940"] --> B{"Covering ROA?"} B -- "yes: 162.55.0.0/16" --> C{"Origin AS in ROA?
announced origin = 24940" } C -- "yes" --> D{"Prefix length ≤ maxLength?
24 ≤ 24" } D -- "yes" --> E["RPKI: VALID ✅
route accepted, hijack invisible to ROV"] style E fill:#fef9c3,stroke:#ca8a04
Because the forged path appended the real origin instead of replacing it, the announcement’s origin ASN is exactly what the ROA authorizes, at exactly the maximum allowed length. ROV classified the forged route as Valid; even universal ROV deployment, by itself, would not have rejected this announcement.
A further wrinkle: had Hetzner signed the ROA for exactly the /16 without
maxLength 24 (the minimal-ROA practice recommended by
RFC 9319), the hijacked
/24 would have been RPKI Invalid and dropped by every validating
network; the over-broad maxLength is what kept it Valid. Bryton Herdes
made exactly this point when the disclosure
hit the community.
This forged-origin pattern (real origin retained, unauthorized AS inserted before it) is exactly what RFC 9319 warns about (see also the BGPKIT use-case note on ASPA path verification), and what ASPA (draft-ietf-sidrops-aspa-verification) is designed to catch.
What would have stopped the route from propagating? Unexciting, decades-old prefix filtering at the transit: a customer-prefix filter at AS6204 that restricted AS62390 to prefixes it was authorized to originate would have rejected this announcement. Its acceptance and re-export turned a single suspicious announcement into a global diversion. Customer prefix filtering at transit remains the control that would have confined this incident to a single hop.
How ASPA would have changed the outcome
ROV authorizes origins; it says nothing about who may sit immediately upstream of the origin, which is exactly the degree of freedom this attack used. ASPA closes that gap, and its effect here is unusually concrete, so we replayed the incident’s actual paths through the ASPA verification algorithm (draft §5.2-5.6) under three ASPA states. The “today” state is not hypothetical: we checked the live global ASPA corpus (rpki-client export, 2,820 objects): no AS on these paths has published one, and no object anywhere lists AS62390 as a provider.
| ASPA state | Hijack /24 | Legit /16 | Hetzner countermeasure /24 |
|---|---|---|---|
| Today (no ASPA on path) | Unknown | Unknown | Unknown |
| ASPA(24940) published | Invalid | Unknown | Unknown / Valid† |
| ASPA(24940) + ASPA(6204) | Invalid | Unknown | Unknown / Valid† |
† upstream (customer/peer-received) / downstream (provider-received) verdict; the recommended mitigation policy rejects Invalid only.
Why one object suffices: Hetzner’s ASPA lists its real providers, which
by definition exclude AS62390, so the hop 24940 → 62390 becomes
provably “Not Provider+”. The up-ramp from the origin collapses to zero
hops and the algorithm halts at Invalid, in both the upstream and
downstream variants. No cooperation from AS62390, AS6204, or
anyone else on the path is needed.
Why there is no collateral damage: Hetzner’s legitimate routes stay Unknown (not Invalid) because their un-attested hops are “No Attestation” (a missing assertion) rather than a contradicted one. Under the reject-Invalid-only policy, publishing the ASPA costs Hetzner nothing and takes the entire forged-upstream attack class off the table for its space.
You can watch this counterfactual play out live: Cloudflare Radar’s ASPA validation tool checks any AS path against the ASPA records currently published, hop by hop. Paste the hijack path (or follow this pre-filled link) and the verdict today is Unknown. The day Hetzner publishes its ASPA, the same link turns Invalid for everyone who opens it.
And the deployment asymmetry is the punchline: defenders opt in one object at a time, each hardening their own origin regardless of what anyone else does. Had ASPA(24940) existed and verification been enabled at even the major transits that carried the forged route (AS3257, AS5511, AS6830, AS3320 were all on observed paths), the 20:57:30 announcement could have been classified Invalid and filtered at every validating network, without requiring AS62390 or AS6204 to publish ASPA of their own: no sustained diversion, no certificate minted through the hijacked path, no poisoned update channel.
Why TLS didn’t stop it
With traffic for files.virtualizor.com (162.55.80.8 during the
incident) flowing to the impostor, the certificate layer was subverted
in a familiar way: the impostor requested certificates from Let’s Encrypt, and
the CA’s automated domain-control validation, an HTTP request to the
hijacked address, completed over the diverted path. The result was a
technically valid cert covering virtualizor.com,
api.virtualizor.com, files.virtualizor.com, softaculous.com, and 22
more names (full list in the disclosure’s Appendix A [1]).
No client saw a warning. The impostor host even carried reverse DNS
server.softaculous.com.
For the update channel itself, the final blow was that Virtualizor’s update client did not cryptographically verify package signatures: a valid-looking HTTPS response from a lookalike server was sufficient to deliver the payload. The fake update advertised itself as 3.2.9.8; affected hosts reported the “update succeeded” while remaining on 3.2.9.7; the version mismatch in the logs is itself a detectable IoC [3].
The full chain, then:
flowchart LR BGP["BGP hijack
RPKI-valid /24"] --> CERT["Let's Encrypt DCV
via hijacked path"] CERT --> TLS["valid cert ×26 domains
no client warnings"] BGP --> CLONE["clone of update server
at 162.55.80.8"] TLS --> PKG["backdoored update pkg
(unsigned client accepts it)"] CLONE --> PKG PKG --> ROOT["hypervisor root
systemd persistence + SSH key"] style BGP fill:#fee2e2,stroke:#dc2626 style ROOT fill:#fee2e2,stroke:#dc2626
Known indicators from the merged YARA rules
[2]:
a java-jre-update.service systemd unit, a /tmp/widdow.jar-class Java
payload (SHA-256 b81a4e1f...a7870c7), a planted ed25519 SSH key, and
C2 endpoints including 31.77.220.138:2025. Virtualizor could not
produce a victim list (malicious responses never touched their logs), so
every installation that ran an update check inside the window must be
treated as potentially affected.
Detecting this class of hijack with BGPKIT tooling
The forged route was visible in public BGP data at 20:57:30 on the second it was first announced. The supply-chain consequences were only community-confirmed 2.5 days later. The information was there; the workflow was missing. So here is the workflow, concretely, with the tools we used on this very incident.
Stage 1: notice, a new more-specific inside a valid covering block
The detection predicate for this incident is one sentence: a /24 that
had never been announced before appeared inside an RPKI-valid /16, and
the AS right before the origin was one nobody had ever seen adjacent to
that origin. RPKI origin validation checks out (that is the attack),
so the discriminator is the penultimate hop, not the origin. We
verified the “never announced before” half directly: zero 162.55.80.0/24
records in rrc00’s update file immediately preceding onset. Daily RIB
snapshots give you the baseline side for free.
Continuously running that predicate is a pipeline job, not a CLI query:
discover each new update file as it lands
(BGPKIT Broker new_file event
stream), stream it through bgpkit-parser, and diff what
you see against
a per-prefix baseline built from the previous day’s RIBs. That is exactly
the architecture of our spectrum pipeline: Broker for discovery,
parser for the streaming pass, a small state machine for the
“new more-specific + unseen penultimate hop” rule.
Once the alert fires, monocle is the tool for the investigation that follows. It resolves the exact MRT files (via the Broker) and slices them by prefix, origin, or AS path:
# baseline: has this /24 ever existed? (monocle over yesterday's RIBs)
monocle search -t <yesterday> -D rib -p "162.55.80.0/24"
# live: new more-specific appears; who is the origin-adjacent AS?
monocle search -t 1787950650 -d 60s -c rrc00 -p "162.55.80.0/24" \
-a "62390" -f timestamp,as_path,peer_asn
# 1787950650 | 162.55.80.0/24 | 29504 15935 6204 62390 24940 | 29504
# 1787950651 | 162.55.80.0/24 | 59919 9002 3257 6204 62390 24940 | 59919
# ...
Stage 2: corroborate, independent vantage, multiple transit chains
One collector can be a policy artifact. Two projects, different
continents, different transit chains (RIS saw it via 15935 6204 and
6830 6204; Route Views via 5511 6204) make the finding robust. The
The BGPKIT Broker search
API is how each of these commands
finds its exact input file: one API call, timestamp + collector + data
type, no guessing archive URLs.
# route-views2 saw the same forged origin 3.5 minutes later
curl -sO https://archive.routeviews.org/route-views2/bgpdata/2026.08/UPDATES/updates.20260828.2100.bz2
monocle parse updates.20260828.2100.bz2 -p "162.55.80.0/24"
Stage 3: attribute the trust failure, ROA history
The claim “RPKI-valid by design” is checkable against a ROA history service, not just today’s snapshot: the ROA must have been in force at incident time. The BGPKIT ROA history API answers this with one call:
curl "https://api.bgpkit.com/v3/roas/search?prefix=162.55.0.0/16"
# -> 162.55.0.0/16 | AS24940 | max_len 24 | date_ranges through 2026-08-31
Stage 4: pin the evidence, bytes, not summaries
When an incident escalates, “we saw a weird path” is not evidence. The
144-byte MRT record is. The
bgpkit-parser RawMrtRecord API exists exactly
for this: parse the archive, match the record, keep the unmodified wire
bytes.
for raw in parser.into_raw_record_iter() {
let wire = raw.raw_bytes(); // freeze the evidence
let rec = raw.parse()?; // then interpret it
// match: prefix == 162.55.80.0/24 && path contains 62390
}
The frozen bytes feed the wirescope deep link above, so anyone can audit the claim against the raw message, no BGPKIT infrastructure required.
Stage 5: report it
The difference between a claim and evidence is a source list: which MRT file, which timestamp, which exact bytes. Every command in stages 1-4 is cheap to re-run; write them down as the report’s replay recipe, and anyone can audit the conclusions against public archives.
The uncomfortable summary of this incident for the “just deploy RPKI” era: origin validation is necessary, but it could not detect this attack. The hijack lived in the gap RPKI explicitly does not cover, namely who is allowed to be your upstream. That is the gap ASPA targets, while AS62390’s own transit accepting an out-of-scope prefix is the gap filtering discipline targets. And once routing folds, only update-client package signing separates a routing incident from a supply-chain compromise. In this incident it was absent, so nothing stood at the last line of defense.
Reproducing this analysis
Every number and timestamp above comes from public data you can pull yourself:
Locate the exact MRT files with the
Broker search API
(GET /v3/broker/search with ts_start, ts_end, project,
collector_id, data_type=updates), then slice the hijack out of the
update stream:
monocle search -t 1787950650 -d 86400 -c rrc00 -p "162.55.80.0/24" -a "62390"
The 144-byte record dissected above is also built into wirescope as a
sample (“UPDATE · hijack, real MRT record, RIS rrc00”), and the deep link
above reproduces it from the URL alone. The extraction used the
bgpkit-parser RawMrtRecord API: parse the archive,
match the record, dump raw_bytes(), paste into wirescope. A fuller case-study writeup,
including the multi-collector timeline verification and ROA history, is
available on request.
Sources
- Virtualizor / Softaculous, Security Incident: BGP Hijacking, 2026-08-31. https://www.virtualizor.com/blog/security-incident-bgp-hijacking/
- Neo23x0/signature-base PR #417, YARA rules and IoCs for the Virtualizor compromise, 2026-08-31. https://github.com/Neo23x0/signature-base/pull/417
- LowEndTalk, URGENT: Virtualizor Compromised (31st AUG): operator reports from affected hosts. https://lowendtalk.com/discussion/220625/urgent-virtualizor-compromised-31st-aug/p1
- RIPE RIS raw MRT archives (rrc00, rrc01) and Route Views (route-views2), 2026-08-28 through 08-30.
- PeeringDB records for AS62390 (NEXONHOST) and AS6204 (ZET.NET), retrieved 2026-08-31.
- BGPKIT ROA history API, daily ROA snapshots 2011-present.
- rpki-client global ASPA export (NTT), 2026-09-01 build. https://rpki.gin.ntt.net/api/export.json
- Bryton Herdes on the minimal-ROA counterfactual for this incident. https://x.com/next_hopself/status/2094553871087509799