# PIP-0018 · Tunnels

A signed agreement between two PATH networks that lets a search cross from one directory to the other, within what each network's gate admits. Closed by default.

## Why

A PATH network is a club: its directory answers its members ([Networks and members](/learn/networks)). Several networks coexist, and a payer in one may need to reach a subject held in another. [PATH CROSSWAY](/docs/crossway) resolves destinations on networks outside PATH. This PIP defines the link between two PATH networks: the **tunnel**.

A member never deals with the other network. It asks its own network, which routes.

## Terms

| Term | Meaning |
|---|---|
| **Tunnel** | A signed agreement between two PATH networks. It exists only when both sign it |
| **Gate** | What one network admits through a tunnel, in each direction. Each end of a tunnel has its own gate |
| **Gateway** | The endpoint that relays searches for a network. A network may operate it or delegate it |

A network with no tunnel takes no search from another network. That is the default.

## The tunnel

```json
{
  "type": "path.tunnel",
  "reference": "tnl_8f2k4m9q",
  "networks": ["openconnect", "network-b"],
  "gates": {
    "openconnect": {
      "inbound": { "identifier_types": ["alias", "phone"], "zones": ["+221", "+225"], "queries_per_day": 50000 },
      "outbound": { "identifier_types": ["alias", "phone"] }
    },
    "network-b": {
      "inbound": { "identifier_types": ["alias"], "queries_per_day": 20000 },
      "outbound": { "identifier_types": ["alias", "phone"] }
    }
  },
  "gateways": {
    "openconnect": "https://gw.openconnect.example/api/path/v1",
    "network-b": "https://gw.network-b.example/api/path/v1"
  },
  "rulebooks": {
    "openconnect": { "url": "https://…", "version": "1.2" },
    "network-b": { "url": "https://…", "version": "3.0" }
  },
  "disputes": { "rulebook": "network-b" },
  "effective_at": "2026-11-01T00:00:00Z",
  "expires_at": "2027-11-01T00:00:00Z",
  "status": "active",
  "signatures": [
    { "network": "network-b", "kid": "network-b_2026_01", "signed_at": "…", "protocol_version": "0.2.0", "signature": "…" },
    { "network": "openconnect", "kid": "openconnect_2026_01", "signed_at": "…", "protocol_version": "0.2.0", "signature": "…" }
  ]
}
```

The object is signed by both networks. Each signature covers the payload — every field except `status`, `signatures` and `revocation` — with its own `protocol_version`, `signed_at` and `kid`, as in any PATH envelope. `networks` is sorted, so both networks sign the same bytes. A tunnel is `pending` until it carries both signatures.

| Field | Rule |
|---|---|
| `networks` | Exactly two network slugs |
| `gates.<network>.inbound` | What that network answers for searches coming from the other. Absent: nothing |
| `gates.<network>.outbound` | What that network sends to the other. Absent: nothing |
| `identifier_types` | The types a gate admits. `address` is not among them: an address is resolved without a directory |
| `zones` | Optional. A `phone` prefix (`+221`) restricts `phone`; a country code (`SN`) restricts `reg` and `tax`. Other types are not restricted by zone |
| `queries_per_day` | Optional. The inbound budget of that network for this tunnel |
| `disputes.rulebook` | The network whose rulebook governs disputes across the tunnel. Required |
| `status` | `active` or `revoked` |

A search crosses only when the sending network's outbound gate and the receiving network's inbound gate both admit it.

## Network keys

A network publishes its signing keys at `/.well-known/path-keys` on its own domain, in the same format as a member. The network register lists, for each network, its domain and the `kid` of its keys. A signature by a network is valid when its key is served on the network's domain and its `kid` is listed in the register. Rotation follows the rules for member keys.

## Disputes

A tunnel names, under `disputes`, the rulebook that governs a dispute between a payer in one of its networks and a payee in the other. A tunnel without it is not valid.

```json
"disputes": { "rulebook": "network-b" }
```

The value is one of the two networks. When the two networks agree on no other, they name the network of the payee. Procedures, time limits and liability are set by that rulebook.

## Discoverability: `federated`

A new discoverability level, between `network` and `public`.

| Level | Findable by |
|---|---|
| `private` | Nobody, through the directory |
| `network` | Members of the subject's network |
| `federated` | Members of the subject's network, and of the networks its network has a tunnel with |
| `public` | Anyone with access to a directory query |

Only `federated` and `public` entries are answered through a tunnel. A subject chooses `federated` explicitly, through its member. A network may set `federated` as the default for `alias` in its rulebook, and states it in `finder.aliases.default_discoverability`; for every other type the defaults of [PATH ID](/docs/id) are unchanged.

A member may also exclude all of its subjects from tunnels. Its entries are then answered within its network only, whatever their level.

## Resolution through a tunnel

SONAR answers *which members hold this key*. Within one network a key has at most one holder. Across networks it may have several: the same phone number can be held by a member of each. A search through tunnels returns every holder the gates admit.

1. **Fan-out.** The payer's network runs SONAR on its own index and, at the same time, its gateway sends the search through every tunnel whose gates admit the identifier type and zone. An `alias:<network>:<name>` goes only to the network it names.
2. **SONAR in each other network.** Its gateway normalises and hashes the identifier under its own pepper, and answers. The answer is signed by that network and names it. Negatives are indistinguishable, as within a network.
3. **One answer to the member.** The payer's network returns every holder found, its own and those of the other networks, each with its network. The wallet shows the network with each result and the payer chooses.
4. **RESOLVER at the chosen holder.** The payer's member calls the holding member directly, as in the `relationship` profile. No membership of the other network is required.
5. **Payee check**, as in [PIP-0013](/pips/0013).

The inbound budget of each gate applies to every search it receives, whether or not it finds a holder.

## Requests between gateways

A gateway signs each search it sends through a tunnel, as a member signs its requests:

| Header | Value |
|---|---|
| `Path-Tunnel` | The tunnel `reference` |
| `Path-Key-Id` | A `kid` of the sending network, listed for it in the register |
| `Path-Timestamp` | RFC 3339, within five minutes |
| `Path-Signature` | Ed25519 over `<method>\n<path>\n<timestamp>\n<sha256 of body>` |

The receiving gateway answers only for a tunnel in force. Its answer is a signed envelope of kind `tunnel_sonar_answer` that binds the tunnel, both networks, the identifier type, the nonce and the result. The sending network checks each binding and the signature before it returns the answer.

## What each party sees

| Party | Sees |
|---|---|
| The payer's network | The search, as within the network. It logs the requesting member |
| Each other network whose gates admit the search | The identifier, in order to hash it under its own pepper; the requesting network and the tunnel. Not the requesting member |
| The holding member | The RESOLVER call, as for any caller |

Each network keeps its own search log, under its own declared retention. Each network's discovery document states its `hashing` mode, which applies to searches it receives through a tunnel.

## What a gate does not close

A gate closes the directory. It does not close the protocol: a subject's PATH link or address can be read and paid by anyone who holds it ([PATH INTEROP](/docs/interop)). A network that opens no tunnel keeps its directory to its members; it does not make its members' links unreachable.

## Lifecycle

- A tunnel is in force from `effective_at` until `expires_at`, or until revoked.
- Either network revokes it alone. Revocation is signed, takes effect at once, and is published in the network register.
- A change to a gate is a new tunnel, with a new `reference`, signed by both networks. The tunnel it replaces is revoked. A network that wants to narrow what it admits at once revokes the tunnel alone, then agrees the new one.

## Publication

- The network register lists each network's active tunnels and their gates.
- A member's discovery document states, under `finder.tunnels`, the networks reachable through its network and the identifier types admitted.

```json
"finder": {
  "tunnels": [
    { "network": "network-b", "identifier_types": ["alias", "phone"] }
  ]
}
```

## Interface

| Call | Who | Effect |
|---|---|---|
| `POST /tunnels` | The network's operator | Records a tunnel agreed with another network, with `peer_kids`, and signs it. `pending` |
| `POST /tunnels/{reference}/signatures` | The network's operator | Adds the other network's signature, verified against its keys. `active` |
| `POST /tunnels/{reference}/revoke` | The network's operator | Revokes the tunnel and publishes a signed revocation |
| `GET /tunnels` | Anyone | The network's tunnels, as published |
| `PUT /tunnels/exclusion` | A member | Keeps all of the member's subjects out of tunnels, or lets them back |
| `POST /tunnel/sonar` | The other network's gateway | A search through the tunnel |
| `POST /sonar/federated` | A member | One search: this network and every admitting tunnel. Returns `holders` and `unavailable` |

Errors: `path.tunnel.inactive`, `path.tunnel.not_admitted`, `path.tunnel.budget_exhausted`, `path.tunnel.signature_invalid` ([Errors](/docs/errors)).

The SDK calls `POST /sonar/federated` with `sonarFederated`.

## Conformance

A network that operates tunnels declares `PATH-NETWORK.Tunnel`: it signs and publishes its tunnels, applies its gates, answers only `federated` and `public` entries through them, and keeps negatives indistinguishable.

## What this is not

- **Not CROSSWAY.** CROSSWAY reaches registries outside PATH, through attested connectors. A tunnel links two PATH networks, under their two rulebooks.
- **Not a rail.** No value moves through a tunnel. Payment executes on a rail both parties use.
- **Not a merger of directories.** No network holds another's index. Each answers for its own members.

## Compatibility

Additive. `federated` is a new value of `discoverability`; a reader that does not know it treats the entry as `network`. Networks without tunnels are unaffected.