Skip to main content

corevpn_config/
client.rs

1//! Client Configuration
2
3use serde::{Deserialize, Serialize};
4
5/// Client configuration for .ovpn file generation
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ClientConfig {
8    /// Client name/identifier
9    pub name: String,
10    /// Server hostname/IP
11    pub remote_host: String,
12    /// Server port
13    pub remote_port: u16,
14    /// Protocol (udp or tcp)
15    pub protocol: String,
16    /// CA certificate (PEM)
17    pub ca_cert: String,
18    /// Client certificate (PEM)
19    pub client_cert: String,
20    /// Client private key (PEM)
21    pub client_key: String,
22    /// TLS auth key (if enabled)
23    pub tls_auth_key: Option<String>,
24    /// TLS crypt key (if enabled)
25    pub tls_crypt_key: Option<String>,
26    /// Cipher
27    pub cipher: String,
28    /// Auth digest
29    pub auth: String,
30    /// Key direction for tls-auth
31    pub key_direction: Option<u8>,
32    /// Additional options
33    pub extra_options: Vec<String>,
34}
35
36impl ClientConfig {
37    /// Validate PEM format
38    fn validate_pem(content: &str, expected_type: &str) -> Result<(), crate::ConfigError> {
39        let content = content.trim();
40        
41        // Check for proper PEM headers/footers
42        let header = format!("-----BEGIN {}", expected_type);
43        let footer = format!("-----END {}", expected_type);
44        
45        if !content.starts_with(&header) {
46            return Err(crate::ConfigError::ValidationError(format!(
47                "Invalid PEM format: missing BEGIN {} header",
48                expected_type
49            )));
50        }
51        
52        if !content.ends_with(&footer) {
53            return Err(crate::ConfigError::ValidationError(format!(
54                "Invalid PEM format: missing END {} footer",
55                expected_type
56            )));
57        }
58        
59        // Basic validation: ensure there's content between headers
60        // Find the end of the header line
61        let header_line_end = content.find('\n').or_else(|| content.find('\r'));
62        if let Some(header_end) = header_line_end {
63            // Find where the footer starts
64            if let Some(footer_start) = content.rfind(&footer) {
65                if footer_start <= header_end {
66                    return Err(crate::ConfigError::ValidationError(format!(
67                        "Invalid PEM format: empty or malformed {} content",
68                        expected_type
69                    )));
70                }
71                // Check that there's actual base64-like content (at least some non-whitespace)
72                let body = &content[header_end + 1..footer_start];
73                if body.trim().is_empty() {
74                    return Err(crate::ConfigError::ValidationError(format!(
75                        "Invalid PEM format: empty {} content",
76                        expected_type
77                    )));
78                }
79            }
80        }
81        
82        Ok(())
83    }
84
85    /// Validate all certificates and keys before generating .ovpn
86    pub fn validate(&self) -> Result<(), crate::ConfigError> {
87        // Validate CA certificate
88        Self::validate_pem(&self.ca_cert, "CERTIFICATE")
89            .map_err(|e| crate::ConfigError::ValidationError(format!("CA certificate: {}", e)))?;
90        
91        // Validate client certificate
92        Self::validate_pem(&self.client_cert, "CERTIFICATE")
93            .map_err(|e| crate::ConfigError::ValidationError(format!("Client certificate: {}", e)))?;
94        
95        // Validate client key (can be PRIVATE KEY or RSA PRIVATE KEY)
96        let key_valid = Self::validate_pem(&self.client_key, "PRIVATE KEY").is_ok()
97            || Self::validate_pem(&self.client_key, "RSA PRIVATE KEY").is_ok()
98            || Self::validate_pem(&self.client_key, "EC PRIVATE KEY").is_ok();
99        
100        if !key_valid {
101            return Err(crate::ConfigError::ValidationError(
102                "Client key: Invalid PEM format - must be PRIVATE KEY, RSA PRIVATE KEY, or EC PRIVATE KEY".into(),
103            ));
104        }
105        
106        // Validate tls-auth key if present
107        if let Some(ta_key) = &self.tls_auth_key {
108            if ta_key.trim().is_empty() {
109                return Err(crate::ConfigError::ValidationError(
110                    "tls-auth key cannot be empty".into(),
111                ));
112            }
113            // tls-auth keys are typically base64 encoded, not PEM
114            // Just check they're not empty and have reasonable length
115            if ta_key.trim().len() < 32 {
116                return Err(crate::ConfigError::ValidationError(
117                    "tls-auth key appears to be too short".into(),
118                ));
119            }
120        }
121        
122        // Validate tls-crypt key if present
123        if let Some(tc_key) = &self.tls_crypt_key {
124            if tc_key.trim().is_empty() {
125                return Err(crate::ConfigError::ValidationError(
126                    "tls-crypt key cannot be empty".into(),
127                ));
128            }
129            if tc_key.trim().len() < 32 {
130                return Err(crate::ConfigError::ValidationError(
131                    "tls-crypt key appears to be too short".into(),
132                ));
133            }
134        }
135        
136        Ok(())
137    }
138
139    /// Generate .ovpn file contents
140    /// 
141    /// # Panics
142    /// Panics if the configuration is invalid. Use `validate()` before calling this method
143    /// to check for errors without panicking.
144    pub fn to_ovpn(&self) -> String {
145        // Validate before generating
146        // We panic here because malformed configs should never be written
147        // Callers should validate first if they want to handle errors gracefully
148        self.validate().expect("Invalid client configuration");
149        let mut lines = vec![
150            "# CoreVPN Client Configuration".to_string(),
151            "# Generated automatically - do not edit".to_string(),
152            "".to_string(),
153            "client".to_string(),
154            "dev tun".to_string(),
155            format!("proto {}", self.protocol),
156            format!("remote {} {}", self.remote_host, self.remote_port),
157            "resolv-retry infinite".to_string(),
158            "nobind".to_string(),
159            "persist-key".to_string(),
160            "persist-tun".to_string(),
161            "remote-cert-tls server".to_string(),
162            format!("cipher {}", self.cipher),
163            format!("auth {}", self.auth),
164            "verb 3".to_string(),
165            "".to_string(),
166            "# Security settings".to_string(),
167            "tls-client".to_string(),
168            "tls-version-min 1.3".to_string(),
169            "".to_string(),
170        ];
171
172        // Add extra options
173        for opt in &self.extra_options {
174            lines.push(opt.clone());
175        }
176        lines.push("".to_string());
177
178        // Add inline certificates
179        lines.push("<ca>".to_string());
180        lines.push(self.ca_cert.trim().to_string());
181        lines.push("</ca>".to_string());
182        lines.push("".to_string());
183
184        lines.push("<cert>".to_string());
185        lines.push(self.client_cert.trim().to_string());
186        lines.push("</cert>".to_string());
187        lines.push("".to_string());
188
189        lines.push("<key>".to_string());
190        lines.push(self.client_key.trim().to_string());
191        lines.push("</key>".to_string());
192        lines.push("".to_string());
193
194        // Add tls-auth or tls-crypt
195        if let Some(key) = &self.tls_crypt_key {
196            lines.push("<tls-crypt>".to_string());
197            lines.push(key.trim().to_string());
198            lines.push("</tls-crypt>".to_string());
199        } else if let Some(key) = &self.tls_auth_key {
200            if let Some(dir) = self.key_direction {
201                lines.push(format!("key-direction {}", dir));
202            }
203            lines.push("<tls-auth>".to_string());
204            lines.push(key.trim().to_string());
205            lines.push("</tls-auth>".to_string());
206        }
207
208        lines.join("\n")
209    }
210
211    /// Generate a minimal .ovpn for mobile devices
212    pub fn to_ovpn_mobile(&self) -> String {
213        // Same as regular but with mobile-optimized settings
214        let mut config = self.clone();
215        config.extra_options.push("# Mobile optimizations".to_string());
216        config.extra_options.push("connect-retry 2".to_string());
217        config.extra_options.push("connect-retry-max 5".to_string());
218        config.extra_options.push("auth-retry interact".to_string());
219        config.to_ovpn()
220    }
221}
222
223/// Builder for client configuration
224pub struct ClientConfigBuilder {
225    name: String,
226    remote_host: String,
227    remote_port: u16,
228    protocol: String,
229    ca_cert: String,
230    client_cert: String,
231    client_key: String,
232    tls_auth_key: Option<String>,
233    tls_crypt_key: Option<String>,
234    cipher: String,
235    auth: String,
236    key_direction: Option<u8>,
237    extra_options: Vec<String>,
238}
239
240impl ClientConfigBuilder {
241    /// Create a new builder
242    pub fn new(name: &str, remote_host: &str) -> Self {
243        Self {
244            name: name.to_string(),
245            remote_host: remote_host.to_string(),
246            remote_port: 1194,
247            protocol: "udp".to_string(),
248            ca_cert: String::new(),
249            client_cert: String::new(),
250            client_key: String::new(),
251            tls_auth_key: None,
252            tls_crypt_key: None,
253            cipher: "AES-256-GCM".to_string(),
254            auth: "SHA256".to_string(),
255            key_direction: Some(1),
256            extra_options: vec![],
257        }
258    }
259
260    /// Set remote port
261    pub fn port(mut self, port: u16) -> Self {
262        self.remote_port = port;
263        self
264    }
265
266    /// Set protocol
267    pub fn protocol(mut self, proto: &str) -> Self {
268        self.protocol = proto.to_string();
269        self
270    }
271
272    /// Set CA certificate
273    pub fn ca_cert(mut self, cert: &str) -> Self {
274        self.ca_cert = cert.to_string();
275        self
276    }
277
278    /// Set client certificate
279    pub fn client_cert(mut self, cert: &str) -> Self {
280        self.client_cert = cert.to_string();
281        self
282    }
283
284    /// Set client private key
285    pub fn client_key(mut self, key: &str) -> Self {
286        self.client_key = key.to_string();
287        self
288    }
289
290    /// Set tls-auth key
291    pub fn tls_auth(mut self, key: &str, direction: u8) -> Self {
292        self.tls_auth_key = Some(key.to_string());
293        self.key_direction = Some(direction);
294        self
295    }
296
297    /// Set tls-crypt key
298    pub fn tls_crypt(mut self, key: &str) -> Self {
299        self.tls_crypt_key = Some(key.to_string());
300        self.tls_auth_key = None;
301        self
302    }
303
304    /// Set cipher
305    pub fn cipher(mut self, cipher: &str) -> Self {
306        self.cipher = cipher.to_string();
307        self
308    }
309
310    /// Add extra option
311    pub fn extra_option(mut self, opt: &str) -> Self {
312        self.extra_options.push(opt.to_string());
313        self
314    }
315
316    /// Build the configuration
317    pub fn build(self) -> ClientConfig {
318        ClientConfig {
319            name: self.name,
320            remote_host: self.remote_host,
321            remote_port: self.remote_port,
322            protocol: self.protocol,
323            ca_cert: self.ca_cert,
324            client_cert: self.client_cert,
325            client_key: self.client_key,
326            tls_auth_key: self.tls_auth_key,
327            tls_crypt_key: self.tls_crypt_key,
328            cipher: self.cipher,
329            auth: self.auth,
330            key_direction: self.key_direction,
331            extra_options: self.extra_options,
332        }
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_client_config_builder() {
342        let config = ClientConfigBuilder::new("testuser", "vpn.example.com")
343            .port(443)
344            .protocol("tcp")
345            .ca_cert("CA CERT")
346            .client_cert("CLIENT CERT")
347            .client_key("CLIENT KEY")
348            .tls_auth("TA KEY", 1)
349            .build();
350
351        assert_eq!(config.name, "testuser");
352        assert_eq!(config.remote_port, 443);
353        assert_eq!(config.protocol, "tcp");
354    }
355
356    #[test]
357    fn test_ovpn_generation() {
358        let config = ClientConfigBuilder::new("test", "vpn.example.com")
359            .ca_cert("-----BEGIN CERTIFICATE-----\nTEST\n-----END CERTIFICATE-----")
360            .client_cert("-----BEGIN CERTIFICATE-----\nCLIENT\n-----END CERTIFICATE-----")
361            .client_key("-----BEGIN PRIVATE KEY-----\nKEY\n-----END PRIVATE KEY-----")
362            .build();
363
364        let ovpn = config.to_ovpn();
365
366        assert!(ovpn.contains("client"));
367        assert!(ovpn.contains("remote vpn.example.com 1194"));
368        assert!(ovpn.contains("<ca>"));
369        assert!(ovpn.contains("<cert>"));
370        assert!(ovpn.contains("<key>"));
371    }
372}