1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ClientConfig {
8 pub name: String,
10 pub remote_host: String,
12 pub remote_port: u16,
14 pub protocol: String,
16 pub ca_cert: String,
18 pub client_cert: String,
20 pub client_key: String,
22 pub tls_auth_key: Option<String>,
24 pub tls_crypt_key: Option<String>,
26 pub cipher: String,
28 pub auth: String,
30 pub key_direction: Option<u8>,
32 pub extra_options: Vec<String>,
34}
35
36impl ClientConfig {
37 fn validate_pem(content: &str, expected_type: &str) -> Result<(), crate::ConfigError> {
39 let content = content.trim();
40
41 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 let header_line_end = content.find('\n').or_else(|| content.find('\r'));
62 if let Some(header_end) = header_line_end {
63 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 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 pub fn validate(&self) -> Result<(), crate::ConfigError> {
87 Self::validate_pem(&self.ca_cert, "CERTIFICATE")
89 .map_err(|e| crate::ConfigError::ValidationError(format!("CA certificate: {}", e)))?;
90
91 Self::validate_pem(&self.client_cert, "CERTIFICATE")
93 .map_err(|e| crate::ConfigError::ValidationError(format!("Client certificate: {}", e)))?;
94
95 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 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 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 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 pub fn to_ovpn(&self) -> String {
145 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 for opt in &self.extra_options {
174 lines.push(opt.clone());
175 }
176 lines.push("".to_string());
177
178 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 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 pub fn to_ovpn_mobile(&self) -> String {
213 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
223pub 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 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 pub fn port(mut self, port: u16) -> Self {
262 self.remote_port = port;
263 self
264 }
265
266 pub fn protocol(mut self, proto: &str) -> Self {
268 self.protocol = proto.to_string();
269 self
270 }
271
272 pub fn ca_cert(mut self, cert: &str) -> Self {
274 self.ca_cert = cert.to_string();
275 self
276 }
277
278 pub fn client_cert(mut self, cert: &str) -> Self {
280 self.client_cert = cert.to_string();
281 self
282 }
283
284 pub fn client_key(mut self, key: &str) -> Self {
286 self.client_key = key.to_string();
287 self
288 }
289
290 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 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 pub fn cipher(mut self, cipher: &str) -> Self {
306 self.cipher = cipher.to_string();
307 self
308 }
309
310 pub fn extra_option(mut self, opt: &str) -> Self {
312 self.extra_options.push(opt.to_string());
313 self
314 }
315
316 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}