1use std::net::SocketAddr;
7use std::path::Path;
8
9use anyhow::{Context, Result, bail};
10
11#[derive(Debug)]
13pub struct OvpnConfig {
14 pub remote: SocketAddr,
16 pub protocol: String,
18 pub cipher: String,
20 pub data_ciphers: Vec<String>,
22 pub auth: String,
24 pub verb: u8,
26 pub ca_pem: String,
28 pub cert_pem: String,
30 pub key_pem: String,
32 pub tls_auth_key: Option<Vec<u8>>,
34 pub key_direction: Option<u8>,
36 pub remote_cert_tls: bool,
38 pub dev: String,
40}
41
42impl OvpnConfig {
43 pub fn parse_file(path: &Path) -> Result<Self> {
45 let content = std::fs::read_to_string(path)
46 .with_context(|| format!("Failed to read .ovpn file: {}", path.display()))?;
47 Self::parse(&content)
48 }
49
50 pub fn parse(content: &str) -> Result<Self> {
52 let mut remote_host = String::new();
53 let mut remote_port: u16 = 1194;
54 let mut protocol = "udp".to_string();
55 let mut cipher = "AES-256-GCM".to_string();
56 let mut data_ciphers = Vec::new();
57 let mut auth = "SHA256".to_string();
58 let mut verb: u8 = 3;
59 let mut remote_cert_tls = false;
60 let mut key_direction: Option<u8> = None;
61 let mut dev = "tun".to_string();
62
63 let ca_pem = extract_inline_block(content, "ca")
65 .context("Missing <ca> block in .ovpn file")?;
66 let cert_pem = extract_inline_block(content, "cert")
67 .context("Missing <cert> block in .ovpn file")?;
68 let key_pem = extract_inline_block(content, "key")
69 .context("Missing <key> block in .ovpn file")?;
70 let tls_auth_raw = extract_inline_block(content, "tls-auth");
71
72 let tls_auth_key = if let Some(ref raw) = tls_auth_raw {
74 Some(parse_static_key(raw)?)
75 } else {
76 None
77 };
78
79 for line in content.lines() {
81 let line = line.trim();
82
83 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
85 continue;
86 }
87
88 let mut parts = line.split_whitespace();
89 match parts.next() {
90 Some("remote") => {
91 if let Some(host) = parts.next() {
92 remote_host = host.to_string();
93 }
94 if let Some(port) = parts.next() {
95 remote_port = port.parse().unwrap_or(1194);
96 }
97 }
98 Some("proto") => {
99 if let Some(p) = parts.next() {
100 protocol = p.to_string();
101 }
102 }
103 Some("cipher") => {
104 if let Some(c) = parts.next() {
105 cipher = c.to_string();
106 }
107 }
108 Some("data-ciphers") => {
109 if let Some(ciphers) = parts.next() {
110 data_ciphers = ciphers.split(':').map(|s| s.to_string()).collect();
111 }
112 }
113 Some("auth") => {
114 if let Some(a) = parts.next() {
115 auth = a.to_string();
116 }
117 }
118 Some("verb") => {
119 if let Some(v) = parts.next() {
120 verb = v.parse().unwrap_or(3);
121 }
122 }
123 Some("remote-cert-tls") => {
124 remote_cert_tls = true;
125 }
126 Some("key-direction") => {
127 if let Some(d) = parts.next() {
128 key_direction = Some(d.parse().unwrap_or(1));
129 }
130 }
131 Some("dev") => {
132 if let Some(d) = parts.next() {
133 dev = d.to_string();
134 }
135 }
136 _ => {} }
138 }
139
140 if remote_host.is_empty() {
141 bail!("Missing 'remote' directive in .ovpn file");
142 }
143
144 let remote: SocketAddr = format!("{}:{}", remote_host, remote_port)
146 .parse()
147 .with_context(|| format!("Invalid remote address: {}:{}", remote_host, remote_port))?;
148
149 Ok(Self {
150 remote,
151 protocol,
152 cipher,
153 data_ciphers,
154 auth,
155 verb,
156 ca_pem,
157 cert_pem,
158 key_pem,
159 tls_auth_key,
160 key_direction,
161 remote_cert_tls,
162 dev,
163 })
164 }
165}
166
167fn extract_inline_block(content: &str, tag: &str) -> Option<String> {
169 let start_tag = format!("<{}>", tag);
170 let end_tag = format!("</{}>", tag);
171
172 let start_idx = content.find(&start_tag)?;
173 let end_idx = content.find(&end_tag)?;
174
175 if start_idx >= end_idx {
176 return None;
177 }
178
179 let block_start = start_idx + start_tag.len();
180 let block = &content[block_start..end_idx];
181 Some(block.trim().to_string())
182}
183
184fn parse_static_key(pem_block: &str) -> Result<Vec<u8>> {
186 let mut hex_data = String::new();
187
188 for line in pem_block.lines() {
189 let line = line.trim();
190 if line.is_empty()
192 || line.starts_with('#')
193 || line.starts_with('-')
194 || line.contains("OpenVPN Static key")
195 {
196 continue;
197 }
198 hex_data.push_str(line);
199 }
200
201 let bytes = hex_decode(&hex_data)
203 .context("Failed to decode tls-auth key hex data")?;
204
205 if bytes.len() != 256 {
206 bail!(
207 "tls-auth key has wrong length: expected 256 bytes, got {}",
208 bytes.len()
209 );
210 }
211
212 Ok(bytes)
213}
214
215fn hex_decode(hex: &str) -> Result<Vec<u8>> {
217 if hex.len() % 2 != 0 {
218 bail!("Hex string has odd length");
219 }
220
221 let mut bytes = Vec::with_capacity(hex.len() / 2);
222 for i in (0..hex.len()).step_by(2) {
223 let byte = u8::from_str_radix(&hex[i..i + 2], 16)
224 .with_context(|| format!("Invalid hex at position {}", i))?;
225 bytes.push(byte);
226 }
227 Ok(bytes)
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn test_extract_inline_block() {
236 let content = "<ca>\n-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----\n</ca>";
237 let block = extract_inline_block(content, "ca").unwrap();
238 assert!(block.contains("BEGIN CERTIFICATE"));
239 }
240
241 #[test]
242 fn test_hex_decode() {
243 let bytes = hex_decode("deadbeef").unwrap();
244 assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]);
245 }
246}