corevpn_protocol/
control.rs

1//! Control Channel Message Types
2
3use bytes::Bytes;
4use serde::{Deserialize, Serialize};
5
6use crate::{ProtocolError, Result};
7
8/// Control channel message types
9#[derive(Debug, Clone)]
10pub enum ControlMessage {
11    /// TLS data (wrapped in control channel)
12    TlsData(Bytes),
13    /// Push request from client
14    PushRequest,
15    /// Push reply from server
16    PushReply(PushReply),
17    /// Authentication data
18    Auth(AuthMessage),
19    /// Info message (version, etc.)
20    Info(String),
21    /// Exit/shutdown
22    Exit,
23}
24
25/// Control packet for the reliable transport layer
26#[derive(Debug, Clone)]
27pub struct ControlPacket {
28    /// Packet ID for reliability
29    pub packet_id: u32,
30    /// Message content
31    pub message: ControlMessage,
32}
33
34impl ControlPacket {
35    /// Create a new control packet
36    pub fn new(packet_id: u32, message: ControlMessage) -> Self {
37        Self { packet_id, message }
38    }
39}
40
41/// Push reply containing VPN configuration
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct PushReply {
44    /// Routes to push
45    pub routes: Vec<PushRoute>,
46    /// IPv4 address and netmask
47    pub ifconfig: Option<(String, String)>,
48    /// IPv6 address
49    pub ifconfig_ipv6: Option<String>,
50    /// DNS servers
51    pub dns: Vec<String>,
52    /// Search domains
53    pub dns_search: Vec<String>,
54    /// Redirect gateway (full tunnel)
55    pub redirect_gateway: bool,
56    /// Topology type
57    pub topology: Topology,
58    /// Ping interval
59    pub ping: u32,
60    /// Ping restart timeout
61    pub ping_restart: u32,
62    /// Additional options
63    pub options: Vec<String>,
64}
65
66impl Default for PushReply {
67    fn default() -> Self {
68        Self {
69            routes: vec![],
70            ifconfig: None,
71            ifconfig_ipv6: None,
72            dns: vec![],
73            dns_search: vec![],
74            redirect_gateway: false,
75            topology: Topology::Subnet,
76            ping: 10,
77            ping_restart: 60,
78            options: vec![],
79        }
80    }
81}
82
83impl PushReply {
84    /// Encode as OpenVPN push reply string
85    pub fn encode(&self) -> String {
86        let mut parts = vec!["PUSH_REPLY".to_string()];
87
88        // Topology
89        parts.push(format!("topology {}", self.topology.as_str()));
90
91        // ifconfig
92        if let Some((ip, mask)) = &self.ifconfig {
93            parts.push(format!("ifconfig {} {}", ip, mask));
94        }
95
96        // ifconfig-ipv6
97        if let Some(ipv6) = &self.ifconfig_ipv6 {
98            parts.push(format!("ifconfig-ipv6 {}", ipv6));
99        }
100
101        // Routes
102        for route in &self.routes {
103            parts.push(route.encode());
104        }
105
106        // Redirect gateway
107        if self.redirect_gateway {
108            parts.push("redirect-gateway def1".to_string());
109        }
110
111        // DNS
112        for dns in &self.dns {
113            parts.push(format!("dhcp-option DNS {}", dns));
114        }
115
116        // DNS search domains
117        for domain in &self.dns_search {
118            parts.push(format!("dhcp-option DOMAIN {}", domain));
119        }
120
121        // Ping settings
122        parts.push(format!("ping {}", self.ping));
123        parts.push(format!("ping-restart {}", self.ping_restart));
124
125        // Additional options
126        for opt in &self.options {
127            parts.push(opt.clone());
128        }
129
130        parts.join(",")
131    }
132
133    /// Parse from OpenVPN push reply string
134    pub fn parse(s: &str) -> Result<Self> {
135        let mut reply = Self::default();
136
137        // Remove PUSH_REPLY prefix if present
138        let s = s.strip_prefix("PUSH_REPLY,").unwrap_or(s);
139
140        for part in s.split(',') {
141            let part = part.trim();
142            if part.is_empty() {
143                continue;
144            }
145
146            let mut tokens = part.split_whitespace();
147            match tokens.next() {
148                Some("topology") => {
149                    if let Some(topo) = tokens.next() {
150                        reply.topology = Topology::parse(topo);
151                    }
152                }
153                Some("ifconfig") => {
154                    let ip = tokens.next().unwrap_or("").to_string();
155                    let mask = tokens.next().unwrap_or("").to_string();
156                    reply.ifconfig = Some((ip, mask));
157                }
158                Some("ifconfig-ipv6") => {
159                    if let Some(ipv6) = tokens.next() {
160                        reply.ifconfig_ipv6 = Some(ipv6.to_string());
161                    }
162                }
163                Some("route") => {
164                    if let Ok(route) = PushRoute::parse(part) {
165                        reply.routes.push(route);
166                    }
167                }
168                Some("redirect-gateway") => {
169                    reply.redirect_gateway = true;
170                }
171                Some("dhcp-option") => {
172                    match tokens.next() {
173                        Some("DNS") => {
174                            if let Some(dns) = tokens.next() {
175                                reply.dns.push(dns.to_string());
176                            }
177                        }
178                        Some("DOMAIN") => {
179                            if let Some(domain) = tokens.next() {
180                                reply.dns_search.push(domain.to_string());
181                            }
182                        }
183                        _ => {}
184                    }
185                }
186                Some("ping") => {
187                    if let Some(Ok(p)) = tokens.next().map(|s| s.parse()) {
188                        reply.ping = p;
189                    }
190                }
191                Some("ping-restart") => {
192                    if let Some(Ok(p)) = tokens.next().map(|s| s.parse()) {
193                        reply.ping_restart = p;
194                    }
195                }
196                _ => {
197                    reply.options.push(part.to_string());
198                }
199            }
200        }
201
202        Ok(reply)
203    }
204}
205
206/// Route to push to client
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct PushRoute {
209    /// Network address
210    pub network: String,
211    /// Netmask
212    pub netmask: String,
213    /// Gateway (optional, vpn_gateway used if not set)
214    pub gateway: Option<String>,
215    /// Metric
216    pub metric: Option<u32>,
217}
218
219impl PushRoute {
220    /// Create a new route
221    pub fn new(network: &str, netmask: &str) -> Self {
222        Self {
223            network: network.to_string(),
224            netmask: netmask.to_string(),
225            gateway: None,
226            metric: None,
227        }
228    }
229
230    /// Encode as OpenVPN route directive
231    pub fn encode(&self) -> String {
232        let mut s = format!("route {} {}", self.network, self.netmask);
233        if let Some(gw) = &self.gateway {
234            s.push_str(&format!(" {}", gw));
235        } else {
236            s.push_str(" vpn_gateway");
237        }
238        if let Some(metric) = self.metric {
239            s.push_str(&format!(" {}", metric));
240        }
241        s
242    }
243
244    /// Parse from OpenVPN route directive
245    pub fn parse(s: &str) -> Result<Self> {
246        let mut tokens = s.split_whitespace();
247        tokens.next(); // skip "route"
248
249        let network = tokens
250            .next()
251            .ok_or_else(|| ProtocolError::InvalidPacket("missing network in route".into()))?
252            .to_string();
253
254        let netmask = tokens
255            .next()
256            .ok_or_else(|| ProtocolError::InvalidPacket("missing netmask in route".into()))?
257            .to_string();
258
259        let gateway = tokens.next().and_then(|g| {
260            if g == "vpn_gateway" {
261                None
262            } else {
263                Some(g.to_string())
264            }
265        });
266
267        let metric = tokens.next().and_then(|m| m.parse().ok());
268
269        Ok(Self {
270            network,
271            netmask,
272            gateway,
273            metric,
274        })
275    }
276}
277
278/// Network topology type
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
280pub enum Topology {
281    /// Point-to-point (net30)
282    Net30,
283    /// Point-to-point (p2p)
284    P2P,
285    /// Subnet mode (recommended)
286    #[default]
287    Subnet,
288}
289
290impl Topology {
291    /// Parse from string
292    pub fn parse(s: &str) -> Self {
293        match s.to_lowercase().as_str() {
294            "net30" => Topology::Net30,
295            "p2p" => Topology::P2P,
296            "subnet" => Topology::Subnet,
297            _ => Topology::Subnet,
298        }
299    }
300
301    /// Convert to string
302    pub fn as_str(&self) -> &'static str {
303        match self {
304            Topology::Net30 => "net30",
305            Topology::P2P => "p2p",
306            Topology::Subnet => "subnet",
307        }
308    }
309}
310
311/// Authentication message from client
312#[derive(Debug, Clone)]
313pub struct AuthMessage {
314    /// Username
315    pub username: String,
316    /// Password
317    pub password: String,
318}
319
320impl AuthMessage {
321    /// Parse from OpenVPN auth data
322    pub fn parse(data: &[u8]) -> Result<Self> {
323        // Format: username\0password\0
324        let s = std::str::from_utf8(data)
325            .map_err(|_| ProtocolError::InvalidPacket("invalid UTF-8 in auth".into()))?;
326
327        let parts: Vec<&str> = s.split('\0').collect();
328        if parts.len() < 2 {
329            return Err(ProtocolError::InvalidPacket("missing auth fields".into()));
330        }
331
332        Ok(Self {
333            username: parts[0].to_string(),
334            password: parts[1].to_string(),
335        })
336    }
337
338    /// Encode to OpenVPN auth format
339    pub fn encode(&self) -> Vec<u8> {
340        let mut data = Vec::new();
341        data.extend_from_slice(self.username.as_bytes());
342        data.push(0);
343        data.extend_from_slice(self.password.as_bytes());
344        data.push(0);
345        data
346    }
347}
348
349/// Key method v2 data (exchanged during TLS handshake)
350#[derive(Debug, Clone)]
351pub struct KeyMethodV2 {
352    /// Pre-master secret (48 bytes)
353    pub pre_master: [u8; 48],
354    /// Random data (32 bytes)
355    pub random: [u8; 32],
356    /// Options string
357    pub options: String,
358    /// Username (if using auth)
359    pub username: Option<String>,
360    /// Password (if using auth)
361    pub password: Option<String>,
362    /// Peer info
363    pub peer_info: Option<String>,
364}
365
366impl KeyMethodV2 {
367    /// Encode to bytes
368    pub fn encode(&self) -> Vec<u8> {
369        let mut buf = Vec::new();
370
371        // Literal 0
372        buf.extend_from_slice(&[0u8; 4]);
373
374        // Key method (2)
375        buf.push(2);
376
377        // Pre-master secret
378        buf.extend_from_slice(&self.pre_master);
379
380        // Random
381        buf.extend_from_slice(&self.random);
382
383        // Options string length + string
384        let opts_bytes = self.options.as_bytes();
385        buf.extend_from_slice(&(opts_bytes.len() as u16).to_be_bytes());
386        buf.extend_from_slice(opts_bytes);
387
388        // Username (optional)
389        if let Some(username) = &self.username {
390            let username_bytes = username.as_bytes();
391            buf.extend_from_slice(&(username_bytes.len() as u16).to_be_bytes());
392            buf.extend_from_slice(username_bytes);
393        } else {
394            buf.extend_from_slice(&0u16.to_be_bytes());
395        }
396
397        // Password (optional)
398        if let Some(password) = &self.password {
399            let password_bytes = password.as_bytes();
400            buf.extend_from_slice(&(password_bytes.len() as u16).to_be_bytes());
401            buf.extend_from_slice(password_bytes);
402        } else {
403            buf.extend_from_slice(&0u16.to_be_bytes());
404        }
405
406        // Peer info (optional)
407        if let Some(peer_info) = &self.peer_info {
408            let peer_info_bytes = peer_info.as_bytes();
409            buf.extend_from_slice(&(peer_info_bytes.len() as u16).to_be_bytes());
410            buf.extend_from_slice(peer_info_bytes);
411        }
412
413        buf
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    #[test]
422    fn test_push_reply_roundtrip() {
423        let mut reply = PushReply::default();
424        reply.ifconfig = Some(("10.8.0.2".to_string(), "255.255.255.0".to_string()));
425        reply.dns.push("1.1.1.1".to_string());
426        reply.routes.push(PushRoute::new("192.168.1.0", "255.255.255.0"));
427        reply.redirect_gateway = true;
428
429        let encoded = reply.encode();
430        let parsed = PushReply::parse(&encoded).unwrap();
431
432        assert_eq!(parsed.ifconfig, reply.ifconfig);
433        assert_eq!(parsed.dns, reply.dns);
434        assert!(parsed.redirect_gateway);
435    }
436
437    #[test]
438    fn test_auth_message() {
439        let auth = AuthMessage {
440            username: "user".to_string(),
441            password: "pass".to_string(),
442        };
443
444        let encoded = auth.encode();
445        let parsed = AuthMessage::parse(&encoded).unwrap();
446
447        assert_eq!(parsed.username, "user");
448        assert_eq!(parsed.password, "pass");
449    }
450}