From 6bc3c00b64cf2400648030404724a99ad4ce6912 Mon Sep 17 00:00:00 2001 From: bzuccaro Date: Sun, 2 Aug 2026 12:07:54 -0600 Subject: [PATCH] feat: compress connection keys (rtun2) for easier copy/paste --- Cargo.lock | 16 ++++++ Cargo.toml | 1 + droid-wiki/applications/rustunnel-cli.md | 2 +- droid-wiki/features/connection-keys.md | 27 ++++++--- .../patterns-and-conventions.md | 2 +- droid-wiki/overview/glossary.md | 2 +- droid-wiki/overview/index.md | 2 +- droid-wiki/reference/data-models.md | 4 +- droid-wiki/reference/dependencies.md | 1 + droid-wiki/security.md | 2 +- src/connkey.rs | 56 ++++++++++++++++--- src/main.rs | 2 +- src/redact.rs | 5 +- 13 files changed, 97 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d31dc6a..f9771b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -925,6 +931,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.2.1" @@ -1294,6 +1309,7 @@ dependencies = [ "hyper", "hyper-util", "libc", + "miniz_oxide", "rand", "rcgen", "rustls", diff --git a/Cargo.toml b/Cargo.toml index 47f0faf..de596ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ hyper-util = { version = "0.1", features = ["full", "server-graceful"] } http-body-util = "0.1" tower-service = "0.3" base64 = "0.22" +miniz_oxide = "0.8" sha2 = "0.10" bytes = "1" futures = "0.3" diff --git a/droid-wiki/applications/rustunnel-cli.md b/droid-wiki/applications/rustunnel-cli.md index 72264ab..5ea8d98 100644 --- a/droid-wiki/applications/rustunnel-cli.md +++ b/droid-wiki/applications/rustunnel-cli.md @@ -39,7 +39,7 @@ Creates: `ca.pem`, `ca.key`, `server.crt`, `server.key`, `client.crt`, `client.k ### `keygen` -Generate a single reusable connection key string. This bundles all certificate material, the auth token, and the target address into a base64-encoded JSON blob prefixed with `rtun1.`. +Generate a single reusable connection key string. This bundles all certificate material, the auth token, and the target address into a base64url-encoded, DEFLATE-compressed JSON blob prefixed with `rtun2.`. ### `version` diff --git a/droid-wiki/features/connection-keys.md b/droid-wiki/features/connection-keys.md index c50e29e..87eb8e4 100644 --- a/droid-wiki/features/connection-keys.md +++ b/droid-wiki/features/connection-keys.md @@ -8,11 +8,19 @@ Simplify distribution of tunnel credentials between machines. Instead of transfe ## Format -Connection keys start with the prefix `rtun1.` followed by base64url-encoded (no padding) JSON: +Connection keys start with the prefix `rtun2.` followed by base64url-encoded (no +padding) **DEFLATE-compressed** JSON: + +``` +rtun2. +``` + +The JSON payload is compressed with DEFLATE (`miniz_oxide`) before base64url-encoding +to keep the key short enough to copy and paste comfortably. Example payload: ```json { - "version": 1, + "version": 2, "target": "198.51.100.10:4180", "ca_cert_pem": "-----BEGIN CERTIFICATE-----...", "server_cert_pem": "-----BEGIN CERTIFICATE-----...", @@ -28,20 +36,23 @@ Connection keys start with the prefix `rtun1.` followed by base64url-encoded (no | Type | File | Description | | ---- | ---- | ----------- | | `ConnectionKey` | `src/connkey.rs` | Struct with all fields, version check, validation | -| `ConnectionKey::encode` | `src/connkey.rs` | Serialize to JSON, base64url-encode, prepend prefix | -| `ConnectionKey::decode` | `src/connkey.rs` | Strip prefix, base64url-decode, deserialize, validate | -| `looks_like_connection_key` | `src/connkey.rs` | Quick check if a string starts with `rtun1.` | +| `ConnectionKey::encode` | `src/connkey.rs` | Serialize to JSON, DEFLATE-compress, base64url-encode, prepend prefix | +| `ConnectionKey::decode` | `src/connkey.rs` | Strip prefix, base64url-decode, DEFLATE-decompress, deserialize, validate | +| `looks_like_connection_key` | `src/connkey.rs` | Quick check if a string starts with `rtun2.` | ## Validation `decode` validates: -- Prefix must be `rtun1.` +- Prefix must be `rtun2.` - Base64 decoding must succeed +- DEFLATE decompression must succeed - JSON deserialization must succeed -- Version must be exactly `1` +- Version must be exactly `2` - All string fields must be non-empty after trimming +Legacy `rtun1.` keys are **not** accepted. + ## Integration `src/main.rs` uses `ConnectionKey::decode` when the `--connection-key` flag or positional argument is provided. The decoded material is passed to `ServerTlsMaterial::Pem` or `ClientTlsMaterial::Pem` variants, which bypass file loading and use the embedded PEM strings directly. @@ -49,7 +60,7 @@ Connection keys start with the prefix `rtun1.` followed by base64url-encoded (no ## Entry points for modification - To change the key format or add versioning: modify `src/connkey.rs`. -- To add compression or encryption: consider extending the encode/decode pipeline in `ConnectionKey`. +- To add encryption: consider extending the encode/decode pipeline in `ConnectionKey`. ## Key source files diff --git a/droid-wiki/how-to-contribute/patterns-and-conventions.md b/droid-wiki/how-to-contribute/patterns-and-conventions.md index 5bd3b8a..45bf910 100644 --- a/droid-wiki/how-to-contribute/patterns-and-conventions.md +++ b/droid-wiki/how-to-contribute/patterns-and-conventions.md @@ -13,7 +13,7 @@ The project uses a layered error strategy: - The `Redacted` wrapper in `src/redact.rs` must be used before logging any secret. - `Redacted::inner()` and `into_inner()` are marked `#[allow(dead_code)]` to discourage use, but available for file writes. - `is_sensitive()` detects PEM private key blocks. `is_auth_token()` detects long alphanumeric strings. -- `is_connection_key()` detects strings starting with `rtun1.`. +- `is_connection_key()` detects strings starting with `rtun2.`. ## Async patterns diff --git a/droid-wiki/overview/glossary.md b/droid-wiki/overview/glossary.md index 38ac0c5..25584c8 100644 --- a/droid-wiki/overview/glossary.md +++ b/droid-wiki/overview/glossary.md @@ -1,6 +1,6 @@ # Glossary -**Connection key** — a base64-encoded JSON blob (prefix `rtun1.`) that bundles the CA certificate, server certificate, server key, client certificate, client key, target address, auth token, and version. Generated by `rustunnel keygen` and consumed by `rustunnel listen` and `rustunnel connect`. +**Connection key** — a base64url-encoded, DEFLATE-compressed JSON blob (prefix `rtun2.`) that bundles the CA certificate, server certificate, server key, client certificate, client key, target address, auth token, and version. Generated by `rustunnel keygen` and consumed by `rustunnel listen` and `rustunnel connect`. **Connector** — the `rustunnel connect` side. Establishes an outbound HTTPS+mTLS connection to the listener, authenticates, then exposes a local SOCKS5 proxy for local applications. diff --git a/droid-wiki/overview/index.md b/droid-wiki/overview/index.md index a79f618..248b798 100644 --- a/droid-wiki/overview/index.md +++ b/droid-wiki/overview/index.md @@ -9,7 +9,7 @@ The tool is designed for local labs, development environments, and controlled te - **Listener** (`rustunnel listen`) — binds an HTTPS server that accepts mTLS connections from connectors, validates an auth token, then upgrades the connection to a persistent binary-framed tunnel. - **Connector** (`rustunnel connect`) — connects to the listener over HTTPS with mTLS, authenticates, then exposes a local SOCKS5 proxy that forwards traffic through the tunnel. - **Credential generation** (`rustunnel generate`) — creates a self-signed CA, server certificate, client certificate, and auth token for a local session. -- **Connection keys** (`rustunnel keygen`) — bundles all credential material into a single base64-encoded string that can be copy-pasted between machines. +- **Connection keys** (`rustunnel keygen`) — bundles all credential material into a single compressed base64url string that can be copy-pasted between machines. ## Quick links diff --git a/droid-wiki/reference/data-models.md b/droid-wiki/reference/data-models.md index 4dc4613..2cb6e67 100644 --- a/droid-wiki/reference/data-models.md +++ b/droid-wiki/reference/data-models.md @@ -4,7 +4,7 @@ ```rust pub struct ConnectionKey { - pub version: u8, // must be 1 + pub version: u8, // must be 2 pub target: String, // listener address pub ca_cert_pem: String, pub server_cert_pem: String, @@ -15,7 +15,7 @@ pub struct ConnectionKey { } ``` -Encoded as: `rtun1.` + base64url(JSON) — no padding. +Encoded as: `rtun2.` + base64url(deflate(JSON)) — no padding. ## Config diff --git a/droid-wiki/reference/dependencies.md b/droid-wiki/reference/dependencies.md index a9cf859..8f29fa1 100644 --- a/droid-wiki/reference/dependencies.md +++ b/droid-wiki/reference/dependencies.md @@ -27,6 +27,7 @@ | `http-body-util` | 0.1 | HTTP body utilities | | `tower-service` | 0.3 | Service trait (Hyper ecosystem) | | `base64` | 0.22 | Connection key encoding | +| `miniz_oxide` | 0.8 | DEFLATE compression for connection keys | | `sha2` | 0.10 | Certificate fingerprinting | | `bytes` | 1 | Byte buffers for framing | | `futures` | 0.3 | Future utilities | diff --git a/droid-wiki/security.md b/droid-wiki/security.md index ffa02cb..6778206 100644 --- a/droid-wiki/security.md +++ b/droid-wiki/security.md @@ -33,7 +33,7 @@ The E2E test `auth_required_in_addition_to_mtls` verifies that valid mTLS alone - `redact_config_json()` scrubs sensitive keys from JSON. - `is_sensitive()` detects PEM private key blocks. - `is_auth_token()` detects long alphanumeric strings. -- `is_connection_key()` detects strings starting with `rtun1.`. +- `is_connection_key()` detects strings starting with `rtun2.`. The E2E test `ops_auth_failure_is_actionable_and_redacted` verifies that error messages do not contain raw token values. diff --git a/src/connkey.rs b/src/connkey.rs index 80daf3b..526bd2a 100644 --- a/src/connkey.rs +++ b/src/connkey.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::generate::GeneratedMaterial; -const PREFIX: &str = "rtun1."; +const PREFIX: &str = "rtun2."; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ConnectionKey { @@ -21,7 +21,7 @@ pub struct ConnectionKey { impl ConnectionKey { pub fn new(target: String, material: GeneratedMaterial) -> Self { Self { - version: 1, + version: 2, target, ca_cert_pem: material.ca_cert_pem, server_cert_pem: material.server_cert_pem, @@ -34,7 +34,8 @@ impl ConnectionKey { pub fn encode(&self) -> anyhow::Result { let json = serde_json::to_vec(self)?; - Ok(format!("{}{}", PREFIX, URL_SAFE_NO_PAD.encode(json))) + let compressed = miniz_oxide::deflate::compress_to_vec(&json, 6); + Ok(format!("{}{}", PREFIX, URL_SAFE_NO_PAD.encode(compressed))) } pub fn decode(input: &str) -> anyhow::Result { @@ -45,9 +46,11 @@ impl ConnectionKey { let bytes = URL_SAFE_NO_PAD .decode(encoded) .map_err(|e| anyhow::anyhow!("invalid connection key encoding: {}", e))?; - let key: Self = serde_json::from_slice(&bytes) + let json = miniz_oxide::inflate::decompress_to_vec(&bytes) .map_err(|e| anyhow::anyhow!("invalid connection key payload: {}", e))?; - if key.version != 1 { + let key: Self = serde_json::from_slice(&json) + .map_err(|e| anyhow::anyhow!("invalid connection key payload: {}", e))?; + if key.version != 2 { anyhow::bail!("unsupported connection key version {}", key.version); } if key.target.trim().is_empty() @@ -79,15 +82,54 @@ mod tests { crate::generate::generate_material("ca", "server", "client").expect("material"); let key = ConnectionKey::new("127.0.0.1:4180".to_string(), material); let encoded = key.encode().unwrap(); - assert!(encoded.starts_with("rtun1.")); + assert!(encoded.starts_with("rtun2.")); let decoded = ConnectionKey::decode(&encoded).unwrap(); - assert_eq!(decoded.version, 1); + assert_eq!(decoded.version, 2); assert_eq!(decoded.target, "127.0.0.1:4180"); assert_eq!(decoded.auth_token, key.auth_token); + assert_eq!(decoded.ca_cert_pem, key.ca_cert_pem); + assert_eq!(decoded.server_cert_pem, key.server_cert_pem); + assert_eq!(decoded.server_key_pem, key.server_key_pem); + assert_eq!(decoded.client_cert_pem, key.client_cert_pem); + assert_eq!(decoded.client_key_pem, key.client_key_pem); + } + + #[test] + fn encoded_key_is_smaller_than_raw_json() { + let material = + crate::generate::generate_material("ca", "server", "client").expect("material"); + let key = ConnectionKey::new("127.0.0.1:4180".to_string(), material); + let encoded = key.encode().unwrap(); + + let json = serde_json::to_vec(&key).unwrap(); + let raw = format!("{}{}", PREFIX, URL_SAFE_NO_PAD.encode(json)); + assert!( + encoded.len() < raw.len(), + "compressed key ({} chars) should be shorter than uncompressed key ({} chars)", + encoded.len(), + raw.len() + ); + assert!( + encoded.len() < (raw.len() * 8) / 10, + "compressed key ({} chars) should be meaningfully shorter than uncompressed ({} chars)", + encoded.len(), + raw.len() + ); } #[test] fn invalid_prefix_fails() { assert!(ConnectionKey::decode("bad").is_err()); + assert!( + ConnectionKey::decode("rtun1.not-a-valid-payload").is_err(), + "legacy rtun1. keys are not accepted" + ); + } + + #[test] + fn looks_like_connection_key_detects_rtun2_prefix() { + assert!(looks_like_connection_key("rtun2.abc")); + assert!(!looks_like_connection_key("abc")); + assert!(!looks_like_connection_key("rtun1.def")); } } diff --git a/src/main.rs b/src/main.rs index 656f687..6e962ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -633,7 +633,7 @@ mod tests { fn test_connection_key(target: &str) -> ConnectionKey { ConnectionKey { - version: 1, + version: 2, target: target.to_string(), ca_cert_pem: "ca".to_string(), server_cert_pem: "server-cert".to_string(), diff --git a/src/redact.rs b/src/redact.rs index 680ea6e..90e38ac 100644 --- a/src/redact.rs +++ b/src/redact.rs @@ -120,7 +120,7 @@ pub fn is_auth_token(content: &str) -> bool { #[allow(dead_code)] pub fn is_connection_key(content: &str) -> bool { - content.trim_start().starts_with("rtun1.") + content.trim_start().starts_with("rtun2.") } #[cfg(test)] @@ -213,8 +213,9 @@ mod tests { #[test] fn is_connection_key_detects_rtun_prefix() { - assert!(is_connection_key("rtun1.abc")); + assert!(is_connection_key("rtun2.abc")); assert!(!is_connection_key("abc")); + assert!(!is_connection_key("rtun1.abc")); } #[test]