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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ directories-next = "2"
log = "0.4"
pretty_env_logger = "0.5"
clap = { version = "4", features = ["derive"] }
which = "5"
which = "6"
dialoguer = "0.11"
compound_duration = "1"
signal-hook = "0.3"
Expand Down
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>,
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
78 changes: 58 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 All @@ -169,6 +171,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()>
.ok()
});

// TODO: Modify this to allow creating base netns only
// Assign protocol and server from args or vopono config file or custom config if used
if let Some(path) = &custom_config {
protocol = command
Expand Down Expand Up @@ -382,6 +385,8 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()>
firewall,
)?;
_sysctl = SysCtl::enable_ipv4_forwarding();

// TODO: Skip this if netns config only
match protocol {
Protocol::Warp => ns.run_warp(
command.open_ports.as_ref(),
Expand Down Expand Up @@ -429,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 @@ -464,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 @@ -479,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 @@ -490,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 @@ -547,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 @@ -589,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 @@ -598,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
8 changes: 5 additions & 3 deletions vopono_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ keywords = ["vopono", "vpn", "wireguard", "openvpn", "netns"]
anyhow = "1"
directories-next = "2"
log = "0.4"
which = "5"
which = "6"
users = "0.11"
nix = { version = "0.27", features = ["user", "signal", "fs", "process"] }
serde = { version = "1", features = ["derive", "std"] }
Expand All @@ -30,7 +30,7 @@ reqwest = { default-features = false, version = "0.11", features = [
"json",
"rustls-tls",
] } # TODO: Can we remove Tokio dependency?
sysinfo = "0.29"
sysinfo = "0.30"
base64 = "0.21"
x25519-dalek = { version = "2", features = ["static_secrets"] }
strum = "0.25"
Expand All @@ -40,5 +40,7 @@ maplit = "1"
webbrowser = "0.8"
serde_json = "1"
signal-hook = "0.3"
sha2 = "0.10.6"
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
1 change: 1 addition & 0 deletions vopono_core/src/config/providers/mozilla/wireguard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ impl ConfigurationChoice for Devices {
}
}

// TODO: Update API calls for new API
impl MozillaVPN {
fn upload_new_device(
&self,
Expand Down
Loading
Loading