From 8e1614ae10ef24376bcea6b5c5da17c13f34a4ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arturo=20Filast=C3=B2?= Date: Wed, 5 Aug 2026 22:24:32 +0200 Subject: [PATCH 1/3] oonirun: specify configuration model, target identity and attribution Three additions and a set of corrections to the OONI Run v2 spec, motivated by the plan to rebuild the instant-messaging cards as links composed from web_connectivity building blocks, and by the data pipeline's requirements on provenance and series stability. Configuration model (3.1). Reinstates the nettest-level `options` object alongside the per-input `inputs_extra` overlay, with defined merge semantics: engine defaults, then options, then inputs_extra[i], shallow merge. Without a nettest-level object, input-less nettests had no configuration surface at all and shared settings had to be duplicated into every inputs_extra entry. Backends validate option names at CREATE so a typo fails loudly instead of silently doing nothing in the field; probes ignore unknown names so old probes keep working. The safe_ prefix convention, which the spec used but never documented, is now stated together with its scrubbing rule. The effective per-input configuration is recorded in the measurement, so analysis can condition on what actually ran. Target identity (3.2). Inputs are addresses and may rotate freely between revisions; targets are durable names, expressed as service roles and never as addresses, kept in a shared append-only registry. Inputs sharing a target_id form a redundant pool; the breaks_service flag marks a target whose blocking breaks the whole service for the user, the way WhatsApp registration does regardless of the chat pool. One declaration drives the probe-side card status and the pipeline-side aggregation, which is what lets an IM card decompose into web_connectivity runs without collapsing into per-hostname series that break at every infrastructure rename. Measurement attribution (5.0). Probes annotate every measurement with the link id, the revision and a per-run attempt id, which is what scopes aggregates to a campaign and reconstructs a composed link as one logical check. Consent is defined to bind to revisions rather than resolutions: a volunteer consents to the named list, not to the specific inputs it yields on a given day, so dynamic-list churn no longer re-prompts while every definition change still does. Resolutions themselves are ephemeral by design, since retaining every prioritization output would grow without bound; the measurements submitted under one attempt are the durable record of what was served, and the spec states the accepted tradeoff that the unmeasured remainder is not reconstructible. Corrections: duplicate 4.4 heading renumbered (engine descriptor is now 4.5, LIST is 4.6, and it is titled POST, which is what it specifies); stale v1 API path and run.ooni.io addresses updated; expired/archived terminology unified; User-Agent field list typo fixed. --- backends/bk-005-ooni-run-v2.md | 219 +++++++++++++++++++++++++++++---- 1 file changed, 194 insertions(+), 25 deletions(-) diff --git a/backends/bk-005-ooni-run-v2.md b/backends/bk-005-ooni-run-v2.md index 8cd14043..c96bcc6c 100644 --- a/backends/bk-005-ooni-run-v2.md +++ b/backends/bk-005-ooni-run-v2.md @@ -1,7 +1,7 @@ # OONI Run v2 specification - author: Norbel Ambanumben, Arturo Filastò -- version: 2024.02.23 +- version: 2026.08.05 - status: release-candidate This document provides a functional specification for OONI Run. @@ -36,19 +36,19 @@ sequenceDiagram actor Volunteer CampaignOrganizer->>OONIAPI: Create OONI Run link to run web_connectivity with http://example.com - OONIAPI->>CampaignOrganizer: OONI Run link: https://run.ooni.io/v2/deadbeef - CampaignOrganizer-->>Volunteer: Hey, can you open the link https://run.ooni.io/v2/deadbeef with OONI Probe? + OONIAPI->>CampaignOrganizer: OONI Run link: https://run.ooni.org/v2/deadbeef + CampaignOrganizer-->>Volunteer: Hey, can you open the link https://run.ooni.org/v2/deadbeef with OONI Probe? Volunteer-)OONIAPI: What is the descriptor for OONI Run link with ID "deadbeef" OONIAPI->>Volunteer: Here is the descriptor for "deadbeef" ``` It's important to notice how, in the event that the user has the OONI Probe app -installed, a web request to `https://run.ooni.org/io/v2/deadbeef` will never be +installed, a web request to `https://run.ooni.org/v2/deadbeef` will never be issued on the network, but rather the metadata encoded in the URL itself is used to retrieve the OONI Run descriptor from a different OONI API endpoint. When a Volunteer taps on an OONI Run link (in the above example -`https://run.ooni.io/v2/deadbeef`) the OONI Probe app is opened and a they are +`https://run.ooni.org/v2/deadbeef`) the OONI Probe app is opened and a they are presented with the metadata of the OONI Run link as well as the nettests it is configured with. At this point, assuming they feel confortable with running the nettests they see @@ -73,7 +73,22 @@ such as TLS or onion services. Whenever a change is made to an OONI Run link, it's important that the end user is informed about them and the OONI Run link is disabled until they agree with -them. +them. Consent binds to the link *revision*: a new revision (any change to the +metadata, the nettests, their configuration or their `targets_name`) requires +renewed consent, while a new *resolution* of a dynamic target list (see +4.5) does not, because the volunteer consented to the named list, not to the +specific inputs it yielded on a given day. Probes MUST therefore detect changes +by comparing revisions, never by comparing resolved descriptors. + +Nettest configuration options whose name starts with `safe_` MAY carry secrets +(for example VPN credentials). Probes MUST make these values available to the +experiment but MUST NOT serialize them into the submitted measurement. + +Since the default OONI Probe cards are themselves OONI Run links, a malicious +or compromised revision could redirect a large probe population against +arbitrary endpoints. Stock links MUST be authored by OONI and validated at +creation and update time against the shared target registry (see 3.2); the +revision consent mechanism above applies to them like to any other link. It is outside of the current scope of this document to prescribe if and how some level of blocking resistance should be implemented or provided by the system. @@ -120,7 +135,7 @@ An OONI Run link descriptor is a JSON file with the following semantics: "author": "(optional) `string` name of the creator of this OONI Run link", - "is_expired": "(optional) `bool` a boolean flag used to indicate if this OONI Run link is expired. When an OONI Run link is archived, it does not run", + "is_expired": "(optional) `bool` flag indicating that this OONI Run link is expired. An expired link remains retrievable, but it does not lead to tests being initiated", // `array` provides a JSON array of tests to be run. "nettests":[{ @@ -131,9 +146,19 @@ An OONI Run link descriptor is a JSON file with the following semantics: "https://ooni.org/" ], + // (optional) `map` of default configuration options for this nettest, + // applied to every input. For nettests that take no input this is the only + // configuration surface. See section 3.1 for the configuration model. + "options": { + "http3_enabled": false + }, + // (optional) `array` provides a richer JSON array containing extra parameters for each input. - // If provided, the length of inputs_extra should match the length of inputs. + // If provided, the length of inputs_extra MUST match the length of inputs. + // Each entry is overlaid on top of `options` for its input (section 3.1) + // and MAY carry the reserved target-identity keys of section 3.2. "inputs_extra": [{ + "target_id": "example/website", "category_code": "HUMR", }], @@ -180,6 +205,108 @@ card. The OONI Run descriptor, as specified from the link creator, is saying application will then receive a prioritized and sorted list which will change every time a new run is performed. +## 3.1 Configuration model + +A nettest is configured through two levels of the same mechanism: + +* `options` (optional) is a JSON object of configuration options applied to + every input of the nettest. For nettests that take no input, it is the only + configuration surface. + +* `inputs_extra` (optional) is an array of JSON objects, index-aligned with + `inputs`, whose entries are overlaid on top of `options` for the + corresponding input. + +The effective configuration for input *i* is computed by a shallow, field-wise +merge: engine defaults, then `options`, then `inputs_extra[i]`, with later +values winning. Nested objects are replaced, not merged. + +Option names and their meaning are defined by each nettest. The following +rules keep authoring mistakes loud and probes forward-compatible: + +* Backends MUST reject at CREATE and UPDATE time (with a `4xx`) any option + name that is not known for the declared `test_name`, whether it appears in + `options` or in an `inputs_extra` entry. The reserved keys of section 3.2 + are exempt. Without this check, a misspelled option silently does nothing + in the field. + +* Probes MUST ignore option names they do not recognize, so that older probes + keep working when new options are introduced. + +* Option names starting with `safe_` MAY carry secrets and are subject to the + scrubbing rule of section 2.0. + +* Probes MUST record the effective per-input configuration — after the merge, + excluding `safe_` options — in the submitted measurement, so that data + analysis can condition on what actually ran. + +## 3.2 Target identity + +An input is an *address*: a URL, hostname or IP endpoint that may rotate +freely between revisions as infrastructure changes. A *target* is the durable +name of the thing being measured. Keeping the two distinct is what allows +measurement series to stay longitudinally comparable while the addresses +underneath them churn. + +The following `inputs_extra` keys are reserved across all nettests. They are +consumed by probes and by the data pipeline, are never passed to the +experiment as options, and experiments MUST NOT define options with these +names: + +* `target_id` (string, optional): the durable name of the target this input + belongs to, expressed as a service role (ex. `signal/chat`, + `whatsapp/endpoints`), never as an address. Multiple inputs sharing one + `target_id` form a *pool* of redundant members. Distinct `target_id`s + within one link describe distinct components of a larger service. + +* `breaks_service` (bool, optional, defaults to `false`): when true, this + *target* being down or blocked means the overall service is broken for the + user, regardless of the state of the other targets. Although it is written + per input, the flag is a property of the target, which is why all entries + sharing a `target_id` MUST agree on it. + +* `category_code` (string, optional): display metadata following the + [Citizen Lab category codes](https://github.com/citizenlab/test-lists). + It is informational only: data analysis derives categorization from its own + reference data, and this field is not authoritative for it. + +The state of a composed link is evaluated in two steps, inputs to targets and +targets to service: + +1. A target is reachable if any one of its member inputs is reachable, and it + is down or blocked only when every member fails. A pool is therefore "one + working member is enough" by construction. + +2. The service is broken when any target with `breaks_service: true` is down + or blocked. Failures of targets without the flag mean the service is + degraded rather than broken; how to render degradation is left to the + probe. + +Both classic shapes fall out of this without further vocabulary. Telegram's +datacentre pool is many inputs sharing `telegram/dc_pool`, each with +`breaks_service: true`: reaching a single datacentre means Telegram works, +and only losing all of them breaks it. WhatsApp's registration endpoint is a +single-member target with `breaks_service: true`, which breaks the service on +its own even while the chat pool stays reachable. + +The `target_id` vocabulary is maintained in a shared, versioned target +registry, in the same way test lists and blockpage fingerprints are maintained +as community reference data. Within the registry, target ids are append-only: +new ids may be added and old ones deprecated, but an id is never renamed or +re-pointed at a different service role, because measurement series key on it. +A revision that only rotates the addresses under stable `target_id`s changes +what probes contact without changing the identity of what is measured. + +Backends MUST validate stock links (the OONI-authored links implementing the +default OONI Probe cards) against the registry at CREATE and UPDATE time, +rejecting unknown `target_id`s. For other links the registry SHOULD be used to +warn rather than reject, since campaign authors may legitimately measure +services the registry does not describe yet. + +Probes MAY use `target_id` and `breaks_service` to compute and display the +outcome of a composed link, for example rendering a single card status for a +messaging app whose link measures several pools and services. + Based on the above specification it would be possible to re-implement the cards for the OONI Probe dashboard as follows. @@ -402,7 +529,9 @@ corresponding to an entry in the `inputs` list. This allows you to attach additional metadata to each input. The `targets_name` field specifies the name of a predefined target list that will be used to dynamically generate the inputs list. This name must be recognized by the backend and agreed upon in advance -between the link creator and the backend system. +between the link creator and the backend system. The semantics of `options` and +`inputs_extra` are specified in section 3.1, and the reserved target-identity +keys in section 3.2. ### Response status code @@ -412,7 +541,13 @@ Upon receiving a request to create a link, the API will respond: any required field is missing and/or if any present field has an invalid value. In particular, note that it will error when `targets_name` and `inputs` are provided at the same time in any nettest -2. if everything is okay, MUST return a `200` response. +2. MUST fail with `4xx` if `inputs_extra` is present and its length does not + match the length of `inputs`; if `options` or any `inputs_extra` entry + contains an option name unknown for the declared `test_name` (section 3.1); + if entries sharing a `target_id` disagree on `breaks_service` (section 3.2); or, + for stock links, if a `target_id` is unknown to the target registry. + +3. if everything is okay, MUST return a `200` response. ### Response body @@ -544,8 +679,8 @@ following JSON body: } ``` -Note: This endpoint does not compute dynamic test lists. As a result, -nettests with `target_name` will always have an empty `inputs` field. +Note: This endpoint does not compute dynamic test lists. As a result, +nettests with `targets_name` will always have an empty `inputs` field. ## 4.4 GET the OONI Run full descriptor by revision @@ -572,7 +707,7 @@ Same as 4.3 GET the OONI Run descriptor When the specified OONI Run link contains dynamic targets, the `inputs` list may contain different targets. -## 4.4 GET the OONI Run engine descriptor revision +## 4.5 POST the OONI Run engine descriptor This operation is performed by OONI Probe clients to retrieve the engine descriptor of a certain OONI Run link given the ID and revision @@ -614,7 +749,7 @@ properly generate dynamic target lists: * `X-OONI-Credentials`: base64 encoded OONI anonymous credentials -The `platform`, `software_name`, `software_name`, `engine_name` and +The `platform`, `software_name`, `software_version`, `engine_name` and `engine_version` are encoded inside of the `User-Agent` string using the following format: ``` @@ -629,7 +764,7 @@ following JSON body: ```JavaScript { "revision": "1", - "date_created": "" + "date_created": "", "nettests": [ { // See CREATE response format for other fields @@ -644,12 +779,21 @@ following JSON body: ] } ``` -Note: While nettests can't include both `inputs` and `target_name` during creation, -this endpoint may show both since the backend dynamically populates -`inputs` based on `target_name`. +Note: While nettests can't include both `inputs` and `targets_name` during creation, +this endpoint may show both since the backend dynamically populates +`inputs` based on `targets_name`. The backend computes dynamic test lists only for this request. Other requests will return an empty `inputs` list. +Resolutions are ephemeral by design: they are the output of the +prioritization system, and retaining every served list would grow without +bound. The durable record of what a probe did is the measurements it +submitted, which carry the link id, revision and attempt id (see 5.0). The +tradeoff this accepts is that the *unmeasured* remainder of a served list is +not reconstructible after the fact; questions about why a target went +unmeasured are answered from the prioritization system's own configuration +and rules, not from a log of individual resolutions. + Additionally, the `Vary` header should specify the list of headers that affect the response body caching, which are all headers starting with the `X-OONI-` prefix. @@ -657,7 +801,7 @@ prefix. The server might also return an updated version of the submitted anonymous credentials using the `X-OONI-Credentials` header. -## 4.7 LIST the OONI Run descriptors +## 4.6 LIST the OONI Run descriptors This operation is performed by users of the OONI Run platform to list all the existing OONI Run links. @@ -670,7 +814,7 @@ To retrieve an OONI Run link descriptor, the client issues a request compliant w `GET /api/v2/oonirun/links?is_mine=true&is_expired=true` - `is_mine` , boolean flag to filter only the links of the logged in user. Will only work when the Authentication header is used. -- `is_expired` , boolean flag used to indicate if the listing should include archived links as well. +- `is_expired` , boolean flag used to indicate if the listing should include expired links as well. ### Response status code @@ -695,7 +839,26 @@ following JSON body: } ``` -# 5.0 Implementation considerations +# 5.0 Measurement attribution + +Probes MUST annotate every measurement produced while running an OONI Run link +with the following annotations: + +| annotation | value | +| --- | --- | +| `ooni_run_link_id` | the OONI Run link id | +| `ooni_run_link_revision` | the revision of the descriptor that was run | +| `ooni_run_attempt` | a random UUID minted once per link run and shared by all measurements produced by that run | + +This is what ties measurements back to the campaign that produced them. It is +what allows aggregate results to be scoped to a link ("what did the volunteers +of this campaign find") and a link composing several nettests to be +reconstructed after the fact as a single logical check. For dynamically +generated target lists, the measurements submitted under one +`ooni_run_attempt` are also the record of what the resolution served, up to +the inputs the probe did not reach (see 4.5). + +# 6.0 Implementation considerations Special attention should be placed in ensuring the OONI Run links (which are mobile deep links) are sharable though various apps. @@ -709,7 +872,7 @@ numbers + spaces or dashes to make it easier to type. Mobile deep links can be registered using two different methods, one is a custom prefix (ex. `ooni://`), the other is a custom URL prefix (ex. -`https://run.ooni.io/1234`). In our testing we have seen that the custom prefix +`https://run.ooni.org/v2/1234`). In our testing we have seen that the custom prefix is more reliable, yet it has the tradeoff of not allowing us to display a web page when the user does not have the app installed. As such the recommended strategy is to encourage users to share the custom URL prefix OONI Run link, but @@ -719,13 +882,19 @@ have a link to the custom prefix approach to "force" the opening of the app As such we recommend using the following addresses for OONI Run link and OONI Run descriptor URLs: -* `https://run.ooni.io/{ooni_run_link_id}`, where `{ooni_run_link_id}` is a number +* `https://run.ooni.org/v2/{ooni_run_link_id}`, where `{ooni_run_link_id}` is a number * `ooni://runv2/{ooni_run_link_id}` -* `https://api.ooni.io/api/v1/ooni_run/{ooni_run_link_id}` +* `https://api.ooni.io/api/v2/oonirun/links/{ooni_run_link_id}` + +# 7.0 Future work -# 6.0 Future work +The combination semantics of section 3.2 are deliberately limited to pools +plus the `breaks_service` flag. If a real service ever needs more, such as a +k-of-n threshold over a pool or a service that works when either of two +distinct targets does, that is the trigger for a richer combination grammar; +until then, the two-step model stays. We could at some point host these links on s3 or github and have them be accessible via URLs in the form: From f157252ce926671acf1d57b2ea99315f4d253d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arturo=20Filast=C3=B2?= Date: Wed, 5 Aug 2026 22:24:32 +0200 Subject: [PATCH 2/3] oonirun: specify configuration model, target identity and attribution Additions and corrections to the OONI Run v2 spec, motivated by the plan to rebuild the instant-messaging cards as links composed from web_connectivity building blocks, and by the data pipeline's requirements on provenance and series stability. Configuration model (3.1). Reinstates the nettest-level `options` object alongside the per-input `inputs_extra` overlay, with defined merge semantics: engine defaults, then options, then inputs_extra[i], shallow merge. Without a nettest-level object, input-less nettests had no configuration surface at all and shared settings had to be duplicated into every inputs_extra entry. Backends validate option names at CREATE so a typo fails loudly instead of silently doing nothing in the field; probes ignore unknown names so old probes keep working. The safe_ prefix convention, which the spec used but never documented, is now stated together with its scrubbing rule. The effective per-input configuration is recorded in the measurement, so analysis can condition on what actually ran. Target identity (3.2). Inputs are addresses and may rotate freely between revisions; targets are durable names, expressed as service roles and never as addresses, kept in a shared append-only registry. Inputs sharing a target_id form a redundant pool; the breaks_service flag marks a target whose blocking breaks the whole service for the user. The state of a composed link is evaluated in two explicit steps: a target is down only when every pool member fails, and the service is broken when any breaks_service target is down. One declaration drives the probe-side card status and the pipeline-side aggregation, which is what lets an IM card decompose into web_connectivity runs without collapsing into per-hostname series that break at every infrastructure rename. Dynamic target lists (3.3). targets_name names a backend-generated input list and shares its namespace with the target registry: the stock Websites card uses citizenlab/test_lists, while a registry target_id (ex. whatsapp/endpoints) expands to the target's current members with the identity keys stamped on the served entries. A composed IM link is therefore written entirely without addresses, and pool rotation happens in the registry as a new resolution, with no link revision and no renewed consent. Names are owned and validated by the backend; probes treat them as opaque and never validate against a hardcoded list, so new names ship without a client update. Measurement attribution (5.0). Probes annotate every measurement with the link id, the revision and a per-run attempt id, which is what scopes aggregates to a campaign and reconstructs a composed link as one logical check. Consent is defined to bind to revisions rather than resolutions: a volunteer consents to the named list, not to the specific inputs it yields on a given day, so dynamic-list churn no longer re-prompts while every definition change still does. Resolutions themselves are ephemeral by design, since retaining every prioritization output would grow without bound; the measurements submitted under one attempt are the durable record of what was served, and the spec states the accepted tradeoff that the unmeasured remainder is not reconstructible. Corrections: duplicate 4.4 heading renumbered (engine descriptor is now 4.5, LIST is 4.6, and it is titled POST, which is what it specifies); stale v1 API path and run.ooni.io addresses updated; expired/archived terminology unified; User-Agent field list typo fixed. --- backends/bk-005-ooni-run-v2.md | 283 ++++++++++++++++++++++++++++----- 1 file changed, 239 insertions(+), 44 deletions(-) diff --git a/backends/bk-005-ooni-run-v2.md b/backends/bk-005-ooni-run-v2.md index 8cd14043..0b8f8761 100644 --- a/backends/bk-005-ooni-run-v2.md +++ b/backends/bk-005-ooni-run-v2.md @@ -1,7 +1,7 @@ # OONI Run v2 specification - author: Norbel Ambanumben, Arturo Filastò -- version: 2024.02.23 +- version: 2026.08.05 - status: release-candidate This document provides a functional specification for OONI Run. @@ -36,19 +36,19 @@ sequenceDiagram actor Volunteer CampaignOrganizer->>OONIAPI: Create OONI Run link to run web_connectivity with http://example.com - OONIAPI->>CampaignOrganizer: OONI Run link: https://run.ooni.io/v2/deadbeef - CampaignOrganizer-->>Volunteer: Hey, can you open the link https://run.ooni.io/v2/deadbeef with OONI Probe? + OONIAPI->>CampaignOrganizer: OONI Run link: https://run.ooni.org/v2/deadbeef + CampaignOrganizer-->>Volunteer: Hey, can you open the link https://run.ooni.org/v2/deadbeef with OONI Probe? Volunteer-)OONIAPI: What is the descriptor for OONI Run link with ID "deadbeef" OONIAPI->>Volunteer: Here is the descriptor for "deadbeef" ``` It's important to notice how, in the event that the user has the OONI Probe app -installed, a web request to `https://run.ooni.org/io/v2/deadbeef` will never be +installed, a web request to `https://run.ooni.org/v2/deadbeef` will never be issued on the network, but rather the metadata encoded in the URL itself is used to retrieve the OONI Run descriptor from a different OONI API endpoint. When a Volunteer taps on an OONI Run link (in the above example -`https://run.ooni.io/v2/deadbeef`) the OONI Probe app is opened and a they are +`https://run.ooni.org/v2/deadbeef`) the OONI Probe app is opened and a they are presented with the metadata of the OONI Run link as well as the nettests it is configured with. At this point, assuming they feel confortable with running the nettests they see @@ -73,7 +73,22 @@ such as TLS or onion services. Whenever a change is made to an OONI Run link, it's important that the end user is informed about them and the OONI Run link is disabled until they agree with -them. +them. Consent binds to the link *revision*: a new revision (any change to the +metadata, the nettests, their configuration or their `targets_name`) requires +renewed consent, while a new *resolution* of a dynamic target list (see +4.5) does not, because the volunteer consented to the named list, not to the +specific inputs it yielded on a given day. Probes MUST therefore detect changes +by comparing revisions, never by comparing resolved descriptors. + +Nettest configuration options whose name starts with `safe_` MAY carry secrets +(for example VPN credentials). Probes MUST make these values available to the +experiment but MUST NOT serialize them into the submitted measurement. + +Since the default OONI Probe cards are themselves OONI Run links, a malicious +or compromised revision could redirect a large probe population against +arbitrary endpoints. Stock links MUST be authored by OONI and validated at +creation and update time against the shared target registry (see 3.2); the +revision consent mechanism above applies to them like to any other link. It is outside of the current scope of this document to prescribe if and how some level of blocking resistance should be implemented or provided by the system. @@ -120,7 +135,7 @@ An OONI Run link descriptor is a JSON file with the following semantics: "author": "(optional) `string` name of the creator of this OONI Run link", - "is_expired": "(optional) `bool` a boolean flag used to indicate if this OONI Run link is expired. When an OONI Run link is archived, it does not run", + "is_expired": "(optional) `bool` flag indicating that this OONI Run link is expired. An expired link remains retrievable, but it does not lead to tests being initiated", // `array` provides a JSON array of tests to be run. "nettests":[{ @@ -131,14 +146,27 @@ An OONI Run link descriptor is a JSON file with the following semantics: "https://ooni.org/" ], + // (optional) `map` of default configuration options for this nettest, + // applied to every input. For nettests that take no input this is the only + // configuration surface. See section 3.1 for the configuration model. + "options": { + "http3_enabled": false + }, + // (optional) `array` provides a richer JSON array containing extra parameters for each input. - // If provided, the length of inputs_extra should match the length of inputs. + // If provided, the length of inputs_extra MUST match the length of inputs. + // Each entry is overlaid on top of `options` for its input (section 3.1) + // and MAY carry the reserved target-identity keys of section 3.2. "inputs_extra": [{ + "target_id": "example/website", "category_code": "HUMR", }], - // (optional) string used to specify during creation that the input list should be dynamically generated. The semantics of each string is up to the backend implementation. - "targets_name": "websites_list_prioritized", + // (optional) `string` naming a backend-generated dynamic input list (see + // section 3.3). Names are owned and validated by the backend; probes MUST + // treat them as opaque and MUST NOT validate them against any hardcoded + // list, so that new names can be introduced without a client update. + "targets_name": "citizenlab/test_lists", // (optional) `bool` indicates if this test should be run as part of autoruns. Defaults to true. // @@ -180,6 +208,140 @@ card. The OONI Run descriptor, as specified from the link creator, is saying application will then receive a prioritized and sorted list which will change every time a new run is performed. +## 3.1 Configuration model + +A nettest is configured through two levels of the same mechanism: + +* `options` (optional) is a JSON object of configuration options applied to + every input of the nettest. For nettests that take no input, it is the only + configuration surface. + +* `inputs_extra` (optional) is an array of JSON objects, index-aligned with + `inputs`, whose entries are overlaid on top of `options` for the + corresponding input. + +The effective configuration for input *i* is computed by a shallow, field-wise +merge: engine defaults, then `options`, then `inputs_extra[i]`, with later +values winning. Nested objects are replaced, not merged. + +Option names and their meaning are defined by each nettest. The following +rules keep authoring mistakes loud and probes forward-compatible: + +* Backends MUST reject at CREATE and UPDATE time (with a `4xx`) any option + name that is not known for the declared `test_name`, whether it appears in + `options` or in an `inputs_extra` entry. The reserved keys of section 3.2 + are exempt. Without this check, a misspelled option silently does nothing + in the field. + +* Probes MUST ignore option names they do not recognize, so that older probes + keep working when new options are introduced. + +* Option names starting with `safe_` MAY carry secrets and are subject to the + scrubbing rule of section 2.0. + +* Probes MUST record the effective per-input configuration — after the merge, + excluding `safe_` options — in the submitted measurement, so that data + analysis can condition on what actually ran. + +## 3.2 Target identity + +An input is an *address*: a URL, hostname or IP endpoint that may rotate +freely between revisions as infrastructure changes. A *target* is the durable +name of the thing being measured. Keeping the two distinct is what allows +measurement series to stay longitudinally comparable while the addresses +underneath them churn. + +The following `inputs_extra` keys are reserved across all nettests. They are +consumed by probes and by the data pipeline, are never passed to the +experiment as options, and experiments MUST NOT define options with these +names: + +* `target_id` (string, optional): the durable name of the target this input + belongs to, expressed as a service role (ex. `signal/chat`, + `whatsapp/endpoints`), never as an address. Multiple inputs sharing one + `target_id` form a *pool* of redundant members. Distinct `target_id`s + within one link describe distinct components of a larger service. + +* `breaks_service` (bool, optional, defaults to `false`): when true, this + *target* being down or blocked means the overall service is broken for the + user, regardless of the state of the other targets. Although it is written + per input, the flag is a property of the target, which is why all entries + sharing a `target_id` MUST agree on it. + +* `category_code` (string, optional): display metadata following the + [Citizen Lab category codes](https://github.com/citizenlab/test-lists). + It is informational only: data analysis derives categorization from its own + reference data, and this field is not authoritative for it. + +The state of a composed link is evaluated in two steps, inputs to targets and +targets to service: + +1. A target is reachable if any one of its member inputs is reachable, and it + is down or blocked only when every member fails. A pool is therefore "one + working member is enough" by construction. + +2. The service is broken when any target with `breaks_service: true` is down + or blocked. Failures of targets without the flag mean the service is + degraded rather than broken; how to render degradation is left to the + probe. + +Both classic shapes fall out of this without further vocabulary. Telegram's +datacentre pool is many inputs sharing `telegram/dc_pool`, each with +`breaks_service: true`: reaching a single datacentre means Telegram works, +and only losing all of them breaks it. WhatsApp's registration endpoint is a +single-member target with `breaks_service: true`, which breaks the service on +its own even while the chat pool stays reachable. + +The `target_id` vocabulary is maintained in a shared, versioned target +registry, in the same way test lists and blockpage fingerprints are maintained +as community reference data. Within the registry, target ids are append-only: +new ids may be added and old ones deprecated, but an id is never renamed or +re-pointed at a different service role, because measurement series key on it. +A revision that only rotates the addresses under stable `target_id`s changes +what probes contact without changing the identity of what is measured. + +Backends MUST validate stock links (the OONI-authored links implementing the +default OONI Probe cards) against the registry at CREATE and UPDATE time, +rejecting unknown `target_id`s. For other links the registry SHOULD be used to +warn rather than reject, since campaign authors may legitimately measure +services the registry does not describe yet. + +Probes MAY use `target_id` and `breaks_service` to compute and display the +outcome of a composed link, for example rendering a single card status for a +messaging app whose link measures several pools and services. + +## 3.3 Dynamic target lists + +`targets_name` names an input list that the backend generates at resolution +time (see 4.5) instead of the list being written into the descriptor. Two +namespaces are defined today: + +* `citizenlab/test_lists`: the community test lists, prioritized and sorted + by the backend for the requesting probe. This is what the stock Websites + card uses. + +* any `target_id` from the target registry (section 3.2): the backend expands + it to the target's current member addresses and stamps each served input's + `inputs_extra` entry with the corresponding target-identity keys. A + composed link can therefore be written entirely without addresses: + + ```JSON + { + "test_name": "web_connectivity", + "targets_name": "whatsapp/endpoints" + } + ``` + + Rotating a pool's membership then happens in the registry and reaches + probes as a new resolution, without creating a link revision and without + triggering renewed consent (see 2.0). + +Names are owned and validated by the backend: a CREATE or UPDATE naming an +unknown `targets_name` fails with a `4xx` (see 4.1). Probes MUST treat the +field as opaque and MUST NOT validate it against any hardcoded list, so that +new names, including newly registered targets, can be introduced without a +client update. + Based on the above specification it would be possible to re-implement the cards for the OONI Probe dashboard as follows. @@ -195,21 +357,9 @@ dashboard as follows. "nettests": [ { - "inputs": [ - "https://example.com/" - ], - "inputs_extra": [{ - "category_code": "HUMR", - }], - "is_manual_run_enabled": true, - "is_background_run_enabled": false, - "test_name": "web_connectivity" - }, - { - "is_manual_run_enabled": false, - "is_background_run_enabled": true, + "targets_name": "citizenlab/test_lists", "test_name": "web_connectivity" - }, + } ] } ``` @@ -399,10 +549,10 @@ request conforming to the following: The `inputs_extra` field should be a list of JSON objects, with each object corresponding to an entry in the `inputs` list. This allows you to attach -additional metadata to each input. The `targets_name` field specifies the name -of a predefined target list that will be used to dynamically generate the inputs -list. This name must be recognized by the backend and agreed upon in advance -between the link creator and the backend system. +additional metadata to each input. The `targets_name` field names a +backend-generated dynamic input list, as specified in section 3.3. The +semantics of `options` and `inputs_extra` are specified in section 3.1, and +the reserved target-identity keys in section 3.2. ### Response status code @@ -412,7 +562,14 @@ Upon receiving a request to create a link, the API will respond: any required field is missing and/or if any present field has an invalid value. In particular, note that it will error when `targets_name` and `inputs` are provided at the same time in any nettest -2. if everything is okay, MUST return a `200` response. +2. MUST fail with `4xx` if `inputs_extra` is present and its length does not + match the length of `inputs`; if `options` or any `inputs_extra` entry + contains an option name unknown for the declared `test_name` (section 3.1); + if entries sharing a `target_id` disagree on `breaks_service` (section 3.2); + if `targets_name` is not a name the backend recognizes (section 3.3); or, + for stock links, if a `target_id` is unknown to the target registry. + +3. if everything is okay, MUST return a `200` response. ### Response body @@ -544,8 +701,8 @@ following JSON body: } ``` -Note: This endpoint does not compute dynamic test lists. As a result, -nettests with `target_name` will always have an empty `inputs` field. +Note: This endpoint does not compute dynamic test lists. As a result, +nettests with `targets_name` will always have an empty `inputs` field. ## 4.4 GET the OONI Run full descriptor by revision @@ -572,7 +729,7 @@ Same as 4.3 GET the OONI Run descriptor When the specified OONI Run link contains dynamic targets, the `inputs` list may contain different targets. -## 4.4 GET the OONI Run engine descriptor revision +## 4.5 POST the OONI Run engine descriptor This operation is performed by OONI Probe clients to retrieve the engine descriptor of a certain OONI Run link given the ID and revision @@ -614,7 +771,7 @@ properly generate dynamic target lists: * `X-OONI-Credentials`: base64 encoded OONI anonymous credentials -The `platform`, `software_name`, `software_name`, `engine_name` and +The `platform`, `software_name`, `software_version`, `engine_name` and `engine_version` are encoded inside of the `User-Agent` string using the following format: ``` @@ -629,7 +786,7 @@ following JSON body: ```JavaScript { "revision": "1", - "date_created": "" + "date_created": "", "nettests": [ { // See CREATE response format for other fields @@ -644,12 +801,25 @@ following JSON body: ] } ``` -Note: While nettests can't include both `inputs` and `target_name` during creation, -this endpoint may show both since the backend dynamically populates -`inputs` based on `target_name`. +Note: While nettests can't include both `inputs` and `targets_name` during creation, +this endpoint may show both since the backend dynamically populates +`inputs` based on `targets_name`. The backend computes dynamic test lists only for this request. Other requests will return an empty `inputs` list. +When `targets_name` names a registry `target_id` (section 3.3), the served +`inputs_extra` entries carry the target-identity keys of section 3.2, so the +resolved descriptor is self-describing. + +Resolutions are ephemeral by design: they are the output of the +prioritization system, and retaining every served list would grow without +bound. The durable record of what a probe did is the measurements it +submitted, which carry the link id, revision and attempt id (see 5.0). The +tradeoff this accepts is that the *unmeasured* remainder of a served list is +not reconstructible after the fact; questions about why a target went +unmeasured are answered from the prioritization system's own configuration +and rules, not from a log of individual resolutions. + Additionally, the `Vary` header should specify the list of headers that affect the response body caching, which are all headers starting with the `X-OONI-` prefix. @@ -657,7 +827,7 @@ prefix. The server might also return an updated version of the submitted anonymous credentials using the `X-OONI-Credentials` header. -## 4.7 LIST the OONI Run descriptors +## 4.6 LIST the OONI Run descriptors This operation is performed by users of the OONI Run platform to list all the existing OONI Run links. @@ -670,7 +840,7 @@ To retrieve an OONI Run link descriptor, the client issues a request compliant w `GET /api/v2/oonirun/links?is_mine=true&is_expired=true` - `is_mine` , boolean flag to filter only the links of the logged in user. Will only work when the Authentication header is used. -- `is_expired` , boolean flag used to indicate if the listing should include archived links as well. +- `is_expired` , boolean flag used to indicate if the listing should include expired links as well. ### Response status code @@ -695,7 +865,26 @@ following JSON body: } ``` -# 5.0 Implementation considerations +# 5.0 Measurement attribution + +Probes MUST annotate every measurement produced while running an OONI Run link +with the following annotations: + +| annotation | value | +| --- | --- | +| `ooni_run_link_id` | the OONI Run link id | +| `ooni_run_link_revision` | the revision of the descriptor that was run | +| `ooni_run_attempt` | a random UUID minted once per link run and shared by all measurements produced by that run | + +This is what ties measurements back to the campaign that produced them. It is +what allows aggregate results to be scoped to a link ("what did the volunteers +of this campaign find") and a link composing several nettests to be +reconstructed after the fact as a single logical check. For dynamically +generated target lists, the measurements submitted under one +`ooni_run_attempt` are also the record of what the resolution served, up to +the inputs the probe did not reach (see 4.5). + +# 6.0 Implementation considerations Special attention should be placed in ensuring the OONI Run links (which are mobile deep links) are sharable though various apps. @@ -709,7 +898,7 @@ numbers + spaces or dashes to make it easier to type. Mobile deep links can be registered using two different methods, one is a custom prefix (ex. `ooni://`), the other is a custom URL prefix (ex. -`https://run.ooni.io/1234`). In our testing we have seen that the custom prefix +`https://run.ooni.org/v2/1234`). In our testing we have seen that the custom prefix is more reliable, yet it has the tradeoff of not allowing us to display a web page when the user does not have the app installed. As such the recommended strategy is to encourage users to share the custom URL prefix OONI Run link, but @@ -719,13 +908,19 @@ have a link to the custom prefix approach to "force" the opening of the app As such we recommend using the following addresses for OONI Run link and OONI Run descriptor URLs: -* `https://run.ooni.io/{ooni_run_link_id}`, where `{ooni_run_link_id}` is a number +* `https://run.ooni.org/v2/{ooni_run_link_id}`, where `{ooni_run_link_id}` is a number * `ooni://runv2/{ooni_run_link_id}` -* `https://api.ooni.io/api/v1/ooni_run/{ooni_run_link_id}` +* `https://api.ooni.io/api/v2/oonirun/links/{ooni_run_link_id}` + +# 7.0 Future work -# 6.0 Future work +The combination semantics of section 3.2 are deliberately limited to pools +plus the `breaks_service` flag. If a real service ever needs more, such as a +k-of-n threshold over a pool or a service that works when either of two +distinct targets does, that is the trigger for a richer combination grammar; +until then, the two-step model stays. We could at some point host these links on s3 or github and have them be accessible via URLs in the form: From 454d5454673890a3880f957071befbbc2a5fc050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arturo=20Filast=C3=B2?= Date: Thu, 6 Aug 2026 13:12:55 +0200 Subject: [PATCH 3/3] Add further clarifications following the call --- backends/bk-005-ooni-run-v2.md | 50 +++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/backends/bk-005-ooni-run-v2.md b/backends/bk-005-ooni-run-v2.md index d8655186..5a7ec69c 100644 --- a/backends/bk-005-ooni-run-v2.md +++ b/backends/bk-005-ooni-run-v2.md @@ -143,7 +143,9 @@ An OONI Run link descriptor is a JSON file with the following semantics: // (optional) `array` provides a JSON array of inputs for the specified test. "inputs": [ "https://example.com/", - "https://ooni.org/" + "https://ooni.org/", + "https://ooni.io/", + "https://explorer.ooni.org/", ], // (optional) `map` of default configuration options for this nettest, @@ -160,6 +162,18 @@ An OONI Run link descriptor is a JSON file with the following semantics: "inputs_extra": [{ "target_id": "example/website", "category_code": "HUMR", + }, + { + "target_id": "ooni/website", + "category_code": "HUMR", + }, + { + "target_id": "ooni/website", + "category_code": "HUMR", + }, + { + "target_id": "ooni/explorer", + "category_code": "HUMR", }], // (optional) `string` naming a backend-generated dynamic input list (see @@ -239,9 +253,9 @@ rules keep authoring mistakes loud and probes forward-compatible: * Option names starting with `safe_` MAY carry secrets and are subject to the scrubbing rule of section 2.0. -* Probes MUST record the effective per-input configuration — after the merge, - excluding `safe_` options — in the submitted measurement, so that data - analysis can condition on what actually ran. +* Probes MUST record the effective per-input configuration, after the merge, + excluding `safe_` options, in the submitted measurement inside of the + `config` key, so that data analysis can condition on what actually ran. ## 3.2 Target identity @@ -886,6 +900,34 @@ generated target lists, the measurements submitted under one `ooni_run_attempt` are also the record of what the resolution served, up to the inputs the probe did not reach (see 4.5). +Additionally, a top level key called `config` should include the configuration for +the test that was resolved at the `input` level. For example given the following: +``` +"options": { + "http3_enabled": false +} + +"inputs_extra": { + "category_code": "HUMR", + "safe_value": "something_sekrit" +} +``` + +and the engine having a default setting for `dot_enabled=true` + +The config key shall contain: +``` +{ + "config": { + "http3_enabled": false, + "category_code": "HUMR", + "dot_enabled": true + } +} +``` + +Note that the `inputs_extra` that was prefixed with `safe_` got stripped. + # 6.0 Implementation considerations Special attention should be placed in ensuring the OONI Run links (which are