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
7.6 KiB
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
tt://?<base64url-encoded payload>
- Scheme:
tt - Payload: The endpoint configuration is serialized into a binary format, then encoded using Base64url (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 Tag–Length–Value (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 (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
- Fields MAY appear in any order.
- 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. - Boolean fields that match their default value MAY be omitted to save space.
- A parser MUST reject a payload that is missing any required field.
Example
Given the following exported endpoint configuration:
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
-
Serialize each field into TLV entries:
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. -
Concatenate all TLV entries into a single byte buffer.
-
Base64url-encode the buffer (no padding).
-
Construct the URI:
tt://?AQAL... (full Base64url string)
Versioning
The deep link format carries an explicit version number in tag 0x00.
- If the
0x00tag 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:
- Strip the
tt://?prefix. - Base64url-decode the remainder.
- Parse the TLV binary payload.
- 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
usernameandpasswordin cleartext (after decoding). Treat deep link URIs with the same care as passwords. Do not log or persist them unnecessarily. - Certificate pinning: When
certificateis present andskip_verificationisfalse, 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.