MPAT is a high-performance command-line tool for extracting and analyzing Internet-scale route tracing measurements. It enables researchers and network operators to retrieve, store, and query traceroute-like probe data from measurement platforms at scale.
MPAT is part of Sorbonne's IP Route Survey (IPRS) initiative: https://iprs.dioptra.io
Internet Measurement Platforms (IMPs) such as Iris collect massive amounts of probe data for observing network behavior. However, retrieving, storing, and querying this data at scale requires efficient tooling.
MPAT addresses this by providing:
- A client for the Iris measurement platform API.
- A client for the RIPE Stat Data API for fetching BGP prefix data.
- A client for the Retina live stream API for consuming FIEs in real time.
- A high-throughput pipeline for fetching probe results into a local ClickHouse instance.
- A computation pipeline for deriving higher-level structures such as Forwarding Info Elements (FIEs) from raw probe results.
- A flexible query interface for filtering measurements by state, date range, and tag.
MPAT is structured around the following internal packages:
internal/iris— Client for the Iris API. Handles JWT authentication, measurement queries, and ClickHouse result retrieval via HTTP streaming.internal/ripe— Client for the RIPE Stat Data API. Handles BGP prefix queries using a builder pattern, with support for historical snapshots via time-of-day or raw timestamp.internal/retina— Client for the Retina live stream API. Handles NDJSON streaming and batch delivery of Forwarding Info Elements.internal/store— Low-level client for the local ClickHouse instance. Handles table creation, write policies, and bulk insertion.internal/schema— Schema definitions for all supported table types (results,resultslite,fies,ripeprefixes). Provides schema introspection, compatibility checking, DDL rendering, and DDL parsing via the AfterShip ClickHouse SQL parser.internal/service— Business logic for fetch and compute operations. Each service owns its SQL templates and orchestrates store and client interactions.
Data flows as follows:
Iris ClickHouse → mp fetch iris-results (HTTP stream) → Local ClickHouse
↓
mp compute fies
↓
Derived tables (e.g. FIEs)
RIPE Stat API → mp fetch ripe-prefixes (native insert) → Local ClickHouse
Retina Stream API → mp fetch retina-fies (NDJSON stream) → Local ClickHouse
No intermediate deserialization occurs during Iris fetch — the JSON stream is piped directly into ClickHouse. RIPE prefix data is inserted via the native ClickHouse driver. Retina FIEs are streamed as NDJSON, deserialized, and inserted in batches. Compute operations run entirely server-side within ClickHouse.
- Go 1.23.4+
- A running ClickHouse instance
- Access to the Iris measurement platform
git clone https://github.com/dioptra-io/mpat.git
cd mpat
make installThis builds the mp binary and installs it to $GOPATH/bin.
| Variable | Required | Description |
|---|---|---|
IRIS_USERNAME |
Yes | Iris account email |
IRIS_PASSWORD |
Yes | Iris account password |
IRIS_ENDPOINT |
No | Iris API endpoint (default: https://api.iris.dioptra.io) |
MPAT_CLICKHOUSE |
Yes | ClickHouse DSN (e.g. clickhouse://user:pass@localhost:9000/mpat) |
MPAT_DATABASE |
No | Destination ClickHouse database (default: mpat) |
MPAT_RIPE_STAT_ENDPOINT |
No | RIPE Stat API endpoint (default: https://stat.ripe.net) |
Fetches Iris probe results into a local ClickHouse table. Supports four source selection modes — exactly one must be specified.
By default, only the columns required for downstream computation are fetched (--lite). Use --lite=false to fetch the full results schema.
| Flag | Default | Description |
|---|---|---|
--policy |
fail |
Write policy: replace, truncate, fail, append |
--database |
mpat |
Destination ClickHouse database |
--lite |
true |
Use ResultsLiteSchema (fewer columns, faster fetch) |
--chunk-size |
500000 |
Number of rows per streaming chunk |
--ewma-alpha |
0.2 |
Alpha parameter for ETA estimation |
--table |
— | Mode 1: fetch a specific source table by name |
--measurement |
— | Mode 2: fetch all result tables for a measurement UUID |
--from |
— | Mode 3: start of date range (RFC3339) |
--to |
— | Mode 3: end of date range (RFC3339) |
--date |
— | Mode 4: date to fetch (YYYY-MM-DD), used with --kind and --index |
--kind |
— | Mode 4: measurement kind: zeph (IPv4) or ipv6 (required) |
--index |
— | Mode 4: 0-based index of the measurement to fetch, ordered by creation time (required) |
--state |
finished |
Measurement state filter (modes 3 and 4) |
--tag |
— | Mode 3: tag regex filter |
--filter-source |
true |
Exclude rows whose IP version does not match the kind (zeph → IPv4, ipv6 → IPv6). Only applies to mode 4. |
| Policy | Behaviour |
|---|---|
replace |
Drop destination table if it exists, recreate and insert |
truncate |
Truncate destination table if not empty, then insert |
fail |
Fail if destination table is not empty |
append |
Insert into destination regardless of existing data |
mp fetch iris-results my_results \
--table results__b78e5bf4_100a_4c20_af14__53863928_7a54_45de_b51a \
--policy replacemp fetch iris-results my_results \
--measurement b78e5bf4-100a-4c20-af14-311a9d43f8a0 \
--policy replacemp fetch iris-results my_results \
--from 2026-06-01T00:00:00Z \
--to 2026-06-02T00:00:00Z \
--policy replaceWith optional filters:
mp fetch iris-results my_results \
--from 2026-06-01T00:00:00Z \
--to 2026-06-02T00:00:00Z \
--state finished \
--tag "zeph" \
--policy appendFetches a specific measurement for a given date. All measurements for the day are retrieved and filtered by --kind, then sorted by creation time. --index 0 selects the first measurement, --index 1 the second, and so on. An error is returned if the index is out of bounds.
--kind also determines the IP version filter: zeph → IPv4, ipv6 → IPv6. --state is supported as an optional filter.
mp fetch iris-results my_results \
--date 2026-06-01 \
--kind zeph \
--index 0 \
--policy replaceWith optional state filter:
mp fetch iris-results my_results \
--date 2026-06-01 \
--kind ipv6 \
--index 1 \
--state finished \
--policy appendfound 2 table(s), policy is set to 'replace'
total of 147 chunk(s) will be fetched.
[1/2] results__b78e5bf4_... 58,365,836 rows 117 chunks
chunk 1/1/147 | 4s | 119,047 rows/s | Jun 2, 8:12pm (in ~10m14s)
chunk 2/2/147 | 4s | 123,451 rows/s | Jun 2, 8:11pm (in ~9m58s)
[2/2] results__c4a1_... 44,619,062 rows 30 chunks
chunk 1/118/147 | 4s | 121,951 rows/s | Jun 2, 8:10pm (in ~2m1s)
Fetches BGP prefixes originated by a set of ASes from the RIPE Stat RIS API and inserts them into a local ClickHouse table. Data is retrieved from historical RIS snapshots, which are available three times per day at 00:00, 08:00, and 16:00 UTC.
Exactly one of --asns or --tier1 must be specified to select the ASes to query. Exactly one of --date or --timestamp must be specified to select the snapshot time.
| Flag | Default | Description |
|---|---|---|
--policy |
fail |
Write policy: replace, truncate, fail, append |
--database |
mpat |
Destination ClickHouse database |
--asns |
— | Comma-separated list of ASNs (e.g. 3356,1299,3257) |
--tier1 |
false |
Use the hardcoded list of 16 tier-1 ASNs |
--date |
— | Date for the snapshot (e.g. 2026-06-01), used with --snapshot |
--snapshot |
dawn |
Time of day: dawn (08:00 UTC), day (16:00 UTC), night (00:00 UTC next day) |
--timestamp |
— | Raw RFC3339 timestamp, alternative to --date + --snapshot |
--max-retries |
10 |
Maximum number of retry attempts on failure |
--retry-delay |
5s |
Duration to wait between retry attempts |
| Policy | Behaviour |
|---|---|
replace |
Drop destination table if it exists, recreate and insert |
truncate |
Truncate destination table if not empty, then insert |
fail |
Fail if destination table is not empty |
append |
Insert into destination regardless of existing data |
The --tier1 flag uses the following 16 ASNs:
| ASN | Operator |
|---|---|
| 3356 | Lumen (Level 3) |
| 1299 | Arelion |
| 3257 | GTT |
| 2914 | NTT |
| 6453 | Tata |
| 6461 | Zayo |
| 6762 | Sparkle (Telecom Italia) |
| 3491 | PCCW Global |
| 5511 | Orange |
| 12956 | Telxius (Telefonica) |
| 3320 | Deutsche Telekom |
| 6830 | Liberty Global |
| 7018 | AT&T |
| 701 | Verizon |
| 174 | Cogent |
| 6939 | Hurricane Electric |
# Fetch tier-1 ASNs at dawn on June 1st 2026
mp fetch ripe-prefixes ripeprefixes_20260601 \
--tier1 \
--date 2026-06-01 \
--snapshot dawn
# Fetch tier-1 ASNs at night (resolves to 2026-06-02 00:00 UTC)
mp fetch ripe-prefixes ripeprefixes_20260601 \
--tier1 \
--date 2026-06-01 \
--snapshot night
# Fetch specific ASNs using a raw timestamp
mp fetch ripe-prefixes ripeprefixes_20260601 \
--asns 3356,1299,3257 \
--timestamp 2026-06-01T08:00:00Z
# Append a second snapshot to an existing table
mp fetch ripe-prefixes ripeprefixes_20260601 \
--tier1 \
--date 2026-06-01 \
--snapshot day \
--policy append
# Custom retry configuration
mp fetch ripe-prefixes ripeprefixes_20260601 \
--tier1 \
--date 2026-06-01 \
--snapshot dawn \
--max-retries 5 \
--retry-delay 10s| Column | Type | Description |
|---|---|---|
asn |
UInt32 |
AS number |
network |
IPv6 |
Prefix address (IPv4 mapped to ::ffff:x.x.x.x) |
prefix_len |
UInt8 |
Prefix length |
ip_version |
UInt8 |
MATERIALIZED: 4 for IPv4-mapped, 6 for IPv6 |
query_time |
DateTime |
RIS snapshot time |
fetched_at |
DateTime |
Time at which the data was fetched |
The table is ordered by (asn, network, prefix_len) for efficient per-ASN queries and prefix lookups. It is designed to work with ClickHouse's IP_TRIE dictionary layout for fast prefix matching against other tables.
Streams Forwarding Info Elements (FIEs) from the Retina live stream API and inserts them into a local ClickHouse table. FIEs are delivered as a continuous NDJSON stream and inserted in batches. If --timeout is set, the stream is stopped after the given duration and any accumulated FIEs are flushed before exit.
| Flag | Default | Description |
|---|---|---|
--policy |
fail |
Write policy: replace, truncate, fail, append |
--timeout |
0 |
Stream duration; 0 means stream until EOF |
--endpoint |
http://iprl.dioptra.io/api/v1/stream |
Retina stream endpoint URL |
--batch-size |
1000 |
Number of FIEs to accumulate per insert batch |
| Policy | Behaviour |
|---|---|
replace |
Drop destination table if it exists, recreate and insert |
truncate |
Truncate destination table if not empty, then insert |
fail |
Fail if destination table is not empty |
append |
Insert into destination regardless of existing data |
# Stream for 30 seconds into a new table
mp fetch retina-fies retina_fies_20260611 \
--timeout 30s \
--policy replace
# Stream indefinitely, appending to an existing table
mp fetch retina-fies retina_fies_20260611 \
--policy append
# Custom endpoint and batch size
mp fetch retina-fies retina_fies_20260611 \
--endpoint http://my-retina-instance/api/v1/stream \
--batch-size 500 \
--policy replace| Column | Type | Description |
|---|---|---|
sequence_number |
UInt64 |
Globally unique monotonic FIE identifier |
agent_id |
IPv6 |
Probing agent address |
probing_directive_id |
UInt32 |
Directive that triggered this FIE |
ip_version |
UInt8 |
IP version: 4 or 6 |
protocol |
UInt8 |
Probe protocol (ICMP=1, UDP=17, ICMPv6=58) |
source_address |
IPv6 |
Probe source address |
destination_address |
IPv6 |
Probe destination address |
near_probe_ttl |
UInt8 |
TTL of the near hop |
near_reply_address |
IPv6 |
Reply address at near TTL |
near_sent_timestamp |
DateTime |
Probe send time at near TTL |
near_received_timestamp |
DateTime |
Reply receive time at near TTL |
far_probe_ttl |
UInt8 |
TTL of the far hop |
far_reply_address |
IPv6 |
Reply address at far TTL |
far_sent_timestamp |
DateTime |
Probe send time at far TTL |
far_received_timestamp |
DateTime |
Reply receive time at far TTL |
production_timestamp |
DateTime |
Time at which this FIE was produced |
The table is ordered by (near_reply_address, destination_address, agent_id, production_timestamp), matching the mp compute fies output schema and making the two sources directly interchangeable for downstream queries.
Computes Forwarding Info Elements (FIEs) from a raw Iris results table and writes them into a destination ClickHouse table. The computation runs entirely server-side within ClickHouse using keyset-paginated chunks, with no data movement through the client.
A FIE represents an observed forwarding step between two consecutive TTL hops h and h+1 within the same flow. Given a flow with probe TTL values h and h+1, a FIE captures the near router (at TTL h) and the far router (at TTL h+1) along with their associated timestamps and reply addresses. The set of included FIEs is controlled by two independent filtering policies: a cardinality policy (how many distinct reply addresses are allowed per hop) and a nullity policy (whether hops with a missing far reply are included). The default behavior (--cardinality one_to_one --nullity both_some) preserves the original semantics: only flows where each hop has exactly one distinct reply address are included.
Both results and resultslite source schemas are supported. The appropriate computation template is selected automatically based on the source table's schema.
| Flag | Default | Description |
|---|---|---|
--policy |
append |
Write policy: replace, truncate, fail, append |
--chunk-size |
1000000 |
Number of destination prefixes per chunk |
--rtt-resolution |
0.1 |
RTT resolution in milliseconds (Iris default: 0.1) |
--cardinality |
one_to_one |
Cardinality policy: one_to_one, many_to_one, one_to_many, all |
--nullity |
both_some |
Nullity policy: both_some, far_none, any |
Two independent policies control which hop pairs are included in the output.
Cardinality policy — constrains the number of distinct reply addresses per hop:
| Value | Near replies | Far replies |
|---|---|---|
one_to_one |
exactly 1 | exactly 1 (or absent) |
many_to_one |
any | exactly 1 (or absent) |
one_to_many |
exactly 1 | any |
all |
any | any |
Nullity policy — controls whether hops with a missing or empty far reply are included:
| Value | Behavior |
|---|---|
both_some |
Far hop must have at least one reply address |
far_none |
Far hop must have no reply address (missing h+1 hop) |
any |
Far hop presence is not constrained |
Note: combining far_none with one_to_one or many_to_one is rejected as contradictory — those cardinality policies require the far side to be non-empty, which conflicts with far_none.
When the cardinality policy is one_to_one or many_to_one and the nullity policy allows a missing far hop (any), the cardinality constraint is applied tolerantly: a missing far hop (zero replies) is allowed through, and only non-empty far hops are constrained to have exactly one reply.
When cardinality is all and nullity is any, all hop pairs are included and the near and far reply arrays are fully exploded into a cartesian product of rows.
| Policy | Behaviour |
|---|---|
replace |
Drop destination table if it exists, recreate and insert |
truncate |
Truncate destination table if not empty, then insert |
fail |
Fail if destination table is not empty |
append |
Insert into destination regardless of existing data |
# Default behavior: one_to_one cardinality, both_some nullity (original semantics)
mp compute fies iris_resultslite__20260601 iris_fies__20260601
# Keep all pairs including missing far hops, one reply per hop
mp compute fies iris_resultslite__20260601 iris_fies__20260601 \
--cardinality one_to_one \
--nullity any
# Keep all pairs, full cartesian product on multi-reply hops
mp compute fies iris_resultslite__20260601 iris_fies__20260601 \
--cardinality all \
--nullity any
# Only pairs where far hop has no reply (missing h+1)
mp compute fies iris_resultslite__20260601 iris_fies__20260601 \
--cardinality one_to_many \
--nullity far_nonecomputing [results to fies]: mpat.iris_resultslite__20260601 -> mpat.iris_fies__20260601
[chunk 1] cursor=:: last=::ffff:1.199.230.219 rows=3384343 elapsed=3.5s total=3384343
[chunk 2] cursor=::ffff:1.199.230.219 last=::ffff:2.x.x.x rows=3291872 elapsed=3.2s total=6676215
...
done: 156 chunks, 696001505 rows, elapsed=16m38s
| Column | Type | Description |
|---|---|---|
sequence_number |
UInt64 |
Globally unique monotonic FIE identifier |
agent_id |
IPv6 |
Probing agent (source address) |
probing_directive_id |
UInt32 |
Always 0 for this computation |
ip_version |
UInt8 |
IP version: 4 or 6 |
protocol |
UInt8 |
Probe protocol (ICMP=1, UDP=17, ICMPv6=58) |
source_address |
IPv6 |
Probe source address |
destination_address |
IPv6 |
Probe destination address |
near_probe_ttl |
UInt8 |
TTL of the near hop h |
near_reply_address |
IPv6 |
Reply address at TTL h |
near_sent_timestamp |
DateTime |
Probe send time at TTL h |
near_received_timestamp |
DateTime |
Estimated reply receive time at TTL h |
far_probe_ttl |
UInt8 |
TTL of the far hop h+1 |
far_reply_address |
IPv6 |
Reply address at TTL h+1 |
far_sent_timestamp |
DateTime |
Probe send time at TTL h+1 |
far_received_timestamp |
DateTime |
Estimated reply receive time at TTL h+1 |
production_timestamp |
DateTime |
Time at which this FIE was computed |
The table is ordered by (near_reply_address, destination_address, agent_id, production_timestamp) for efficient queries grouping by forwarding hop and destination.
- Ufuk Bombar – Sorbonne Université / LINCS · contact@bombar.dev
MIT License
