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#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum Status {
38 Initialized,
40
41 Disconnecting,
43
44 Disconnected,
46
47 Connecting(String),
49
50 Connected,
52
53 Error(OpenconnectError),
55}
56
57#[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 let ifname = client
85 .get_hostname()
86 .map(|hostname| format!("tun_{}", hostname));
87
88 println!("ifname: {:?}", ifname);
89
90 let _result = client.setup_tun_device(None, ifname);
92 }
93
94 #[cfg(not(target_os = "windows"))]
95 {
96 let _result = client.setup_tun_device(None, None);
98 }
99 }
100
101 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; 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 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
440pub 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 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; let ret = openconnect_init_ssl();
478 if ret != 0 {
479 panic!("openconnect_init_ssl failed");
480 }
481
482 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), 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 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 }
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 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 fn run_loop(&self) -> OpenconnectResult<()> {
597 loop {
598 if self.main_loop(300, RECONNECT_INTERVAL_MIN).is_err() {
599 break;
600 }
601 }
602
603 self.emit_state_change(Status::Disconnected);
607
608 Ok(())
609 }
610
611 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 }
654 }
655 }
656
657 fn emit_error(&self, error: &OpenconnectError) {
659 self.emit_state_change(Status::Error(error.clone()));
660 }
661}