openconnect_core/
lib.rs

1#![doc = include_str!("../README.md")]
2
3mod cert;
4pub mod command;
5pub mod config;
6pub mod elevator;
7pub mod events;
8mod form;
9pub mod ip_info;
10pub mod log;
11pub mod protocols;
12pub mod result;
13pub mod stats;
14pub mod storage;
15
16use crate::cert::PeerCerts;
17use crate::command::{CmdPipe, SIGNAL_HANDLE};
18use crate::config::{Config, Entrypoint, LogLevel};
19use crate::events::{EventHandlers, Events};
20use crate::form::FormManager;
21use crate::ip_info::IpInfo;
22use crate::log::Logger;
23use crate::result::{EmitError, OpenconnectError, OpenconnectResult};
24use crate::stats::Stats;
25
26use openconnect_sys::*;
27use std::{
28    ffi::CString,
29    sync::{
30        atomic::{AtomicI32, Ordering},
31        Arc, RwLock, Weak,
32    },
33};
34
35/// Describe the connection status of the client
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum Status {
38    /// The client is initialized
39    Initialized,
40
41    /// The client is disconnecting to the VPN server
42    Disconnecting,
43
44    /// The client is disconnected from the VPN server and command pipe is closed
45    Disconnected,
46
47    /// The client is connecting to the VPN server, with several stages described in the string
48    Connecting(String),
49
50    /// The client is connected to the VPN server and the main loop is running
51    Connected,
52
53    /// The client is in an error state
54    Error(OpenconnectError),
55}
56
57/// VpnClient struct
58///
59/// This struct is the main entrypoint for interacting with the Openconnect C library (on top of [openconnect-sys](https://crates.io/crates/openconnect-sys))
60#[repr(C)]
61pub struct VpnClient {
62    vpninfo: *mut openconnect_info,
63    config: Config,
64    cmd_fd: AtomicI32,
65    status: RwLock<Status>,
66    callbacks: EventHandlers,
67    entrypoint: RwLock<Option<Entrypoint>>,
68    form_manager: RwLock<FormManager>,
69    peer_certs: PeerCerts,
70}
71
72unsafe impl Send for VpnClient {}
73unsafe impl Sync for VpnClient {}
74
75impl VpnClient {
76    pub(crate) extern "C" fn default_setup_tun_vfn(privdata: *mut ::std::os::raw::c_void) {
77        let client = unsafe { VpnClient::ref_from_raw(privdata) };
78
79        #[cfg(target_os = "windows")]
80        {
81            // currently use wintun on windows
82            // https://gitlab.com/openconnect/openconnect-gui/-/blob/main/src/vpninfo.cpp?ref_type=heads#L407
83            // TODO: investigate tap ip address allocation, since it works well in Openconnect-GUI
84            let ifname = client
85                .get_hostname()
86                .map(|hostname| format!("tun_{}", hostname));
87
88            println!("ifname: {:?}", ifname);
89
90            // TODO: handle result
91            let _result = client.setup_tun_device(None, ifname);
92        }
93
94        #[cfg(not(target_os = "windows"))]
95        {
96            // TODO: handle result
97            let _result = client.setup_tun_device(None, None);
98        }
99    }
100
101    /// Reclaim a reference from c_void
102    ///
103    /// SAFETY: You must ensure that the pointer is valid and points to a valid instance of `Self`
104    pub(crate) unsafe fn ref_from_raw<'a>(ptr: *mut std::os::raw::c_void) -> &'a Self {
105        let ptr = ptr.cast::<Self>();
106        &*ptr
107    }
108
109    pub(crate) fn handle_text_input(&self, field_name: &str) -> Option<String> {
110        let entrypoint = self.entrypoint.read().ok()?;
111        let entrypoint = (*entrypoint).as_ref()?;
112        match field_name {
113            "username" | "user" | "uname" => entrypoint.username.clone(),
114            _ => todo!("handle_text_input: {}", field_name),
115        }
116    }
117
118    pub(crate) fn handle_password_input(&self) -> Option<String> {
119        let entrypoint = self.entrypoint.read().ok()?;
120        (*entrypoint).as_ref()?.password.clone()
121    }
122
123    pub(crate) fn handle_stats(&self, (dlts, stats): (Option<String>, Option<Stats>)) {
124        println!("stats: {:?}, {:?}", dlts, stats);
125    }
126
127    pub(crate) fn handle_accept_insecure_cert(&self, fingerprint: &str) -> bool {
128        let entrypoint = self.entrypoint.read();
129        let accept_in_entrypoint_config = {
130            if let Ok(entrypoint) = entrypoint {
131                (*entrypoint)
132                    .as_ref()
133                    .map(|entrypoint| entrypoint.accept_insecure_cert)
134                    .unwrap_or(false)
135            } else {
136                false
137            }
138        };
139
140        if accept_in_entrypoint_config {
141            return true;
142        }
143
144        if let Some(ref handler) = self.callbacks.handle_peer_cert_invalid {
145            handler(fingerprint)
146        } else {
147            false
148        }
149    }
150
151    pub fn set_loglevel(&self, level: LogLevel) {
152        unsafe {
153            openconnect_set_loglevel(self.vpninfo, level as i32);
154        }
155    }
156
157    pub fn set_protocol(&self, protocol: &str) -> OpenconnectResult<()> {
158        let protocol =
159            CString::new(protocol).map_err(|_| OpenconnectError::SetProtocolError(libc::EIO))?;
160        let ret = unsafe { openconnect_set_protocol(self.vpninfo, protocol.as_ptr()) };
161        match ret {
162            0 => Ok(()),
163            _ => Err(OpenconnectError::SetProtocolError(ret)),
164        }
165    }
166
167    pub fn set_stats_handler(&self) {
168        unsafe {
169            openconnect_set_stats_handler(self.vpninfo, Some(stats::stats_fn));
170        }
171    }
172
173    pub fn setup_tun_device(
174        &self,
175        vpnc_script: Option<String>,
176        ifname: Option<String>,
177    ) -> OpenconnectResult<()> {
178        let vpnc_script_from_config = vpnc_script.or_else(|| self.config.vpncscript.clone());
179
180        let vpnc_script = {
181            if let Some(vpnc_script) = vpnc_script_from_config {
182                CString::new(vpnc_script)
183                    .map_err(|_| OpenconnectError::SetupTunDeviceEror(libc::EIO))?
184            } else {
185                #[cfg(not(target_os = "windows"))]
186                const DEFAULT_SCRIPT: &str = "./vpnc-script";
187
188                #[cfg(target_os = "windows")]
189                const DEFAULT_SCRIPT: &str = "./vpnc-script-win.js";
190
191                CString::new(DEFAULT_SCRIPT)
192                    .map_err(|_| OpenconnectError::SetupTunDeviceEror(libc::EIO))?
193            }
194        };
195
196        let ifname = ifname.and_then(|s| CString::new(s).ok());
197
198        let ret = unsafe {
199            openconnect_setup_tun_device(
200                self.vpninfo,
201                vpnc_script.as_ptr(),
202                ifname.as_ref().map_or_else(std::ptr::null, |s| s.as_ptr()),
203            )
204        };
205
206        let _manually_dropped = ifname; // SAFETY: dont remove this line, ifname's lifetime should be extended
207
208        match ret {
209            0 => Ok(()),
210            _ => Err(OpenconnectError::SetupTunDeviceEror(ret)),
211        }
212    }
213
214    pub fn set_setup_tun_handler(&self) {
215        unsafe {
216            openconnect_set_setup_tun_handler(self.vpninfo, Some(VpnClient::default_setup_tun_vfn));
217        }
218    }
219
220    pub fn set_report_os(&self, os: &str) -> OpenconnectResult<()> {
221        let os = CString::new(os).map_err(|_| OpenconnectError::SetReportOSError(libc::EIO))?;
222        let ret = unsafe { openconnect_set_reported_os(self.vpninfo, os.as_ptr()) };
223        match ret {
224            0 => Ok(()),
225            _ => Err(OpenconnectError::SetReportOSError(ret)),
226        }
227    }
228
229    pub fn obtain_cookie(&self) -> OpenconnectResult<()> {
230        let ret = unsafe { openconnect_obtain_cookie(self.vpninfo) };
231        match ret {
232            0 => Ok(()),
233            _ => Err(result::OpenconnectError::ObtainCookieError(ret)),
234        }
235    }
236
237    pub fn set_cookie(&self, cookie: &str) -> OpenconnectResult<()> {
238        let cookie =
239            CString::new(cookie).map_err(|_| OpenconnectError::SetCookieError(libc::EIO))?;
240        let ret = unsafe { openconnect_set_cookie(self.vpninfo, cookie.as_ptr()) };
241        match ret {
242            0 => Ok(()),
243            _ => Err(OpenconnectError::SetCookieError(ret)),
244        }
245    }
246
247    pub fn clear_cookie(&self) {
248        unsafe {
249            openconnect_clear_cookie(self.vpninfo);
250        }
251    }
252
253    pub fn get_cookie(&self) -> Option<String> {
254        unsafe {
255            let cookie = openconnect_get_cookie(self.vpninfo);
256            std::ffi::CStr::from_ptr(cookie)
257                .to_str()
258                .map(|s| s.to_string())
259                .ok()
260        }
261    }
262
263    pub fn setup_cmd_pipe(&self) -> OpenconnectResult<()> {
264        let cmd_fd = unsafe {
265            let cmd_fd = openconnect_setup_cmd_pipe(self.vpninfo);
266            self.cmd_fd.store(cmd_fd, Ordering::Relaxed);
267            if cmd_fd < 0 {
268                return Err(result::OpenconnectError::CmdPipeError(cmd_fd));
269            }
270            cmd_fd
271        };
272        self.set_sock_block(cmd_fd);
273        Ok(())
274    }
275
276    pub fn reset_ssl(&self) {
277        unsafe {
278            openconnect_reset_ssl(self.vpninfo);
279        }
280    }
281
282    pub fn make_cstp_connection(&self) -> OpenconnectResult<()> {
283        let ret = unsafe { openconnect_make_cstp_connection(self.vpninfo) };
284        match ret {
285            0 => Ok(()),
286            _ => Err(OpenconnectError::MakeCstpError(ret)),
287        }
288    }
289
290    pub fn get_dlts_cipher(&self) -> Option<String> {
291        unsafe {
292            let cipher = openconnect_get_dtls_cipher(self.vpninfo);
293            if !cipher.is_null() {
294                Some(
295                    std::ffi::CStr::from_ptr(cipher)
296                        .to_str()
297                        .unwrap()
298                        .to_string(),
299                )
300            } else {
301                None
302            }
303        }
304    }
305
306    pub fn get_peer_cert_hash(&self) -> String {
307        // SAFETY: we should not use CString::from_raw(peer_fingerprint)
308        // because peer_fingerprint will be deallocated in rust and cause a double free
309        unsafe { std::ffi::CStr::from_ptr(openconnect_get_peer_cert_hash(self.vpninfo)) }
310            .to_string_lossy()
311            .to_string()
312    }
313
314    pub fn disable_dtls(&self) -> OpenconnectResult<()> {
315        let ret = unsafe { openconnect_disable_dtls(self.vpninfo) };
316        match ret {
317            0 => Ok(()),
318            _ => Err(OpenconnectError::DisableDTLSError(ret)),
319        }
320    }
321
322    pub fn set_http_proxy(&self, proxy: &str) -> OpenconnectResult<()> {
323        let proxy = CString::new(proxy).map_err(|_| OpenconnectError::SetProxyError(libc::EIO))?;
324        let ret = unsafe { openconnect_set_http_proxy(self.vpninfo, proxy.as_ptr()) };
325        match ret {
326            0 => Ok(()),
327            _ => Err(OpenconnectError::SetProxyError(ret)),
328        }
329    }
330
331    pub fn parse_url(&self, url: &str) -> OpenconnectResult<()> {
332        let url = CString::new(url).map_err(|_| OpenconnectError::ParseUrlError(libc::EIO))?;
333        let ret = unsafe { openconnect_parse_url(self.vpninfo, url.as_ptr()) };
334        match ret {
335            0 => Ok(()),
336            _ => Err(OpenconnectError::ParseUrlError(ret)),
337        }
338    }
339
340    pub fn get_server_name(&self) -> Option<String> {
341        {
342            let entrypoint = self.entrypoint.read().ok()?;
343            (*entrypoint).as_ref()?.name.clone()
344        }
345    }
346
347    pub fn get_server_url(&self) -> Option<String> {
348        {
349            let entrypoint = self.entrypoint.read().ok()?;
350            Some((*entrypoint).as_ref()?.server.clone())
351        }
352    }
353
354    pub fn get_port(&self) -> i32 {
355        unsafe { openconnect_get_port(self.vpninfo) }
356    }
357
358    pub fn get_hostname(&self) -> Option<String> {
359        unsafe {
360            let hostname = openconnect_get_hostname(self.vpninfo);
361            std::ffi::CStr::from_ptr(hostname)
362                .to_str()
363                .map(|s| s.to_string())
364                .ok()
365        }
366    }
367
368    pub fn set_client_cert(&self, cert: &str, sslkey: &str) -> OpenconnectResult<()> {
369        let cert =
370            CString::new(cert).map_err(|_| OpenconnectError::SetClientCertError(libc::EIO))?;
371        let sslkey =
372            CString::new(sslkey).map_err(|_| OpenconnectError::SetClientCertError(libc::EIO))?;
373        let ret =
374            unsafe { openconnect_set_client_cert(self.vpninfo, cert.as_ptr(), sslkey.as_ptr()) };
375        match ret {
376            0 => Ok(()),
377            _ => Err(OpenconnectError::SetClientCertError(ret)),
378        }
379    }
380
381    pub fn set_mca_cert(&self, cert: &str, key: &str) -> OpenconnectResult<()> {
382        let cert = CString::new(cert).map_err(|_| OpenconnectError::SetMCACertError(libc::EIO))?;
383        let key = CString::new(key).map_err(|_| OpenconnectError::SetMCACertError(libc::EIO))?;
384        let ret = unsafe { openconnect_set_mca_cert(self.vpninfo, cert.as_ptr(), key.as_ptr()) };
385        match ret {
386            0 => Ok(()),
387            _ => Err(OpenconnectError::SetMCACertError(ret)),
388        }
389    }
390
391    pub fn get_info(&self) -> OpenconnectResult<Option<IpInfo>> {
392        unsafe {
393            let info = std::ptr::null_mut();
394            let ret = openconnect_get_ip_info(
395                self.vpninfo,
396                info,
397                std::ptr::null_mut(),
398                std::ptr::null_mut(),
399            );
400
401            match ret {
402                0 => Ok(info
403                    .as_ref()
404                    .and_then(|info| info.as_ref())
405                    .map(IpInfo::from)),
406                _ => Err(OpenconnectError::GetIpInfoError(ret)),
407            }
408        }
409    }
410
411    pub(crate) fn main_loop(
412        &self,
413        reconnect_timeout: i32,
414        reconnect_interval: u32,
415    ) -> OpenconnectResult<()> {
416        let ret = unsafe {
417            openconnect_mainloop(self.vpninfo, reconnect_timeout, reconnect_interval as i32)
418        };
419        match ret {
420            0 => Ok(()),
421            _ => Err(OpenconnectError::MainLoopError(ret)),
422        }
423    }
424
425    pub(crate) fn free(&self) {
426        unsafe {
427            openconnect_vpninfo_free(self.vpninfo);
428        }
429        tracing::debug!("Client instance is dropped");
430    }
431}
432
433impl Drop for VpnClient {
434    fn drop(&mut self) {
435        self.disconnect();
436        self.free();
437    }
438}
439
440/// Trait for creating a new instance of VpnClient and connecting to the VPN server
441///
442/// This trait is implemented for the lifecycle of the VpnClient
443pub trait Connectable {
444    fn new(config: Config, callbacks: EventHandlers) -> OpenconnectResult<Arc<Self>>;
445    fn connect_for_cookie(&self, entrypoint: Entrypoint) -> OpenconnectResult<Option<String>>;
446    fn init_connection(&self, entrypoint: Entrypoint) -> OpenconnectResult<()>;
447    fn run_loop(&self) -> OpenconnectResult<()>;
448    fn disconnect(&self);
449    fn get_status(&self) -> Status;
450    fn get_server_name(&self) -> Option<String>;
451}
452
453impl Connectable for VpnClient {
454    /// Create a new instance of VpnClient
455    ///
456    /// config can be created using [config::ConfigBuilder]
457    ///
458    /// callbacks can be created using [events::EventHandlers]
459    fn new(config: Config, callbacks: EventHandlers) -> OpenconnectResult<Arc<Self>> {
460        let useragent = std::ffi::CString::new("AnyConnect-compatible OpenConnect VPN Agent")
461            .map_err(|_| OpenconnectError::OtherError("useragent is not valid".to_string()))?;
462
463        let instance = Arc::new(Self {
464            vpninfo: std::ptr::null_mut(),
465            config,
466            cmd_fd: (-1).into(),
467            status: RwLock::new(Status::Initialized),
468            callbacks,
469            entrypoint: RwLock::new(None),
470            form_manager: RwLock::new(FormManager::default()),
471            peer_certs: PeerCerts::default(),
472        });
473
474        unsafe {
475            let weak_instance = Arc::downgrade(&instance);
476            let raw_instance = Weak::into_raw(weak_instance) as *mut VpnClient; // dangerous, leak for assign to vpninfo
477            let ret = openconnect_init_ssl();
478            if ret != 0 {
479                panic!("openconnect_init_ssl failed");
480            }
481
482            // format args on C side
483            helper_set_global_progress_vfn(Some(Logger::raw_handle_process_log));
484
485            let vpninfo = openconnect_vpninfo_new(
486                useragent.as_ptr(),
487                Some(PeerCerts::validate_peer_cert),
488                None,
489                Some(FormManager::process_auth_form_cb),
490                Some(helper_format_vargs), // format args on C side
491                raw_instance as *mut ::std::os::raw::c_void,
492            );
493
494            if vpninfo.is_null() {
495                panic!("openconnect_vpninfo_new failed");
496            }
497
498            (*raw_instance).vpninfo = vpninfo;
499        };
500
501        SIGNAL_HANDLE.update_client_singleton(Arc::downgrade(&instance));
502        instance.set_loglevel(instance.config.loglevel);
503        instance.set_setup_tun_handler();
504
505        if let Some(proxy) = &instance.config.http_proxy {
506            instance
507                .set_http_proxy(proxy.as_str())
508                .emit_error(&instance)?;
509        }
510
511        instance.emit_state_change(Status::Initialized);
512
513        Ok(instance)
514    }
515
516    /// Connect to the VPN server and obtain a cookie
517    ///
518    /// This function will not keep the connection, it will only connect and obtain a cookie. This function will not block the thread
519    ///
520    /// The cookie can be used to connect to the VPN server later by passing it to another [config::EntrypointBuilder]
521    ///
522    /// entrypoint can be created using [config::EntrypointBuilder]
523    fn connect_for_cookie(&self, entrypoint: Entrypoint) -> OpenconnectResult<Option<String>> {
524        self.emit_state_change(Status::Connecting("Initializing connection".to_string()));
525        {
526            if let Ok(mut form_context) = self.form_manager.try_write() {
527                form_context.reset();
528            }
529        }
530        self.set_protocol(&entrypoint.protocol.name)
531            .emit_error(self)?;
532        self.emit_state_change(Status::Connecting("Setting up system pipe".to_string()));
533        self.setup_cmd_pipe().emit_error(self)?;
534        self.set_stats_handler();
535
536        #[cfg(target_os = "windows")]
537        const OS_NAME: &str = "win";
538
539        #[cfg(target_os = "macos")]
540        const OS_NAME: &str = "mac-intel";
541
542        #[cfg(target_os = "linux")]
543        const OS_NAME: &str = "linux-64";
544
545        self.set_report_os(OS_NAME).emit_error(self)?;
546
547        {
548            let mut entrypoint_write_guard = self
549                .entrypoint
550                .write()
551                .map_err(|_| {
552                    OpenconnectError::EntrypointConfigError(
553                        "write entrypoint lock failed".to_string(),
554                    )
555                })
556                .emit_error(self)?;
557
558            *entrypoint_write_guard = Some(entrypoint.clone());
559            // drop entrypoint_write_guard
560        }
561
562        if !entrypoint.enable_udp {
563            self.disable_dtls().emit_error(self)?;
564        }
565
566        self.emit_state_change(Status::Connecting("Parsing URL".to_string()));
567        self.parse_url(&entrypoint.server).emit_error(self)?;
568        let hostname = self.get_hostname();
569
570        self.emit_state_change(Status::Connecting(format!(
571            "Obtaining cookie from: {}",
572            hostname.unwrap_or("".to_string())
573        )));
574        if let Some(cookie) = entrypoint.cookie.clone() {
575            self.set_cookie(&cookie).emit_error(self)?;
576        } else {
577            self.obtain_cookie().emit_error(self)?;
578        }
579
580        Ok(self.get_cookie())
581    }
582
583    /// Initialize the connection to the VPN server, this function will not block the thread and only make a CSTP connection
584    ///
585    /// entrypoint can be created using [config::EntrypointBuilder]
586    fn init_connection(&self, entrypoint: Entrypoint) -> OpenconnectResult<()> {
587        self.emit_state_change(Status::Connecting("Make CSTP connection".to_string()));
588        self.connect_for_cookie(entrypoint)?;
589        self.make_cstp_connection().emit_error(self)?;
590        self.emit_state_change(Status::Connected);
591
592        Ok(())
593    }
594
595    /// Run main loop and block until the connection is closed
596    fn run_loop(&self) -> OpenconnectResult<()> {
597        loop {
598            if self.main_loop(300, RECONNECT_INTERVAL_MIN).is_err() {
599                break;
600            }
601        }
602
603        // TODO: check if the following should be invoke?
604        // self.reset_ssl();
605        // self.clear_cookie();
606        self.emit_state_change(Status::Disconnected);
607
608        Ok(())
609    }
610
611    /// Gracefully stop the main loop
612    ///
613    /// This function will send a cancel command to the main loop and wait for the main loop to stop
614    fn disconnect(&self) {
615        if self.get_status() != Status::Connected {
616            return;
617        }
618
619        self.emit_state_change(Status::Disconnecting);
620        self.send_command(command::Command::Cancel);
621        self.cmd_fd.store(-1, Ordering::SeqCst);
622
623        std::thread::sleep(std::time::Duration::from_millis(200));
624    }
625
626    fn get_server_name(&self) -> Option<String> {
627        self.entrypoint
628            .read()
629            .ok()
630            .and_then(|r| r.as_ref().and_then(|e| e.name.clone()))
631    }
632
633    fn get_status(&self) -> Status {
634        self.status
635            .read()
636            .ok()
637            .map_or(Status::Initialized, |r| r.clone())
638    }
639}
640
641impl Events for VpnClient {
642    fn emit_state_change(&self, status: Status) {
643        if let Some(ref handler) = self.callbacks.handle_connection_state_change {
644            handler(status.clone());
645        }
646
647        {
648            let status_write_guard = self.status.write();
649            if let Ok(mut write) = status_write_guard {
650                *write = status;
651            } else {
652                // FIXME: handle error?
653            }
654        }
655    }
656
657    /// Change state and emit error to state change handler
658    fn emit_error(&self, error: &OpenconnectError) {
659        self.emit_state_change(Status::Error(error.clone()));
660    }
661}