Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add port forwarding with PIA using --port-forwarding #245

Merged
merged 11 commits into from
Jan 20, 2024
11 changes: 8 additions & 3 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,14 @@ pub struct ExecCommand {
#[clap(long = "allow-host-access")]
pub allow_host_access: bool,

/// Enable port forwarding for ProtonVPN connections
#[clap(long = "protonvpn-port-forwarding")]
pub protonvpn_port_forwarding: bool,
/// Enable port forwarding for if supported
#[clap(long = "port-forwarding")]
pub port_forwarding: bool,

/// Path or alias to executable script or binary to be called with the port as an argumnet
/// when the port forwarding is refreshed (PIA only)
#[clap(long = "port-forwarding-callback")]
pub port_forwarding_callback: Option<String>,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have an example use of this? It'd be nice to add to the docs 🙂


/// Only create network namespace (does not run application)
#[clap(long = "create-netns-only")]
Expand Down
68 changes: 48 additions & 20 deletions src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ use vopono_core::config::vpn::{verify_auth, Protocol};
use vopono_core::network::application_wrapper::ApplicationWrapper;
use vopono_core::network::firewall::Firewall;
use vopono_core::network::natpmpc::Natpmpc;
use vopono_core::network::piapf::Piapf;
use vopono_core::network::Forwarder;
use vopono_core::network::netns::NetworkNamespace;
use vopono_core::network::network_interface::{get_active_interfaces, NetworkInterface};
use vopono_core::network::shadowsocks::uses_shadowsocks;
Expand Down Expand Up @@ -139,15 +141,15 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()>
command.working_directory
};

// Port forwarding for ProtonVPN
let protonvpn_port_forwarding = if !command.protonvpn_port_forwarding {
// Port forwarding
let port_forwarding = if !command.port_forwarding {
vopono_config_settings
.get("protonvpn-port-forwarding")
.get("port-forwarding")
.map_err(|_e| anyhow!("Failed to read config file"))
.ok()
.unwrap_or(false)
} else {
command.protonvpn_port_forwarding
command.port_forwarding
};

// Create netns only
Expand Down Expand Up @@ -429,7 +431,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()>
}

ns.run_openvpn(
config_file.expect("No config file provided"),
config_file.clone().expect("No config file provided"),
auth_file,
&dns,
!command.no_killswitch,
Expand Down Expand Up @@ -464,7 +466,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()>
}
Protocol::Wireguard => {
ns.run_wireguard(
config_file.expect("No config file provided"),
config_file.clone().expect("No config file provided"),
!command.no_killswitch,
command.open_ports.as_ref(),
command.forward_ports.as_ref(),
Expand All @@ -479,7 +481,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()>
// TODO: DNS suffixes?
ns.dns_config(&dns, &[], command.hosts_entries.as_ref())?;
ns.run_openconnect(
config_file.expect("No OpenConnect config file provided"),
config_file.clone().expect("No OpenConnect config file provided"),
command.open_ports.as_ref(),
command.forward_ports.as_ref(),
firewall,
Expand All @@ -490,7 +492,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()>
Protocol::OpenFortiVpn => {
// TODO: DNS handled by OpenFortiVpn directly?
ns.run_openfortivpn(
config_file.expect("No OpenFortiVPN config file provided"),
config_file.clone().expect("No OpenFortiVPN config file provided"),
command.open_ports.as_ref(),
command.forward_ports.as_ref(),
command.hosts_entries.as_ref(),
Expand Down Expand Up @@ -547,19 +549,45 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()>

let ns = ns.write_lockfile(&command.application)?;

let natpmpc = if protonvpn_port_forwarding {
vopono_core::util::open_hosts(
&ns,
vec![vopono_core::network::natpmpc::PROTONVPN_GATEWAY],
firewall,
)?;
Some(Natpmpc::new(&ns)?)
let forwarder: Option<Box<dyn Forwarder>> = if port_forwarding {

let callback = command
.port_forwarding_callback
.or_else(|| {
vopono_config_settings
.get("port_forwarding_callback")
.map_err(|_e| anyhow!("Failed to read config file"))
.ok()
});
match provider {
VpnProvider::PrivateInternetAccess => {
let conf_path = config_file
.expect("No PIA config file provided");
let conf_name = conf_path
.file_name().unwrap()
.to_str().expect("No filename for PIA config file")
.to_string();
Some(Box::new(Piapf::new(&ns, &conf_name, &protocol, callback.as_ref())?))
},
VpnProvider::ProtonVPN => {
vopono_core::util::open_hosts(
&ns,
vec![vopono_core::network::natpmpc::PROTONVPN_GATEWAY],
firewall,
)?;
Some(Box::new(Natpmpc::new(&ns)?))
},
_ => {
anyhow::bail!("Port forwarding not supported for the selected provider");
}
}
} else {
None
};

if let Some(pmpc) = natpmpc.as_ref() {
vopono_core::util::open_ports(&ns, &[pmpc.local_port], firewall)?;
// TODO: The forwarder should probably be able to do this (pass firewall?)
if let Some(fwd) = forwarder.as_ref() {
vopono_core::util::open_ports(&ns, &[fwd.forwarded_port()], firewall)?;
}

// Launch TCP proxy server on other threads if forwarding ports
Expand Down Expand Up @@ -589,7 +617,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()>
user,
group,
working_directory.map(PathBuf::from),
natpmpc,
forwarder,
)?;

let pid = application.handle.id();
Expand All @@ -598,8 +626,8 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()>
&command.application, &ns.name, pid
);

if let Some(pmpc) = application.protonvpn_port_forwarding.as_ref() {
info!("ProtonVPN Port Forwarding on port {}", pmpc.local_port)
if let Some(fwd) = application.port_forwarding.as_ref() {
info!("Port Forwarding on port {}", fwd.forwarded_port())
}
let output = application.wait_with_output()?;
io::stdout().write_all(output.stdout.as_slice())?;
Expand Down
1 change: 1 addition & 0 deletions vopono_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ serde_json = "1"
signal-hook = "0.3"
sha2 = "0.10.6"
tiny_http = "0.12"
json = "0.12.4"
25 changes: 22 additions & 3 deletions vopono_core/src/config/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod ivpn;
mod mozilla;
mod mullvad;
mod nordvpn;
mod pia;
pub mod pia;
mod protonvpn;
mod ui;
mod warp;
Expand All @@ -14,8 +14,12 @@ use crate::config::vpn::Protocol;
use crate::util::vopono_dir;
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::{net::IpAddr, path::Path};
use std::{
net::IpAddr,
path::{Path, PathBuf},
fs::File,
io::{BufReader, BufRead},
};
use strum_macros::{Display, EnumIter};
// TODO: Consider removing this re-export
pub use ui::*;
Expand Down Expand Up @@ -137,6 +141,21 @@ pub trait OpenVpnProvider: Provider {
fn prompt_for_auth(&self, uiclient: &dyn UiClient) -> anyhow::Result<(String, String)>;
fn auth_file_path(&self) -> anyhow::Result<Option<PathBuf>>;


fn load_openvpn_auth(&self) -> anyhow::Result<(String, String)> {
let auth_file = self.auth_file_path()?;
if let Some(auth_file) = auth_file {
let mut reader = BufReader::new(File::open(auth_file)?);
let mut user = String::new();
reader.read_line(&mut user)?;
let mut pass = String::new();
reader.read_line(&mut pass)?;
Ok((user.trim().to_string(), pass.trim().to_string()))
} else {
Err(anyhow!("Auth file required to load credentials!"))
}
}

fn openvpn_dir(&self) -> anyhow::Result<PathBuf> {
Ok(self.provider_dir()?.join("openvpn"))
}
Expand Down
60 changes: 60 additions & 0 deletions vopono_core/src/config/providers/pia/openvpn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,38 @@ use std::path::PathBuf;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use zip::ZipArchive;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use log::info;
use regex::Regex;
use anyhow::Context;

#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub hostname_lookup: HashMap<String, String>,
}

impl PrivateInternetAccess {

fn openvpn_config_file_path(&self) -> anyhow::Result<PathBuf> {
Ok(self.openvpn_dir()?.join("config.txt"))
}

//This only works if openvpn was sync'd
pub fn hostname_for_openvpn_conf(&self, config_file: &String) -> anyhow::Result<String> {
let pia_config_file = File::open(self.openvpn_config_file_path()?)?;
let pia_config: Config = serde_json::from_reader(pia_config_file)?;

let hostname = pia_config
.hostname_lookup
.get(config_file)
.with_context(|| format!("Could not find matching hostname for openvpn conf {config_file}"))?;

Ok(hostname.to_string())
}

}

impl OpenVpnProvider for PrivateInternetAccess {
fn provider_dns(&self) -> Option<Vec<IpAddr>> {
Expand Down Expand Up @@ -40,6 +72,11 @@ impl OpenVpnProvider for PrivateInternetAccess {
let country_map = crate::util::country_map::country_to_code_map();
create_dir_all(&openvpn_dir)?;
delete_all_files_in_dir(&openvpn_dir)?;

let mut config = Config {
hostname_lookup: HashMap::new(),
};

for i in 0..zip.len() {
// For each file, detect if ovpn, crl or crt
// Modify auth line for config
Expand Down Expand Up @@ -74,6 +111,20 @@ impl OpenVpnProvider for PrivateInternetAccess {
} else {
file.name().to_string()
};

let re = Regex::new(r"\n *remote +([^ ]+) +\d+ *\n").expect("Failed to compile hostname regex");
if let Some(capture) = re.captures(&String::from_utf8_lossy(&file_contents)) {
let hostname = capture
.get(1)
.expect("No matching hostname group in openvpn config")
.as_str()
.to_string();

info!("Associating {filename} with hostname {hostname}");
config.hostname_lookup.insert(filename.clone(), hostname);
} else {
info!("Configuration {filename} did not have a parseable hostname - port forwarding will not work!");
}

debug!("Reading file: {}", file.name());
let mut outfile =
Expand All @@ -88,8 +139,17 @@ impl OpenVpnProvider for PrivateInternetAccess {
let mut outfile = File::create(auth_file)?;
write!(outfile, "{user}\n{pass}")?;
}

// Write PrivateInternetAccess openvpn config file
let pia_config_file = File::create(self.openvpn_config_file_path()?)?;
serde_json::to_writer(pia_config_file, &config)?;

// Write PIA certificate
self.write_pia_cert()?;

Ok(())
}

}

#[derive(EnumIter, PartialEq)]
Expand Down
Loading
Loading