Skip to content

Commit

Permalink
Merge pull request #245 from BenLand100/pia_forwarding
Browse files Browse the repository at this point in the history
Add port forwarding with PIA using --port-forwarding
  • Loading branch information
jamesmcm authored Jan 20, 2024
2 parents 6919ca8 + 8d103bc commit 5c7e591
Show file tree
Hide file tree
Showing 13 changed files with 479 additions and 49 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ lynx all running through different VPN connections:
| AirVPN |||
| Cloudflare Warp\*\*\*\* |||

\* Port forwarding is not currently supported for PrivateInternetAccess. PRs welcome.
\* Port forwarding supported with the `--port-forwarding` option and `--port-forwarding-callback` to run a command when the port is refreshed.

\*\* See the [User Guide](USERGUIDE.md) for authentication instructions for generating the OpenVPN config files via `vopono sync`. You must copy the authentication header of the form `AUTH-xxx=yyy` where `yyy` is the value of the `x-pm-uid` header in the same request when logged in, in your web browser.

\*\*\* For ProtonVPN you can generate and download specific Wireguard config
files, and use them as a custom provider config. See the [User Guide](USERGUIDE.md)
for details. [Port Forwarding](https://protonvpn.com/support/port-forwarding-manual-setup/) is supported with the `--protonvpn-port-forwarding` argument for both OpenVPN and Wireguard (with `--provider custom --custom xxx.conf --protocol wireguard` ). `natpmpc` must be installed. Note for OpenVPN you must generate the OpenVPN config files appending `+pmp` to your OpenVPN username, and you must choose servers which support this feature (e.g. at the time of writing, the Romania servers do). The assigned port is then printed to the terminal where vopono was launched - this should then be set in any applications that require it.
for details. [Port Forwarding](https://protonvpn.com/support/port-forwarding-manual-setup/) is supported with the `--port-forwarding` argument for both OpenVPN and Wireguard (with `--provider custom --custom xxx.conf --protocol wireguard` ). `natpmpc` must be installed. Note for OpenVPN you must generate the OpenVPN config files appending `+pmp` to your OpenVPN username, and you must choose servers which support this feature (e.g. at the time of writing, the Romania servers do). The assigned port is then printed to the terminal where vopono was launched - this should then be set in any applications that require it.


\*\*\*\* Cloudflare Warp uses its own protocol. Set both the provider and
Expand Down
20 changes: 12 additions & 8 deletions USERGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,12 +488,12 @@ Due to the way Wireguard configuration generation is handled, this should be
generated online and then used as a custom configuration, e.g.:
```bash
$ vopono -v exec --provider custom --custom testwg-UK-17.conf --protocol wireguard --protonvpn-port-forwarding firefox-developer-edition
$ vopono -v exec --provider custom --custom testwg-UK-17.conf --protocol wireguard --port-forwarding firefox-developer-edition
```
#### Port Forwarding
Port forwarding can be enabled with the `--protonvpn-port-forwarding` argument, but requires using a server that supports port forwarding.
Port forwarding can be enabled with the `--port-forwarding` argument, but requires using a server that supports port forwarding.
`natpmpc` must be installed e.g. via the `libnatpmp` package on Arch Linux.
Expand All @@ -508,6 +508,10 @@ The port you are allocated will then be printed to the console like:
And that is the port you would then set up in applications that require it.
### PrivateInternetAccess
Port forwaring supported with the `--port-forwarding` option, use the `--port-forwarding-callback` option to specify a command to run when the port is refreshed.
### Cloudflare Warp
Cloudflare Warp users must first register with Warp via the CLI client:
Expand All @@ -525,31 +529,31 @@ You can then kill `warp-svc` and run it via vopono:
$ vopono -v exec --no-killswitch --provider warp --protocol warp firefox-developer-edition
```
### VPN Provider limitations
## VPN Provider limitations
#### PrivateInternetAccess
### PrivateInternetAccess
Wireguard support for PrivateInternetAccess (PIA) requires the use of a
user token to get the latest servers at time of use. See [issue 9](https://github.com/jamesmcm/vopono/issues/9) for details,
and PIA's [official script for Wireguard access](https://github.com/pia-foss/manual-connections/blob/master/connect_to_wireguard_with_token.sh).
So if you encounter connection issues, first try re-running `vopono sync`.
#### MozillaVPN
### MozillaVPN
There is no easy way to delete MozillaVPN devices (Wireguard keypairs),
unlike Mullvad this _cannot_ be done on the webpage.
I recommend using [MozWire](https://github.com/NilsIrl/MozWire) to manage this.
#### iVPN
### iVPN
iVPN Wireguard keypairs must be uploaded manually, as the Client Area is
behind a captcha login.
#### NordVPN
### NordVPN
Starting 27 June 2023, the required user credentials are no longer your NordVPN login details but need to be generated in the user control panel, under Services → NordVPN. Scroll down and locate the Manual Setup tab, then click on Set up NordVPN manually and follow instructions. Copy your service credentials and re-sync NordVPN configuration inside Vopono.
### Tunnel Port Forwarding
## Tunnel Port Forwarding
Some providers allow port forwarding inside the tunnel, so you can open
some ports inside the network namespace which can be accessed via the
Expand Down
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>,

/// Only create network namespace (does not run application)
#[clap(long = "create-netns-only")]
Expand Down
75 changes: 55 additions & 20 deletions src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ use vopono_core::network::firewall::Firewall;
use vopono_core::network::natpmpc::Natpmpc;
use vopono_core::network::netns::NetworkNamespace;
use vopono_core::network::network_interface::{get_active_interfaces, NetworkInterface};
use vopono_core::network::piapf::Piapf;
use vopono_core::network::shadowsocks::uses_shadowsocks;
use vopono_core::network::sysctl::SysCtl;
use vopono_core::network::Forwarder;
use vopono_core::util::vopono_dir;
use vopono_core::util::{get_config_file_protocol, get_config_from_alias};
use vopono_core::util::{get_existing_namespaces, get_target_subnet};
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 @@ -432,7 +434,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 @@ -467,7 +469,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 @@ -482,7 +484,9 @@ 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 @@ -493,7 +497,9 @@ 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 @@ -550,19 +556,48 @@ 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 @@ -592,7 +627,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 @@ -601,8 +636,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 @@ -43,3 +43,4 @@ signal-hook = "0.3"
sha2 = "0.10"
tiny_http = "0.12"
chrono = "0.4"
json = "0.12"
24 changes: 21 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::{
fs::File,
io::{BufRead, BufReader},
net::IpAddr,
path::{Path, PathBuf},
};
use strum_macros::{Display, EnumIter};
// TODO: Consider removing this re-export
pub use ui::*;
Expand Down Expand Up @@ -137,6 +141,20 @@ 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
62 changes: 61 additions & 1 deletion vopono_core/src/config/providers/pia/openvpn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ use super::PrivateInternetAccess;
use super::{ConfigurationChoice, OpenVpnProvider};
use crate::config::providers::UiClient;
use crate::util::delete_all_files_in_dir;
use log::debug;
use anyhow::Context;
use log::info;
use log::{debug, warn};
use regex::Regex;
use reqwest::Url;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::fmt::Display;
use std::fs::create_dir_all;
use std::fs::File;
Expand All @@ -14,6 +20,32 @@ use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use zip::ZipArchive;

#[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>> {
Some(vec![
Expand All @@ -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 @@ -75,6 +112,21 @@ impl OpenVpnProvider for PrivateInternetAccess {
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 {
warn!("Configuration {filename} did not have a parseable hostname - port forwarding will not work!");
}

debug!("Reading file: {}", file.name());
let mut outfile =
File::create(openvpn_dir.join(filename.to_lowercase().replace(' ', "_")))?;
Expand All @@ -88,6 +140,14 @@ 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(())
}
}
Expand Down
Loading

0 comments on commit 5c7e591

Please sign in to comment.