# MEO Outage Day

September 15, 2026 — 18 min read
Tags: [bgp](https://tomascorreia.net/blog/tags/bgp/), [networking](https://tomascorreia.net/blog/tags/networking/), [ddos](https://tomascorreia.net/blog/tags/ddos/), [incident-analysis](https://tomascorreia.net/blog/tags/incident-analysis/)

Canonical URL: https://tomascorreia.net/blog/meo-outage-day/

---

On Monday evening, a big chunk of Portugal lost the internet.

Not "my Wi-Fi is acting up" lost. Properly lost. Around 17:30 the complaints started, and by 18:20 roughly **seven thousand people** had filed a report on Downdetector about MEO alone. Fixed broadband, mobile data, phone calls, TV apps, all wobbling at once.

Then the arguing started. MEO said "mass attack". Some network people on X said "no, it's a BGP leak, look at Cloudflare Radar". Other people said "it's probably just their infrastructure falling over again".

I spent an evening actually checking. Not reading takes, querying the measurement systems directly. This post is what I found, written for someone who has never heard of BGP, with every command I ran so you can reproduce it.

Spoiler: almost everyone was a little bit right and a little bit wrong.

---

## First, what even is BGP?

Here's the thing that surprises people: **the internet has no map.**

There's no central server that knows where `netflix.com` lives. Instead there are about 80,000 independent networks: your ISP, Google, a university, a bank. Each one is called an **Autonomous System**, or AS. Each has a number: MEO's residential network is `AS3243`, Cloudflare is `AS13335`, and so on.

These networks stay in touch by constantly gossiping to each other:

> "Hey, I can reach these addresses. Send me traffic for them."

That gossip protocol is **BGP**, Border Gateway Protocol. It was sketched out in 1989 on the backs of a few napkins, and it is still what holds the internet together.

The addresses they gossip about come in blocks called **prefixes**. A prefix looks like `194.65.0.0/16`. Think of it as a postal code range, that one covers 65,536 addresses and belongs to MEO.

```mermaid
flowchart LR
    A["AS3243<br/>MEO Residential<br/>'I own 194.65.0.0/16'"] -->|announces| B["AS8657<br/>MEO International"]
    B -->|passes it on| C["AS1299<br/>Arelion"]
    B -->|passes it on| D["AS6939<br/>Hurricane Electric"]
    C --> E["The rest of<br/>the internet"]
    D --> E
    E -.->|"traffic flows back<br/>along the same path"| A
```

So when you load a website, your packets follow a chain of these announcements. Each hop is one network trusting the previous one's claim.

**And that's the weak spot.** BGP has almost no built-in way to verify that a claim is true. If a network says "I can reach those addresses", its neighbours mostly just… believe it. That's how route leaks and hijacks happen, and it's why BGP incidents keep making the news.

Two more terms, then we're done with theory:

- **Transit provider**, a big network you pay to carry your traffic everywhere else. Arelion, Cogent, Hurricane Electric are examples.
- **Route leak**, when a network passes along routes it shouldn't have. Usually accidental, often embarrassing, occasionally catastrophic.

---

## How MEO is actually wired

Before we can talk about what broke, you need to see the shape of the thing.

MEO doesn't run one network, it runs several, each with its own AS number. Straight from RIPE's database:

```bash
$ for asn in 3243 8657 42863 15525; do
    curl -s "https://stat.ripe.net/data/as-overview/data.json?resource=AS$asn" \
    | jq -r '.data | "AS\(.resource)  \(.holder)"'
  done

AS3243   MEO-RESIDENCIAL MEO - SERVICOS DE COMUNICACOES E MULTIMEDIA S.A.
AS8657   MEO-INTERNACIONAL MEO - SERVICOS DE COMUNICACOES E MULTIMEDIA S.A.
AS42863  MEO-MOVEL MEO - SERVICOS DE COMUNICACOES E MULTIMEDIA S.A.
AS15525  MEO-EMPRESAS MEO - SERVICOS DE COMUNICACOES E MULTIMEDIA S.A.
```

Four networks: **homes, backbone, mobile, business.**

Now the interesting bit. I asked RIPE who sits *between each of them and the rest of the internet*:

```bash
$ curl -s "https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS3243" \
  | jq -r '.data.neighbours[] | select(.type=="left") | "\(.asn)  strength \(.power)"'

8657  strength 2714
```

That's it. One answer. Same story for mobile and business, every one of them reaches the world through **AS8657**.

```mermaid
flowchart LR
    H["🏠 Your house"] --> R["AS3243<br/>Residential<br/>17 prefixes"]
    M["📱 Your phone"] --> MO["AS42863<br/>Mobile<br/>10 prefixes"]
    O["🏢 Your office"] --> B["AS15525<br/>Business<br/>45 prefixes"]

    R --> I["AS8657<br/>MEO INTERNATIONAL<br/>only 4 prefixes"]
    MO --> I
    B --> I

    I --> T1["Arelion<br/>AS1299"]
    I --> T2["Cogent<br/>AS174"]
    I --> T3["Hurricane<br/>AS6939"]
    I --> T4["Sparkle<br/>AS6762"]

    T1 & T2 & T3 & T4 --> NET["🌍 Internet"]

    style I fill:#0c1a1c,stroke:#42d9c0,stroke-width:3px,color:#42d9c0
```

Look at that middle box. **AS8657 owns only four prefixes of its own:**

```bash
$ curl -s "https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS8657" \
  | jq -r '.data.prefixes[].prefix'

185.96.172.0/22
217.168.112.0/21
195.8.0.0/19
2001:15d8::/32
```

Four. Meanwhile residential has 17, mobile has 10, business has 45, and **all 72 of those reach the internet through the four-prefix network's connections.**

This confuses people, so: *owning address space* and *carrying traffic* are completely different jobs. A railway company might own four stations while running trains to a thousand destinations. AS8657 is the railway.

Which means if AS8657 has a bad day, everyone has a bad day. Hold that thought.

---

## The timeline

Here's the whole thing. Times are Portugal local (WEST). The attack windows are MEO's own published figures; the measurement rows come from independent systems.

```mermaid
gantt
    title MEO incident, 14-15 September 2026 (WEST)
    dateFormat YYYY-MM-DD HH:mm
    axisFormat %H:%M

    section Attack waves (per MEO)
    Wave 1                  :crit, 2026-09-14 00:45, 2026-09-14 02:55
    Wave 2, the big one    :crit, 2026-09-14 17:30, 2026-09-14 19:30
    Wave 3                  :crit, 2026-09-14 22:45, 2026-09-15 01:05

    section What I measured
    Ghana route leak visible   :active, 2026-09-14 01:13, 2026-09-14 17:37
    BGP churn climbing         :2026-09-14 17:20, 2026-09-14 19:40
    IODA national outage event :crit, 2026-09-14 17:30, 2026-09-14 18:40
    Deepest packet loss        :crit, 2026-09-14 18:00, 2026-09-14 18:20

    section Public reaction
    Complaints climbing        :2026-09-14 17:30, 2026-09-14 19:00
    Peak ~7200 reports         :milestone, 2026-09-14 18:25, 0m
    MEO first statement        :milestone, 2026-09-14 22:00, 0m
    MEO + ANACOM verdict       :milestone, 2026-09-15 16:00, 0m
```

Three separate attack waves. Most people only felt the middle one, because it landed right at dinner time.

---

## How I checked the outage was real

Anyone can tweet "the internet is down". Let's get receipts.

### Step 1, ask a system that pings Portugal all day

Georgia Tech runs a project called [IODA](https://ioda.inetintel.cc.gatech.edu/) that continuously probes address blocks worldwide and records which ones answer. Free API, no key needed.

First, the incident window in Unix time:

```bash
$ date -u -d '2026-09-14 14:00:00' +%s
1789394400
$ date -u -d '2026-09-14 21:00:00' +%s
1789419600
```

Now pull raw signals for MEO residential:

```bash
$ curl -s "https://api.ioda.inetintel.cc.gatech.edu/v2/signals/raw/asn/3243?from=1789394400&until=1789419600&sourceParams=" \
  | jq '.data[0][] | select(.datasource=="ping-slash24") | .values'

[5938, 5933, 5930, 5933, 5933, 5923, 5928, 5930, 5933, 5932, 5932, 5937,
 5933, 5930, 5287, 2905, 1670, 1039,  368,  817,  524, 3957, 5467, 5648,
 5834, 5917, 5932, 5939, 5936, 5941, 5939, 5946, 5944, 5942, 5947, 5944]
```

Read that left to right. Cruising at ~5,930 responsive blocks… then **5287 → 2905 → 1670 → 1039 → 368**.

From 5,933 down to **368**. A **93.8% collapse**, bottoming out at 18:00 WEST.

Same query for the others:

| Network | Role | Normal | Worst | Drop |
|---|---|---|---|---|
| AS3243 | Residential | 5,933 | 368 | **-93.8%** |
| AS42863 | Mobile | 606 | 42 | **-93.1%** |
| AS15525 | Business | 481 | 19 | **-96.0%** |
| AS8657 | International | 6 | 0 | **-100%** |

And packet loss, from the same response:

```bash
$ curl -s "https://api.ioda.inetintel.cc.gatech.edu/v2/signals/raw/asn/3243?from=1789394400&until=1789419600&sourceParams=" \
  | jq '[.data[0][] | select(.datasource=="ping-slash24-loss")
         | .values[][0].agg_values.loss_pct]'

[9.53, 9.59, 9.63, 9.64, 9.58, 9.69, 9.63, 9.6, 9.51, 9.51, 9.51, 9.5,
 9.47, 9.45, 19.4, 57.47, 77.26, 88.76, 97.49, 95.41, 93.91, 72.84,
 26.87, 16.51, 13.93, 10.93, 9.65, 9.48]
```

Baseline loss ~9.5%. Peak loss **97.49%**.

Ninety-seven percent of probes into MEO's network just vanished. This was not a vibe.

### Step 2, was it only MEO, or all of Portugal?

IODA also correlates outage events:

```bash
$ curl -s "https://api.ioda.inetintel.cc.gatech.edu/v2/outages/events?from=1789394400&until=1789419600&limit=100&relatedTo=asn%2F3243" \
  | jq -r '.data[] | select(.datasource=="ping-slash24")
           | "\(.location_name)  start=\(.start|todate)  dur=\(.duration/60)min"'

Coimbra    start=2026-09-14T16:30:00Z  dur=70min
Porto      start=2026-09-14T16:30:00Z  dur=70min
Braga      start=2026-09-14T16:30:00Z  dur=70min
Aveiro     start=2026-09-14T16:30:00Z  dur=70min
Faro       start=2026-09-14T16:30:00Z  dur=70min
Portugal   start=2026-09-14T16:30:00Z  dur=70min
Lisboa     start=2026-09-14T16:40:00Z  dur=60min
Setúbal    start=2026-09-14T16:40:00Z  dur=60min
Madeira    start=2026-09-14T16:40:00Z  dur=60min
```

A **country-level** outage event. Seventy minutes. Lisbon, Porto, Coimbra, Braga, Faro, even Madeira.

16:30 UTC = 17:30 WEST. Exactly when the complaints started.

---

## Now the routing side, this is where it gets fun

RIPE runs the [Routing Information Service](https://ris.ripe.net/), a fleet of collectors worldwide that record BGP messages as they happen. Also free, also no key.

First question: was there unusual BGP activity? Let's compare the incident day against *the same hours the day before*.

```bash
$ for day in 2026-09-13 2026-09-14; do
    printf "%s  " "$day"
    curl -s "https://stat.ripe.net/data/bgp-updates/data.json?resource=AS3243&starttime=${day}T14:00:00&endtime=${day}T21:00:00" \
    | jq '.data.nr_updates'
  done

2026-09-13  545
2026-09-14  72679
```

545 → **72,679**. That's **133 times** normal.

Across all four networks:

| Network | 13 Sep | 14 Sep | Multiplier |
|---|---|---|---|
| AS3243 Residential | 545 | 72,679 | **133×** |
| AS8657 International | 350 | 19,428 | **56×** |
| AS42863 Mobile | 460 | 59,604 | **130×** |
| AS15525 Business | 1,377 | 223,374 | **162×** |

Something shook the routing table hard. Where exactly? There's an endpoint that buckets it by time:

```bash
$ curl -s "https://stat.ripe.net/data/bgp-update-activity/data.json?resource=AS3243&starttime=2026-09-14T16:00:00&endtime=2026-09-14T18:40:00&min_sampling_period=600&hide_empty_samples=false" \
  | jq -r '.data.updates[] | "\(.starttime[11:16])Z   \(.announcements)"'

16:00Z        0
16:10Z        0
16:20Z      250
16:30Z     1069
16:40Z     9724
16:50Z     3824
17:00Z    30458      ← peak
17:10Z    10089
17:20Z     9334
17:30Z     6325
17:40Z        1
17:50Z        9
18:00Z      868
```

Thirty thousand announcements in ten minutes, right at 17:00 UTC (18:00 WEST), the exact moment packet loss peaked. The data plane and the control plane broke together.

### The single best piece of evidence

This is my favourite bit. I took one real MEO prefix (`194.65.0.0/16`, where `www.meo.pt` lives) and pulled the global routing state at three moments: before, during, after.

```bash
$ for ts in 16:00 17:10 18:30; do
    echo "=== $ts UTC ==="
    curl -s "https://stat.ripe.net/data/bgp-state/data.json?resource=194.65.0.0%2F16&timestamp=2026-09-14T${ts}:00" \
    | jq -r '.data.bgp_state[]
             | select(.source_id | test("195.66.227.163|80.81.192.194|12.0.1.63|208.51.134.248"))
             | "\(.source_id)   \(.path|join(" "))"'
  done
```

Output:

```
=== 16:00 UTC ===
01-195.66.227.163   1299 8657 3243
12-80.81.192.194    6762 8657 3243
00-12.0.1.63        7018 1299 8657 3243
00-208.51.134.248   3549 3356 1299 8657 3243

=== 17:10 UTC ===
01-195.66.227.163   1299 174 8657 3243
12-80.81.192.194    6762 174 8657 3243
00-12.0.1.63        7018 174 8657 3243
00-208.51.134.248   3549 3356 174 8657 3243

=== 18:30 UTC ===
01-195.66.227.163   1299 8657 3243
12-80.81.192.194    6762 8657 3243
00-12.0.1.63        7018 1299 8657 3243
00-208.51.134.248   3549 3356 1299 8657 3243
```

Do you see it?

**`174` appears in every single path during the incident, then vanishes again afterwards.**

AS174 is Cogent. Before, the world reached MEO through a healthy spread of providers. During, almost everything funnelled through one.

The aggregate confirms it:

| Time (UTC) | Routes seen | via AS6939 | via AS174 | via AS1299 |
|---|---|---|---|---|
| 16:00, before | 384 | 164 | 17 | 30 |
| 17:10, during | 327 | 19 | **207** | 0 |
| 18:30, after | 385 | 163 | 24 | 24 |

Hurricane Electric went from 164 views to 19. Arelion went to **zero**. Cogent went from 17 to 207.

That is what a network looks like when most of its doors slam shut and everyone piles through the one that's left.

```mermaid
flowchart TB
    subgraph BEFORE["16:00 UTC: healthy"]
        direction LR
        W1["World"] --> A1["Arelion"] --> M1["AS8657"]
        W1 --> H1["Hurricane"] --> M1
        W1 --> C1["Cogent"] --> M1
        W1 --> S1["Sparkle"] --> M1
    end

    subgraph DURING["17:10 UTC: under attack"]
        direction LR
        W2["World"] -.->|gone| A2["Arelion"]
        W2 -.->|mostly gone| H2["Hurricane"]
        W2 ==>|"207 of 327 views"| C2["Cogent"] ==> M2["AS8657"]
        W2 -.->|gone| S2["Sparkle"]
    end

    style C2 fill:#2c100c,stroke:#ff4b34,stroke-width:3px,color:#ff8a78
    style A2 fill:#120808,stroke:#331914,color:#5a4340
    style S2 fill:#120808,stroke:#331914,color:#5a4340
```

---

## The plot twist: Ghana

While I was writing this up, someone pointed me at a [tweet](https://x.com/AnteroJesusPT/status/2099564377212887169) claiming the whole thing was a BGP leak involving **Ghana Telecom**.

I nearly dismissed it. I'm glad I didn't, because it was real.

Cloudflare Radar runs an automatic route-leak detector, and it had logged [event 614156](https://radar.cloudflare.com/routing/anomalies/event/leak-614156):

```
BGP route leak: 614156

Routes obtained from:  AS37468  Angola Cables (AO)
Routes leaked by:      AS29614  Ghana Telecommunications (GH)
Routes leaked to:      AS8657  MEO (PT)

Prefix count:          5
Peer count:            16
Origin count:          2
Detection time:        09/14/2026, 01:13
Latest message time:   09/14/2026, 17:37
```

So: Ghana Telecom took routes it learned from Angola Cables and advertised them to MEO. A textbook **provider-customer-provider leak**.

When I checked the relationships, it got worse:

```bash
$ curl -s "https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS29614" \
  | jq -r '.data.neighbours[] | select(.type=="left") | "\(.asn)  strength \(.power)"' \
  | sort -k3 -rn | head -4

8657   strength 3697     ← MEO is Ghana's STRONGEST upstream
37613  strength 2642
37468  strength 2010     ← Angola Cables
1273   strength 439
```

**MEO is one of Ghana Telecom's transit providers.** Ghana sat between two providers and passed one's routes to the other.

```mermaid
flowchart TD
    AC["AS37468<br/>Angola Cables"] -->|"1. normal: sells transit to Ghana"| GH["AS29614<br/>Ghana Telecom"]
    MEO["AS8657<br/>MEO International"] -->|"2. also sells transit to Ghana"| GH
    GH -.->|"3. ❌ LEAK<br/>passes Angola's routes to MEO"| MEO
    MEO -->|"4. MEO may now send<br/>traffic the wrong way"| GH
    GH --> AC

    style GH fill:#131a0d,stroke:#b6f24a,stroke-width:3px,color:#b6f24a
    style MEO fill:#0c1a1c,stroke:#42d9c0,stroke-width:2px,color:#42d9c0
```

Here's why that's genuinely nasty: most networks give routes learned from a **customer** the highest preference, because customers pay you. Ghana is MEO's customer. So a leaked route arriving from Ghana can beat a perfectly good route from a peer *on preference alone*, before path length is even considered.

### Which five prefixes, though?

Cloudflare tells you there were five. It does not tell you *which* five. I wanted to know, if they turned out to be something enormous like a CDN or a cloud region, this leak would look a lot more dangerous.

The obvious query fails:

```bash
# Look for "MEO ← Ghana ← Angola" in RIPEstat's per-ASN feed
$ curl -s "https://stat.ripe.net/data/bgp-updates/data.json?resource=AS29614&starttime=2026-09-14T00:00:00&endtime=2026-09-14T19:00:00" \
  | jq -r '.data.updates[].attrs.path | select(.) | join(" ")' \
  | grep -c "8657 29614 37468"

0
```

Zero. That endpoint only returns prefixes the ASN **originates**, and the leaked routes belonged to someone further downstream. Narrowing by Angola Cables' customer list doesn't help either:

```bash
$ curl -s "https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS37468" \
  | jq '[.data.neighbours[] | select(.type=="right")] | length'

2435
```

So the convenient APIs are a dead end. Fine, go to the raw tape.

### Reading the actual BGP messages

RIPE publishes every BGP message its collectors receive as **MRT dumps** ([RFC 6396](https://datatracker.ietf.org/doc/html/rfc6396)), five-minute files, going back years. Nothing is summarised for you here; it's the wire format, exactly as the routers spoke it.

To read it you want `bgpdump`, RIPE's own C tool:

```bash
# Arch / Omarchy
yay -S bgpdump

# Debian / Ubuntu
sudo apt install bgpdump

# macOS
brew install bgpdump
```

The files are named by collector and five-minute slot:

```bash
$ curl -s "https://data.ris.ripe.net/rrc00/2026.09/" \
  | grep -o 'updates\.20260914\.17[0-9][0-9]\.gz' | head -4

updates.20260914.1700.gz
updates.20260914.1705.gz
updates.20260914.1710.gz
updates.20260914.1715.gz
```

I pulled 28 of them, four collectors (`rrc00` multihop, `rrc01` LINX London, `rrc03` AMS-IX, `rrc12` DE-CIX Frankfurt) across both moments where Cloudflare said something happened. 122 MB of compressed BGP.

Then the entire search is one grep:

```bash
$ bgpdump -m rrc01.1730.gz | grep '29614 37468'
```

`-m` gives one pipe-delimited line per message, and because the AS path is just text in that line, "Ghana Telecom announcing Angola Cables' routes" is literally the string `29614 37468`.

Here's real output, untouched:

```text
BGP4MP|1789348431|A|195.66.227.160|3214|102.208.80.0/24|
    3214 8657 29614 37468 174 37282 329486|IGP|195.66.227.160|0|0||NAG||

BGP4MP|1789407023|A|195.90.96.2|48185|169.239.249.0/24|
    48185 8657 29614 29614 29614 29614 29614 37468 37282
    327992 327992 327992 327992 327992|IGP|195.90.96.2|0|0||NAG||
```

Run it across all 28 files:

```bash
$ for g in *.gz; do bgpdump -m "$g"; done | grep '29614 37468' > leak.txt
$ wc -l < leak.txt
146

$ awk -F'|' '{print $6}' leak.txt | sort -u
102.208.80.0/24
169.239.248.0/24
169.239.249.0/24
169.239.250.0/24
169.239.251.0/24

$ awk -F'|' '{n=split($7,a," "); print a[n]}' leak.txt | sort -u
327992
329486

$ awk -F'|' '{print $4}' leak.txt | sort -u | wc -l
30
```

**Five prefixes. Two origin ASNs.** Cloudflare's event said five prefixes and two origins. Two completely independent systems, same answer.

And the timing, pulled from field 2:

```bash
$ awk -F'|' '{print $6, strftime("%H:%M:%S",$2,1)}' leak.txt \
  | sort -u -k1,1 -k2,2
```

| Prefix | First seen (UTC) | Last seen (UTC) | Announcements | RIS peers |
|---|---|---|---|---|
| `102.208.80.0/24` | 01:13:51 | 17:31:38 | 22 | 11 |
| `169.239.248.0/24` | 17:30:23 | 17:37:49 | 30 | 29 |
| `169.239.249.0/24` | 17:30:23 | 17:37:49 | 32 | 29 |
| `169.239.250.0/24` | 17:30:23 | 17:37:49 | 32 | 29 |
| `169.239.251.0/24` | 17:30:23 | 17:37:49 | 30 | 29 |

Cloudflare's event window was *01:13 → 17:37*. The dumps say **01:13:51 → 17:37:49**. Matching to the second, from raw router messages.

That bank prefix is the interesting one: announced at 01:13 and still being re-announced at 17:31, **sixteen hours later**. It sat in the table all day.

### So who actually got leaked?

| Prefix | Origin AS | Who that really is | Country |
|---|---|---|---|
| `102.208.80.0/24` | AS329486 | **GCB Bank PLC**, Ghana's largest indigenous bank | 🇬🇭 GH |
| `169.239.248.0/24` | AS327992 | **GARNET**, Ghanaian Academic & Research Network | 🇬🇭 GH |
| `169.239.249.0/24` | AS327992 | GARNET, NOC block | 🇬🇭 GH |
| `169.239.250.0/24` | AS327992 | GARNET, point-to-point block | 🇬🇭 GH |
| `169.239.251.0/24` | AS327992 | GARNET, customer point-to-point | 🇬🇭 GH |

Straight from the registries, no interpretation:

```bash
$ curl -s "https://stat.ripe.net/data/whois/data.json?resource=102.208.80.0/24" | jq -r ...
    inetnum : 102.208.80.0/24
    netname : GCB-BANK
    descr   : GCB BANK PLC
    country : GH
    status  : ASSIGNED PI
    mnt-by  : AFRINIC-HM-MNT

$ curl -s "https://stat.ripe.net/data/whois/data.json?resource=169.239.249.0/24" | jq -r ...
    inetnum : 169.239.249.0/24
    netname : GARNET-NOC
    descr   : for GARNET NOC
    country : GH
    status  : ASSIGNED PA
    mnt-by  : GARNET-MNT
```

**A Ghanaian bank and a Ghanaian university network.** Five /24s, **1,280 IP addresses in total.**

Not a CDN. Not a cloud region. Not Portugal.

### The full chain, decoded

Here's one of the real paths, lifted straight out of the dump:

```text
1103   8657   29614   37468   174   37282   329486
  │      │      │       │      │      │       └─ GCB Bank, Ghana
  │      │      │       │      │      └───────── MainOne Cable, Nigeria
  │      │      │       │      └──────────────── Cogent
  │      │      │       └─────────────────────── Angola Cables
  │      │      └─────────────────────────────── Ghana Telecom  ← leak happens here
  │      └────────────────────────────────────── MEO International
  └───────────────────────────────────────────── SURFnet (the RIS peer that logged it)
```

A Ghanaian bank's traffic went via Nigeria, via Cogent, via Angola, back through Ghana, into **Portugal**, then out to the world. That's a spectacularly stupid path for packets to take, and it is exactly what a route leak looks like.

The GARNET one is even more revealing, look at the prepending:

```text
3257 8657 29614 29614 29614 29614 29614 37468 37282 327992 327992 327992 327992 327992
          └──────── ×5 ────────┘                    └────────── ×5 ──────────┘
```

Ghana Telecom prepended itself five times, GARNET prepended itself five times. Prepending is how you make a route *less* attractive. Somebody was already fighting a traffic-engineering fire when this went out the door.

There's a subtler fingerprint too. `bgpdump` shows the ORIGIN attribute, and it's inconsistent across the four GARNET prefixes:

```text
169.239.248.0/24  INCOMPLETE
169.239.249.0/24  IGP
169.239.250.0/24  IGP
169.239.251.0/24  INCOMPLETE
```

`INCOMPLETE` means the route was redistributed into BGP from somewhere else rather than originated cleanly. Four adjacent /24s from the same network shouldn't disagree about that. It's the signature of a config in mid-edit.

```mermaid
flowchart LR
    BANK["🏦 GCB Bank<br/>AS329486<br/>102.208.80.0/24"] --> MAIN["MainOne<br/>AS37282 🇳🇬"]
    GAR["🎓 GARNET<br/>AS327992<br/>169.239.248-251.0/24"] --> MAIN
    MAIN --> ANG["Angola Cables<br/>AS37468 🇦🇴"]
    ANG --> GH["Ghana Telecom<br/>AS29614 🇬🇭"]
    GH -.->|"❌ LEAK<br/>01:13 and 17:30 UTC"| MEO["MEO International<br/>AS8657 🇵🇹"]
    MEO --> W["🌍 rest of<br/>the internet"]

    style GH fill:#131a0d,stroke:#b6f24a,stroke-width:3px,color:#b6f24a
    style MEO fill:#0c1a1c,stroke:#42d9c0,stroke-width:2px,color:#42d9c0
```

### The detail that made me sit up

Remember MEO's three attack windows? Convert them to UTC and check where every leak event lands:

```text
wave 1   00:45-02:55 WEST  =  23:45-01:55 UTC
wave 2   17:30-19:30 WEST  =  16:30-18:30 UTC

102.208.80.0/24    first  01:13:51 UTC   ->  INSIDE wave 1
102.208.80.0/24    last   17:31:38 UTC   ->  INSIDE wave 2
169.239.248-251    first  17:30:23 UTC   ->  INSIDE wave 2
169.239.248-251    last   17:37:49 UTC   ->  INSIDE wave 2
```

**Every single leak event lands inside a declared attack window.** Not near one, inside.

The bank prefix leaked during wave 1, sat quietly for sixteen hours, then got re-announced during wave 2. The GARNET prefixes appeared 23 seconds after wave 2 started.

That is not what an independent accident looks like. It's what you'd expect when BGP sessions are flapping under load and routers are frantically re-announcing whatever they still hold.

### Would RPKI have stopped it?

No, and this is the part worth understanding.

```bash
$ curl -s "https://stat.ripe.net/data/rpki-validation/data.json?resource=AS327992&prefix=169.239.249.0/24" \
  | jq -r '.data.status'
valid

$ curl -s "https://stat.ripe.net/data/rpki-validation/data.json?resource=AS329486&prefix=102.208.80.0/24" \
  | jq -r '.data.status'
unknown
```

The GARNET prefixes are **RPKI-valid**. GARNET published a signed statement saying "AS327992 may announce this", and AS327992 *is* the origin in the leaked path. Everything checks out cryptographically.

Because RPKI only validates the **origin**, the last hop. It says nothing about the middle of the path. The origin here was completely legitimate; the crime happened four hops later, when Ghana Telecom passed routes to a provider it shouldn't have.

That's why RPKI alone doesn't fix leaks, and why people are working on [ASPA](https://datatracker.ietf.org/doc/draft-ietf-sidrops-aspa-verification/). It lets you also sign *who your providers are*, so a leak like this becomes detectable in the path rather than only at the origin.

### So was Ghana the cause of the outage?

No. Now we can say it with numbers instead of hand-waving.

The leak moved **1,280 IP addresses belonging to a bank and a university in Ghana**. The outage made roughly **93-96% of MEO's entire address space** unreachable across Portugal, millions of addresses, thousands of /24s, in a completely different country.

Five /24s in West Africa cannot do that. The scale is off by orders of magnitude, and the geography is wrong.

The leak was real. It was concurrent. It is now fully identified. And it was **a symptom, not the disease**.

---

## How a flood breaks routing

Here's the mechanism everyone was missing in the argument.

BGP sessions are just TCP connections. Two routers agree to chat, then send each other small **keepalive** messages to prove they're still alive. Typically if you miss three in a row, around 90 seconds of silence, your neighbour declares you dead and throws away every route you ever told it about.

Now imagine someone floods that link with junk traffic:

```mermaid
sequenceDiagram
    participant A as MEO AS8657
    participant L as International link
    participant B as Arelion AS1299

    Note over A,B: 🟢 Normal operation
    A->>B: keepalive
    B->>A: keepalive
    Note over A,B: session healthy, routes shared

    Note over L: 🔴 DDoS traffic floods the link
    A->>L: keepalive
    L--xB: dropped in congestion
    A->>L: keepalive
    L--xB: dropped
    A->>L: keepalive
    L--xB: dropped

    Note over B: hold timer expires
    B->>B: ❌ declare AS8657 unreachable
    B->>B: withdraw all its routes
    B-->>A: "I can no longer reach MEO"

    Note over A,B: 🌍 the whole internet<br/>reroutes around the gap
```

That single failure ripples outward. Every network that learned MEO's routes via Arelion now has to pick a different path. They all recompute. They tell *their* neighbours. And you get 72,679 announcements where you'd normally see 545.

And crucially, when the good paths disappear, **whatever is left wins**. Including a pre-existing leaked route from Ghana that had been sitting there quietly losing the best-path contest all along.

That's the resolution of the whole argument:

```mermaid
flowchart TD
    D["💥 DDoS floods<br/>international links"] --> C["Congestion,<br/>packet loss"]
    C --> U["👥 Customers lose<br/>international sites"]
    C --> K["BGP keepalives<br/>get dropped"]
    K --> S["❌ Sessions time out"]
    S --> W["Routes withdrawn"]
    W --> R["🔄 Global reconvergence<br/>72,679 announcements"]
    R --> CG["Everything funnels<br/>through Cogent AS174"]
    R --> LK["🟡 Pre-existing Ghana leak<br/>becomes the selected path"]

    M["🛡️ Mitigation:<br/>filtering + scrubbing"] --> RE["Sessions restored,<br/>diversity returns"]
    C -.-> M

    style D fill:#2c100c,stroke:#ff4b34,color:#ff8a78
    style LK fill:#131a0d,stroke:#b6f24a,color:#b6f24a
    style M fill:#0c1a1c,stroke:#42d9c0,color:#42d9c0
```

A DDoS **cannot** make Ghana Telecom invent a bad route. That misconfiguration already existed, and the detector spotted it at 01:13, sixteen hours before the evening outage. But a DDoS absolutely **can** delete all the better paths, and suddenly that quiet mistake becomes the road everyone drives down.

---

## What MEO and the regulator finally said

On Tuesday, MEO published a much more detailed statement, and it's worth quoting properly. Via [RTP/Lusa](https://www.rtp.pt/noticias/economia/incidentes-na-meo-foram-provocados-por-ataque-de-negacao-de-servico_n1765613):

> Os incidentes "ocorreram nos períodos, 00:45-02:55, 17:30-19:30 e 22:45-01:05, tendo como alvo específico infraestrutura da Meo, e envolveram elevados volumes de tráfego que provocaram congestionamento e degradação temporária da rede internacional".

And on the BGP question specifically:

> "Instabilidade nos encaminhamentos BGP foi consequência do ataque e não a sua causa."
>
> *("BGP routing instability was a consequence of the attack, not its cause.")*

**ANACOM**, the Portuguese regulator, backed this up. It told *Expresso* that a cyberattack is confirmed, and that a DDoS aimed specifically at MEO's international interconnections is the probable cause while analysis continues. They also noted the congestion caused knock-on degradation at other operators' interconnections, which explains why NOS and Vodafone customers saw a few hundred complaints each despite not being attacked.

The incident was reported to Portugal's **CNCS** (national cybersecurity centre), as legally required.

Final scoreboard:

| Claim | Verdict |
|---|---|
| Nationwide outage was real | ✅ Confirmed independently by IODA |
| Caused by targeted DDoS | ✅ Confirmed by MEO telemetry, probable per ANACOM |
| Massive BGP churn happened | ✅ Confirmed by RIPE RIS |
| BGP churn *caused* the outage | ❌ MEO says consequence, not cause |
| Ghana route leak existed | ✅ Real, Cloudflare Radar |
| Ghana leak caused the outage | ❌ 1,280 Ghanaian addresses can't do that |
| Leaked prefixes identified | ✅ All 5, from raw RIPE MRT dumps |
| Customer data compromised | ❌ None per MEO, no independent audit |

---

## What we still don't know

I want to be honest about the holes, because a lot of incident write-ups quietly paper over them.

- **How big was the attack?** No bandwidth figure, no packet rate. 200 Gbps or 2 Tbps? Unknown.
- **What kind of attack?** UDP flood? DNS reflection? Carpet bomb across many prefixes? Unknown.
- **Who did it?** No attribution, no claimed responsibility.
- **What exactly was targeted?** "International interconnections" is vague. Which routers? Which interfaces?
- **Did MEO actually install the Ghana path?** We can see MEO propagated it onward (that's how RIS peers recorded it), so it was in the table. What we can't see is how much traffic, if any, followed it.
- **Why did Ghana Telecom leak?** A filter change, a session reset, an automation bug? The five-times prepending suggests someone was already mid-fire-fight, but that's inference.
- **How many customers were affected?** MEO declined to say.

What would settle it: NetFlow samples, router logs with timestamps, BGP session state history, and the attack traffic profile. Standard post-incident report material. Whether MEO publishes one is another question.

---

## Try it yourself

Everything here came from free, public, no-authentication APIs. If you want to poke at your own ISP:

```bash
# What's my ISP's AS number and who owns it?
curl -s "https://stat.ripe.net/data/as-overview/data.json?resource=AS3243" | jq .data

# What address blocks do they announce?
curl -s "https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS3243" \
  | jq -r '.data.prefixes[].prefix'

# Who do they connect to?
curl -s "https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS3243" \
  | jq .data.neighbour_counts

# Is a route cryptographically authorised? (RPKI)
curl -s "https://stat.ripe.net/data/rpki-validation/data.json?resource=AS3243&prefix=194.65.0.0/16" \
  | jq '.data | {status, validating_roas}'
```

That last one returns:

```json
{
  "status": "valid",
  "validating_roas": [
    { "origin": "3243", "prefix": "194.65.0.0/16", "max_length": 24, "validity": "valid" }
  ]
}
```

Which means MEO has published a signed statement saying "AS3243 is allowed to announce this". RPKI is the partial fix for BGP's trust problem, it can stop a stranger claiming your addresses, though it doesn't stop leaks like Ghana's, because the origin there was still legitimate.

And a fun one, confirm these prefixes hold actual machines:

```bash
$ getent ahostsv4 www.meo.pt
194.65.61.30    STREAM www.meo.pt          # inside 194.65.0.0/16

$ dig +short -x 2.80.92.107
bl19-92-107.dsl.telepac.pt.               # someone's actual home DSL line

$ dig +short -x 85.240.0.1
bl7-0-1.dsl.telepac.pt.                   # inside 85.240.0.0/13
```

Those `bl…dsl.telepac.pt` names are residential broadband lines. When the graphs above flatlined, that's whose evening got ruined.

### Going deeper: read the raw BGP yourself

If you want to do what I did in the Ghana section, it's three commands:

```bash
# 1. get the tool
yay -S bgpdump          # or: sudo apt install bgpdump / brew install bgpdump

# 2. grab a five-minute slice of global BGP from any collector
curl -sO https://data.ris.ripe.net/rrc01/2026.09/updates.20260914.1730.gz

# 3. hunt for any AS adjacency you care about
bgpdump -m updates.20260914.1730.gz | grep '29614 37468'
```

The `-m` output is pipe-delimited, so `awk` does the rest:

```bash
# field 2 = unix timestamp, 4 = peer IP, 6 = prefix, 7 = AS path
bgpdump -m updates.20260914.1730.gz \
  | awk -F'|' '$7 ~ /29614 37468/ {print strftime("%H:%M:%S",$2,1), $6, $7}'
```

That's the whole technique. Every route leak, hijack and outage since 1999 is sitting in that archive waiting to be read.

---

## The takeaway

Three things worth remembering:

**1. Shared dependencies are invisible until they break.** MEO has serious diversity at the edge, Hurricane, Arelion, Cogent, Sparkle, RETN, exchange points in London, Frankfurt, Amsterdam, Madrid, Marseille. None of it helped, because residential, mobile and business all converge on AS8657 in the public routing view. Many external links, one logical failure domain.

**2. Correlation during an incident is a trap.** The Ghana leak was real, it was concurrent, both bursts landed *inside* the attack windows, and it still wasn't the cause. If you'd stopped investigating the moment you found it, you'd have published something confidently wrong. Two things can be true at once.

And "I can't find out" is usually a tooling problem, not a data problem. The summary APIs could not name those five prefixes, they physically don't carry that view. `bgpdump` and 122 MB of raw dumps named all five, both origins, and the timestamps to the second.

**3. You can check this stuff yourself.** RIPE, IODA, Cloudflare Radar, PeeringDB, all free, all open, all queryable from a laptop with `curl`. The internet's routing system is remarkably transparent if you know where to look. You don't have to take anyone's word for it, including mine.

---

## Sources

**Operator and regulator**

- [RTP / Lusa, MEO confirms targeted DDoS, three attack windows](https://www.rtp.pt/noticias/economia/incidentes-na-meo-foram-provocados-por-ataque-de-negacao-de-servico_n1765613)
- [ECO, same statement, independent publication](https://eco.sapo.pt/2026/09/15/incidentes-na-meo-foram-provocados-por-ataque-de-negacao-de-servico/)
- [Observador, initial "ataque em massa" statement, 14 Sep](https://observador.pt/2026/09/14/meo-com-problemas-de-rede-em-todo-o-pais/)
- [Público, outage coverage and ANACOM reaction](https://www.publico.pt/2026/09/14/tecnologia/noticia/meo-avarias-generalizadas-pais-2149301)
- [Pplware, technical community coverage](https://pplware.sapo.pt/informacao/meo-falha-nacional-ataque/)
- [TEK / Sapo, timeline of statements](https://tek.sapo.pt/noticias/telecomunicacoes/artigos/meo-confirma-ataque-informatico)

**Measurement systems I queried**

- [IODA, Internet Outage Detection and Analysis (Georgia Tech)](https://ioda.inetintel.cc.gatech.edu/) · [API docs](https://api.ioda.inetintel.cc.gatech.edu/v2/)
- [RIPEstat Data API](https://stat.ripe.net/docs/data_api), as-overview, announced-prefixes, asn-neighbours, bgp-updates, bgp-update-activity, bgp-state, rpki-validation
- [RIPE RIS, Routing Information Service](https://ris.ripe.net/) · [raw MRT archive](https://data.ris.ripe.net/)
- [`bgpdump`](https://github.com/RIPE-NCC/bgpdump), RIPE NCC's MRT parser, used for all the raw-dump work here
- [AFRINIC whois](https://afrinic.net/), registry records for the leaked Ghanaian prefixes
- [Cloudflare Radar, route leak event 614156](https://radar.cloudflare.com/routing/anomalies/event/leak-614156)
- [PeeringDB, AS8657 interconnection facts](https://www.peeringdb.com/asn/8657)

**Background reading on BGP**

- [RFC 4271, A Border Gateway Protocol 4 (BGP-4)](https://datatracker.ietf.org/doc/html/rfc4271)
- [RFC 7908, Problem Definition and Classification of BGP Route Leaks](https://datatracker.ietf.org/doc/html/rfc7908)
- [RFC 6396, MRT routing information export format](https://datatracker.ietf.org/doc/html/rfc6396) (what the raw dumps are encoded in)
- [ASPA, Autonomous System Provider Authorization](https://datatracker.ietf.org/doc/draft-ietf-sidrops-aspa-verification/) (the proposed fix for leaks)
- [MANRS, Mutually Agreed Norms for Routing Security](https://www.manrs.org/)
- [Cloudflare Learning, what is BGP?](https://www.cloudflare.com/learning/security/glossary/what-is-bgp/)

---

*All measurements in this post were taken on 14-15 September 2026 from public APIs. Timestamps from IODA and RIPE are UTC; WEST is UTC+1. Where I've inferred something rather than measured it, I've said so. Corrections welcome, the whole point is that you can re-run the commands.*
