Skip to main content

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}