use super::firewall::Firewall;
use super::netns::NetworkNamespace;
use anyhow::{Context, anyhow};
use log::{debug, error, info};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::io::{BufRead, BufReader, Write};
use std::net::{IpAddr, Ipv4Addr};
use std::path::{Path, PathBuf};
use std::str::FromStr;
#[derive(Serialize, Deserialize, Debug)]
pub struct OpenFortiVpn {
pid: u32,
}
impl OpenFortiVpn {
#[allow(clippy::too_many_arguments)]
pub fn run(
netns: &mut NetworkNamespace,
config_file: PathBuf,
open_ports: Option<&Vec<u16>>,
forward_ports: Option<&Vec<u16>>,
hosts_entries: Option<&Vec<String>>,
firewall: Firewall,
allow_host_access: bool,
) -> anyhow::Result<Self> {
if let Err(x) = which::which("openfortivpn") {
error!("OpenFortiVPN not found. Is OpenFortiVPN installed and on PATH?");
return Err(anyhow!(
"OpenFortiVPN not found. Is OpenFortiVPN installed and on PATH?: {:?}",
x
));
}
info!("Launching OpenFortiVPN...");
let command_vec = ([
"openfortivpn",
"-c",
config_file.to_str().expect("Invalid config path"),
])
.to_vec();
let pppd_log = std::path::PathBuf::from_str("/tmp/pppd.log")?;
std::fs::remove_file(&pppd_log).ok();
let mut handle = NetworkNamespace::exec_no_block(
&netns.name,
&command_vec,
None,
None,
false,
true,
false,
None,
)
.context("Failed to launch OpenFortiVPN - is openfortivpn installed?")?;
let stdout = handle.stdout.take().unwrap();
let id = handle.id();
info!(
"Waiting for OpenFortiVPN to establish connection - you may be prompted on your 2FA device"
);
info!("If your VPN password is not in the OpenFortiVPN config file then enter it here now");
let mut bufreader = BufReader::with_capacity(16000, stdout);
let mut buffer = String::with_capacity(16000);
let mut bufcount: usize = 0;
let newbytes = bufreader.read_line(&mut buffer)?;
if newbytes > 0 {
print!("{}", &buffer[bufcount..(bufcount + newbytes)]);
std::io::stdout().flush()?;
bufcount += newbytes;
}
while !buffer.contains("Tunnel is up and running") {
let newbytes = bufreader.read_line(&mut buffer)?;
if newbytes > 0 {
print!("{}", &buffer[bufcount..(bufcount + newbytes)]);
std::io::stdout().flush()?;
bufcount += newbytes;
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
debug!("Full OpenFortiVPN stdout: {:?}", &buffer);
let remote_peer = get_remote_peer(&pppd_log)?;
debug!("Found OpenFortiVPN route: {remote_peer:?}");
NetworkNamespace::exec(&netns.name, &["ip", "route", "del", "default"])?;
NetworkNamespace::exec(
&netns.name,
&[
"ip",
"route",
"add",
"default",
"via",
&remote_peer.to_string(),
],
)?;
let dns = get_dns(&buffer)?;
let dns_ip: Vec<IpAddr> = (dns.0).into_iter().collect();
let suffixes: Vec<&str> = (dns.1).iter().map(|x| x.as_str()).collect();
netns.dns_config(
dns_ip.as_slice(),
suffixes.as_slice(),
hosts_entries,
allow_host_access,
)?;
if let Some(opens) = open_ports {
crate::util::open_ports(netns, opens.as_slice(), firewall)?;
}
if let Some(forwards) = forward_ports {
crate::util::open_ports(netns, forwards.as_slice(), firewall)?;
}
Ok(Self { pid: id })
}
}
impl Drop for OpenFortiVpn {
fn drop(&mut self) {
match nix::sys::signal::kill(
nix::unistd::Pid::from_raw(self.pid as i32),
nix::sys::signal::Signal::SIGKILL,
) {
Ok(_) => debug!("Killed OpenFortiVPN (pid: {})", self.pid),
Err(e) => error!("Failed to kill OpenFortiVPN (pid: {}): {:?}", self.pid, e),
}
}
}
pub fn get_remote_peer(pppd_log: &Path) -> anyhow::Result<Ipv4Addr> {
let stdout = std::fs::read_to_string(pppd_log)
.context(format!("Opening pppd log file: {pppd_log:?}"))?;
let re = Regex::new(r"remote IP address (?P<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})").unwrap();
let mut ips = Vec::new();
for caps in re.captures_iter(&stdout) {
ips.push(Ipv4Addr::from_str(&caps["ip"]).expect("Failed to parse IP address in stdout"));
}
ips.pop()
.ok_or_else(|| anyhow!("Could not find remote IP address in pppd log"))
}
pub fn get_dns(stdout: &str) -> anyhow::Result<(Vec<IpAddr>, Vec<String>)> {
let re = Regex::new(r"ns \[(?P<ip>[^\]]+)\]").unwrap();
let mut ips = Vec::new();
for caps in re.captures_iter(stdout) {
for ip_raw in caps["ip"].split(", ") {
let ip = IpAddr::from_str(ip_raw)?;
if !ips.contains(&ip) && ip != IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) {
ips.push(ip);
}
}
}
let re = Regex::new(r"ns_suffix \[(?P<suffix>[^\]]+)\]").unwrap();
let mut suffixes = Vec::new();
for caps in re.captures_iter(stdout) {
for suffix_raw in caps["suffix"].split(';') {
let suffix = suffix_raw.to_string();
if !suffixes.contains(&suffix) {
suffixes.push(suffix);
}
}
}
debug!(
"Found OpenFortiVPN DNS ips: {:?}, ns suffixes: {:?}",
&ips, &suffixes
);
Ok((ips, suffixes))
}