TrustTunnel/DEEP_LINK.md
Andrey Yakushin 9e144fe4bc Pull request 192: Rename server_display_name->name and dns_servers->dns_upstreams
Squashed commit of the following:

commit 66b133c3e7c18a9bbc2fdf8792268ad18d495b5d
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Tue Apr 7 18:47:13 2026 +0400

    Fix lint

commit 64a9b4862edcb10d9b4b3b38414ee70c03fc9725
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Tue Apr 7 18:44:49 2026 +0400

    Rename server_display_name->name and dns_servers->dns_upstreams
2026-04-07 14:51:04 +00:00

213 lines
7.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# TrustTunnel Deep Link Specification
This document describes the deep link URI scheme used to share TrustTunnel
endpoint configurations between devices and applications.
Status: version 1
- version 1: Added fields for version, server display name, and DNS upstreams.
- draft 2: Changed format to tt://? to use case-sensitive URL part (query) instead of case-insensitive (host)
- draft 1: Initial specification
---
## URI Format
```uri
tt://?<base64url-encoded payload>
```
- **Scheme**: `tt`
- **Payload**: The endpoint configuration is serialized into a binary format,
then encoded using [Base64url](https://datatracker.ietf.org/doc/html/rfc4648#section-5)
(URL-safe Base64 without padding).
### Why Base64url?
Standard Base64 uses `+` and `/` characters that require percent-encoding in
URIs. Base64url replaces them with `-` and `_`, making the result safe to embed
directly in a URI without escaping. Padding (`=`) is omitted.
---
## Binary Payload Format
The payload is a compact binary encoding of the endpoint configuration fields
exported by `trusttunnel_endpoint`.
### Wire Layout
Each field is encoded as a **TagLengthValue (TLV)** entry:
| Component | Encoding | Description |
| --- | --- | --- |
| **Tag** | TLS varint | Field identifier (see table below) |
| **Length** | TLS varint | Byte length of the value that follows |
| **Value** | *Length* bytes | Field-specific payload |
A parser MUST ignore unknown tags to allow forward-compatible extensions.
### TLS Variable-Length Integer Encoding
Tag and Length use the variable-length integer encoding defined in
[RFC 9000 §16](https://www.rfc-editor.org/rfc/rfc9000.html#section-16)
(QUIC / TLS 1.3). The two most-significant bits of the first byte encode the
length of the integer:
| 2-MSB | Integer size | Usable bits | Max value |
| --- | --- | --- | --- |
| `00` | 1 byte | 6 | 63 |
| `01` | 2 bytes | 14 | 16 383 |
| `10` | 4 bytes | 30 | 1 073 741 823 |
| `11` | 8 bytes | 62 | 4 611 686 018 427 387 903 |
Multi-byte varints are in **network byte order** (big-endian). In practice,
current tags fit in a single byte (`00` prefix) and lengths under 16 384 fit
in one or two bytes.
### Value Types
| Type | Description |
| --- | --- |
| VarInt | TLS variable-length integer (see encoding above) |
| Bool | 1 byte: `0x01` = true, `0x00` = false |
| String | UTF-8 encoded bytes |
| Bytes | Raw binary data |
| String[] | Length-prefixed sequence of strings: each element is encoded as a VarInt length followed by UTF-8 bytes. The TLV value field contains the concatenation of all length-prefixed elements. |
### Field Tags
| Tag | Field | Value type | Value encoding | Required |
| --- | --- | --- | --- | --- |
| `0x00` | `version` | VarInt | Deep link format version (see Versioning below) | no (default `0`) |
| `0x01` | `hostname` | String | UTF-8 string | yes |
| `0x02` | `addresses` | String | UTF-8, one `address:port` per entry; multiple entries are encoded as separate TLVs with the same tag | yes |
| `0x03` | `custom_sni` | String | UTF-8 string | no |
| `0x04` | `has_ipv6` | Bool | 1 byte: `0x01` = true, `0x00` = false | no (default `true`) |
| `0x05` | `username` | String | UTF-8 string | yes |
| `0x06` | `password` | String | UTF-8 string | yes |
| `0x07` | `skip_verification` | Bool | 1 byte: `0x01` = true, `0x00` = false | no (default `false`) |
| `0x08` | `certificate` | Bytes | Concatenated DER-encoded certificates (raw binary); omit if the chain is verified by system CAs | no |
| `0x09` | `upstream_protocol` | VarInt | `0x01` = `http2`, `0x02` = `http3` | no (default `http2`) |
| `0x0A` | `anti_dpi` | Bool | 1 byte: `0x01` = true, `0x00` = false | no (default `false`) |
| `0x0B` | `client_random_prefix` | String | UTF-8 hex-encoded string in the following format: `prefix[/mask]` | no |
| `0x0C` | `name` | String | Human-readable server name for display in the client UI | no |
| `0x0D` | `dns_upstreams` | String[] | List of DNS upstream addresses (e.g. `"1.1.1.1"`, `"tls://dns.example.com"`, `"https://dns.example.com/dns-query"`) | no |
### Encoding Rules
1. Fields MAY appear in any order.
2. Tag `0x02` (`addresses`) MAY appear more than once; each occurrence adds one
address to the list. All other tags MUST appear at most once; if duplicated,
the last occurrence wins.
3. Boolean fields that match their default value MAY be omitted to save space.
4. A parser MUST reject a payload that is missing any required field.
---
## Example
Given the following exported endpoint configuration:
```toml
hostname = "vpn.example.com"
addresses = ["1.2.3.4:443"]
custom_sni = "example.org"
has_ipv6 = true
username = "premium"
password = "s3cretPass"
skip_verification = false
certificate = """
-----BEGIN CERTIFICATE-----
MIIDijCCAxGgAwIBAgISBcSirIQr2Y8pK6reoWtJhyXZMAoGCCqGSM49BAMDMDIx
...
-----END CERTIFICATE-----
"""
upstream_protocol = "http2"
anti_dpi = false
```
### Encoding Steps
1. **Serialize** each field into TLV entries:
```text
Tag=0x01 Len=15 Value="vpn.example.com"
Tag=0x02 Len=11 Value="1.2.3.4:443"
Tag=0x03 Len=11 Value="example.org"
Tag=0x04 Len=1 Value=0x01 (has_ipv6 = true)
Tag=0x05 Len=7 Value="premium"
Tag=0x06 Len=10 Value="s3cretPass"
Tag=0x08 Len=N Value=<concatenated DER bytes of the certificate chain>
Tag=0x09 Len=1 Value=0x01 (http2)
```
Fields at their default value (`skip_verification = false`,
`anti_dpi = false`) are omitted.
2. **Concatenate** all TLV entries into a single byte buffer.
3. **Base64url-encode** the buffer (no padding).
4. **Construct** the URI:
```uri
tt://?AQAL... (full Base64url string)
```
---
## Versioning
The deep link format carries an explicit version number in tag `0x00`.
- If the `0x00` tag is absent, parsers MUST assume **version 0**.
- The value is encoded as a VarInt.
- The first explicitly versioned format is **version 1**.
- A client MUST reject a deep link whose version is higher than the maximum
version it supports.
- A client MUST accept deep links with a version equal to or lower than the
maximum version it supports (backward compatibility).
---
## Platform Integration
### Mobile (iOS / Android)
Register the `tt` scheme in the application manifest. When the OS dispatches a
deep link:
1. Strip the `tt://?` prefix.
2. Base64url-decode the remainder.
3. Parse the TLV binary payload.
4. Populate the endpoint configuration and present it to the user for
confirmation before connecting.
### Desktop (macOS / Windows / Linux)
The `tt://?` URI can be passed as a command-line argument or handled via OS URI
scheme registration. The TrustTunnel client or setup wizard parses the payload
using the same decode logic.
### QR Codes
The `tt://?` URI is short enough to be embedded in a QR code for easy scanning,
enabling zero-typing configuration sharing.
---
## Security Considerations
- **Credentials in the URI**: The deep link contains the `username` and
`password` in cleartext (after decoding). Treat deep link URIs with the same
care as passwords. Do not log or persist them unnecessarily.
- **Certificate pinning**: When `certificate` is present and
`skip_verification` is `false`, the client MUST verify the endpoint
certificate against the provided PEM chain.
- **User confirmation**: Clients SHOULD display the decoded configuration to the
user and require explicit confirmation before establishing a connection.
- **URI length**: Very large PEM certificate chains may produce long URIs.
Implementations should handle URIs up to at least 8 KiB. For QR code use,
consider whether the certificate field can be omitted when the endpoint uses a
publicly trusted CA.