warp_wireguard_gen/lib.rs
1//! Generate WireGuard configurations by registering with Cloudflare WARP.
2//!
3//! This crate provides functionality to:
4//! - Register a new device with Cloudflare WARP (consumer)
5//! - Register a device with Cloudflare for Teams / Zero Trust
6//! - Retrieve WireGuard configuration for connecting through WARP
7//! - Optionally apply a Warp+ license key
8//!
9//! # Example
10//!
11//! ```no_run
12//! use warp_wireguard_gen::{register, RegistrationOptions};
13//!
14//! #[tokio::main]
15//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
16//! // Register with default options (consumer WARP)
17//! let (config, credentials) = register(RegistrationOptions::default()).await?;
18//!
19//! // Use config with wireguard-netstack...
20//! // Optionally save credentials for reuse...
21//!
22//! Ok(())
23//! }
24//! ```
25//!
26//! # Cloudflare for Teams (Zero Trust) Enrollment
27//!
28//! To enroll with Cloudflare for Teams:
29//!
30//! 1. Visit `https://<team-name>.cloudflareaccess.com/warp`
31//! 2. Authenticate as you would with the official WARP client
32//! 3. Extract the JWT token from the page source or use browser console:
33//! ```js
34//! console.log(document.querySelector("meta[http-equiv='refresh']").content.split("=")[2])
35//! ```
36//! 4. Pass the JWT token via [`TeamsEnrollment`] in [`RegistrationOptions`]
37//!
38//! ```no_run
39//! use warp_wireguard_gen::{register, RegistrationOptions, TeamsEnrollment};
40//!
41//! # async fn example() -> warp_wireguard_gen::Result<()> {
42//! let (config, credentials) = register(RegistrationOptions {
43//! device_model: "PC".to_string(),
44//! license_key: None,
45//! teams: Some(TeamsEnrollment {
46//! jwt_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...".to_string(),
47//! device_name: Some("My Device".to_string()),
48//! serial_number: None,
49//! }),
50//! }).await?;
51//! # Ok(())
52//! # }
53//! ```
54//!
55//! # Feature Flags
56//!
57//! - `serde`: Enables `Serialize` and `Deserialize` for `WarpCredentials`,
58//! allowing easy persistence to JSON, TOML, etc.
59//!
60//! # Credential Persistence
61//!
62//! The [`WarpCredentials`] struct returned by [`register`] contains all the
63//! information needed to reconnect without re-registering. Enable the `serde`
64//! feature to serialize credentials for storage.
65//!
66//! ```no_run
67//! # #[cfg(feature = "serde")]
68//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
69//! use warp_wireguard_gen::{register, get_config, RegistrationOptions, WarpCredentials};
70//!
71//! // First run: register and save credentials
72//! # tokio::runtime::Runtime::new().unwrap().block_on(async {
73//! let (config, credentials) = register(RegistrationOptions::default()).await?;
74//! let json = serde_json::to_string_pretty(&credentials)?;
75//! std::fs::write("warp-credentials.json", &json)?;
76//!
77//! // Later: load credentials and get fresh config
78//! let json = std::fs::read_to_string("warp-credentials.json")?;
79//! let credentials: WarpCredentials = serde_json::from_str(&json)?;
80//! let config = get_config(&credentials).await?;
81//! # Ok::<(), Box<dyn std::error::Error>>(())
82//! # });
83//! # Ok(())
84//! # }
85//! ```
86
87pub mod api;
88pub mod error;
89pub mod keys;
90pub mod types;
91
92pub use error::{Error, Result};
93
94use base64::{engine::general_purpose::STANDARD, Engine};
95use wireguard_netstack::WireGuardConfig;
96
97/// Options for registering a new WARP device.
98#[derive(Debug, Clone)]
99pub struct RegistrationOptions {
100 /// Device model name displayed in the 1.1.1.1 app.
101 ///
102 /// Default: `"PC"`
103 pub device_model: String,
104
105 /// Optional Warp+ license key.
106 ///
107 /// Must be purchased through the official 1.1.1.1 app.
108 /// Keys obtained by other means (including referrals) will not work.
109 ///
110 /// Note: This is not applicable for Teams enrollment.
111 pub license_key: Option<String>,
112
113 /// Cloudflare for Teams enrollment options.
114 ///
115 /// When set, the registration will use Cloudflare Zero Trust (formerly
116 /// Cloudflare for Teams) enrollment instead of consumer WARP.
117 pub teams: Option<TeamsEnrollment>,
118}
119
120/// Cloudflare for Teams (Zero Trust) enrollment configuration.
121///
122/// To obtain the JWT token:
123/// 1. Visit `https://<team-name>.cloudflareaccess.com/warp`
124/// 2. Authenticate as you would with the official WARP client
125/// 3. Extract the JWT token from the page source or use browser console:
126/// ```js
127/// console.log(document.querySelector("meta[http-equiv='refresh']").content.split("=")[2])
128/// ```
129#[derive(Debug, Clone)]
130#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
131pub struct TeamsEnrollment {
132 /// JWT token obtained from the Teams authentication portal.
133 ///
134 /// This is an ephemeral token that expires shortly after being issued.
135 pub jwt_token: String,
136
137 /// Optional device name shown in the Zero Trust dashboard.
138 pub device_name: Option<String>,
139
140 /// Optional device serial number.
141 pub serial_number: Option<String>,
142}
143
144impl Default for RegistrationOptions {
145 fn default() -> Self {
146 Self {
147 device_model: "PC".to_string(),
148 license_key: None,
149 teams: None,
150 }
151 }
152}
153
154/// Credentials for an existing WARP device registration.
155///
156/// Store these to avoid re-registering on each use. Use [`get_config`] to
157/// obtain a fresh [`WireGuardConfig`] from existing credentials.
158///
159/// Enable the `serde` feature for JSON/TOML serialization support.
160#[derive(Debug, Clone)]
161#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
162pub struct WarpCredentials {
163 /// Unique device identifier assigned by Cloudflare.
164 pub device_id: String,
165
166 /// Bearer token for API authentication.
167 pub access_token: String,
168
169 /// WireGuard private key (32 bytes).
170 #[cfg_attr(feature = "serde", serde(with = "base64_serde"))]
171 pub private_key: [u8; 32],
172
173 /// Account license key.
174 pub license_key: String,
175
176 /// Client ID (3 bytes) for the WARP reserved field.
177 ///
178 /// This is put in the reserved field in the WireGuard header and is used
179 /// by Cloudflare to identify the device/account. This is required for
180 /// proper routing, especially with Cloudflare for Teams.
181 #[cfg_attr(
182 feature = "serde",
183 serde(
184 default,
185 skip_serializing_if = "Option::is_none",
186 with = "base64_opt_serde"
187 )
188 )]
189 pub client_id: Option<[u8; 3]>,
190
191 /// Whether this is a Cloudflare for Teams (Zero Trust) enrollment.
192 #[cfg_attr(feature = "serde", serde(default))]
193 pub is_teams: bool,
194}
195
196/// Serde helper module for base64-encoding the private key.
197#[cfg(feature = "serde")]
198mod base64_serde {
199 use base64::{engine::general_purpose::STANDARD, Engine};
200 use serde::{Deserialize, Deserializer, Serializer};
201
202 pub fn serialize<S: Serializer>(bytes: &[u8; 32], s: S) -> Result<S::Ok, S::Error> {
203 s.serialize_str(&STANDARD.encode(bytes))
204 }
205
206 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 32], D::Error> {
207 let s = String::deserialize(d)?;
208 let bytes = STANDARD
209 .decode(&s)
210 .map_err(serde::de::Error::custom)?;
211 bytes
212 .try_into()
213 .map_err(|_| serde::de::Error::custom("invalid key length, expected 32 bytes"))
214 }
215}
216
217/// Serde helper module for base64-encoding the optional client_id.
218#[cfg(feature = "serde")]
219mod base64_opt_serde {
220 use base64::{engine::general_purpose::STANDARD, Engine};
221 use serde::{Deserialize, Deserializer, Serializer};
222
223 pub fn serialize<S: Serializer>(bytes: &Option<[u8; 3]>, s: S) -> Result<S::Ok, S::Error> {
224 match bytes {
225 Some(b) => s.serialize_some(&STANDARD.encode(b)),
226 None => s.serialize_none(),
227 }
228 }
229
230 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<[u8; 3]>, D::Error> {
231 let opt: Option<String> = Option::deserialize(d)?;
232 match opt {
233 Some(s) => {
234 let bytes = STANDARD
235 .decode(&s)
236 .map_err(serde::de::Error::custom)?;
237 let arr: [u8; 3] = bytes
238 .try_into()
239 .map_err(|_| serde::de::Error::custom("invalid client_id length, expected 3 bytes"))?;
240 Ok(Some(arr))
241 }
242 None => Ok(None),
243 }
244 }
245}
246
247impl WarpCredentials {
248 /// Get the private key as a base64-encoded string.
249 pub fn private_key_base64(&self) -> String {
250 STANDARD.encode(self.private_key)
251 }
252
253 /// Get the client ID as a base64-encoded string.
254 pub fn client_id_base64(&self) -> Option<String> {
255 self.client_id.map(|id| STANDARD.encode(id))
256 }
257
258 /// Get the client ID as a hex string (e.g., "0xaabbcc").
259 pub fn client_id_hex(&self) -> Option<String> {
260 self.client_id
261 .map(|id| format!("0x{:02x}{:02x}{:02x}", id[0], id[1], id[2]))
262 }
263
264 /// Get the client ID as decimal bytes (e.g., "[170, 187, 204]").
265 pub fn client_id_decimal(&self) -> Option<[u8; 3]> {
266 self.client_id
267 }
268}
269
270/// Register a new device with Cloudflare WARP and get a WireGuard configuration.
271///
272/// This creates a new device registration with Cloudflare's WARP service and
273/// returns both the WireGuard configuration and credentials for future use.
274///
275/// Supports both consumer WARP and Cloudflare for Teams (Zero Trust) enrollment.
276///
277/// # Arguments
278///
279/// * `options` - Registration options including device model and optional license key.
280///
281/// # Returns
282///
283/// A tuple of `(WireGuardConfig, WarpCredentials)` on success.
284///
285/// # Example
286///
287/// ```no_run
288/// use warp_wireguard_gen::{register, RegistrationOptions, TeamsEnrollment};
289///
290/// # async fn example() -> warp_wireguard_gen::Result<()> {
291/// // Basic registration (consumer WARP)
292/// let (config, creds) = register(RegistrationOptions::default()).await?;
293///
294/// // With custom device name
295/// let (config, creds) = register(RegistrationOptions {
296/// device_model: "MyApp/1.0".to_string(),
297/// license_key: None,
298/// teams: None,
299/// }).await?;
300///
301/// // With Warp+ license
302/// let (config, creds) = register(RegistrationOptions {
303/// device_model: "PC".to_string(),
304/// license_key: Some("xxxxxxxx-xxxxxxxx-xxxxxxxx".to_string()),
305/// teams: None,
306/// }).await?;
307///
308/// // Cloudflare for Teams (Zero Trust) enrollment
309/// let (config, creds) = register(RegistrationOptions {
310/// device_model: "PC".to_string(),
311/// license_key: None,
312/// teams: Some(TeamsEnrollment {
313/// jwt_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...".to_string(),
314/// device_name: Some("My Device".to_string()),
315/// serial_number: None,
316/// }),
317/// }).await?;
318/// # Ok(())
319/// # }
320/// ```
321pub async fn register(options: RegistrationOptions) -> Result<(WireGuardConfig, WarpCredentials)> {
322 api::register(options).await
323}
324
325/// Get a WireGuard configuration using existing credentials.
326///
327/// Use this to refresh the configuration without creating a new registration.
328/// This is useful when you have saved credentials from a previous [`register`] call.
329///
330/// # Arguments
331///
332/// * `credentials` - Previously obtained credentials from [`register`].
333///
334/// # Example
335///
336/// ```no_run
337/// use warp_wireguard_gen::{get_config, WarpCredentials};
338///
339/// # async fn example(credentials: &WarpCredentials) -> warp_wireguard_gen::Result<()> {
340/// let config = get_config(credentials).await?;
341/// // Use config with wireguard-netstack...
342/// # Ok(())
343/// # }
344/// ```
345pub async fn get_config(credentials: &WarpCredentials) -> Result<WireGuardConfig> {
346 api::get_config(credentials).await
347}
348
349/// Update the license key on an existing registration.
350///
351/// Use this to bind a Warp+ subscription to an existing device.
352///
353/// # Arguments
354///
355/// * `credentials` - Existing device credentials.
356/// * `license_key` - Warp+ license key from the 1.1.1.1 app.
357///
358/// # Note
359///
360/// Only subscriptions purchased directly from the official 1.1.1.1 app are
361/// supported. Keys obtained by other means (including referrals) will not work.
362///
363/// # Example
364///
365/// ```no_run
366/// use warp_wireguard_gen::{update_license, WarpCredentials};
367///
368/// # async fn example(credentials: &WarpCredentials) -> warp_wireguard_gen::Result<()> {
369/// update_license(credentials, "xxxxxxxx-xxxxxxxx-xxxxxxxx").await?;
370/// # Ok(())
371/// # }
372/// ```
373pub async fn update_license(credentials: &WarpCredentials, license_key: &str) -> Result<()> {
374 api::update_license(credentials, license_key).await
375}
376
377/// Generate a new X25519 keypair.
378///
379/// This is exposed for advanced use cases where you want to provide your own key
380/// during registration. Most users should use [`register`] which generates keys automatically.
381///
382/// # Returns
383///
384/// A tuple of `(private_key, public_key)` as 32-byte arrays.
385pub fn generate_keypair() -> ([u8; 32], [u8; 32]) {
386 keys::generate_keypair()
387}