Skip to content

Commit

Permalink
Merge pull request #86 from jamesmcm/newissues
Browse files Browse the repository at this point in the history
Add OpenFortiVPN support and better logging
  • Loading branch information
jamesmcm authored May 23, 2021
2 parents 76c7334 + f6f1159 commit 515e6ca
Show file tree
Hide file tree
Showing 19 changed files with 415 additions and 53 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "vopono"
description = "Launch applications via VPN tunnels using temporary network namespaces"
version = "0.7.1"
version = "0.8.0"
authors = ["James McMurray <[email protected]>"]
edition = "2018"
license = "GPL-3.0-or-later"
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Currently Mullvad, AzireVPN, MozillaVPN, TigerVPN, ProtonVPN, iVPN,
NordVPN, and PrivateInternetAccess are supported directly, with custom
configuration files also supported with the `--custom` argument.

For custom connections the OpenConnect and OpenFortiVPN protocols are
also supported (e.g. for enterprise VPNs). See the [vopono User Guide](USERGUIDE.md) for more details.

## Screenshot

Screenshot showing an example with firefox, google-chrome-stable and
Expand Down Expand Up @@ -39,13 +42,20 @@ Set up VPN provider configuration files:
$ vopono sync
```

Note when creating and uploading new Wireguard keypairs there may be a slight delay
until they are usable (about 30-60 seconds on Mullvad for example).

Run Firefox through an AzireVPN Wireguard connection to a server in
Norway:

```bash
$ vopono exec --provider azirevpn --server norway firefox
```

You should run vopono as your own user (not using sudo) as it will
handle privilege escalation where necessary. For more details around
running as a systemd service, etc. see the [User Guide](USERGUIDE.md).

vopono can handle up to 255 separate network namespaces (i.e. different VPN server
connections - if your VPN provider allows it). Commands launched with
the same server prefix and VPN provider will share the same network
Expand Down Expand Up @@ -157,6 +167,10 @@ $ rustc --version
keypairs) - unlike Mullvad this _cannot_ be done on the webpage. I recommend using [MozWire](https://github.com/NilsIrl/MozWire) to manage this.
- `gnome-terminal` will not run in the network namespace due to the
client-server model - see issue [#48](https://github.com/jamesmcm/vopono/issues/48)
- Port forwarding from inside the network namespace to the host (e.g.
for running `transmission-daemon`) does not work correctly when vopono
is run as root - see issue [#84](https://github.com/jamesmcm/vopono/issues/84)


## License

Expand Down
55 changes: 55 additions & 0 deletions USERGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,40 @@ directory as the config file itself. So any accompanying files (CA certificates,
files, etc.) must be in the same directory with the file if using
relative paths in the config file.

### OpenFortiVPN

OpenFortiVPN is supported as a custom provider, allowing you to connect
to Fortinet VPN servers.

To use it, first create an [OpenFortiVPN](https://github.com/adrienverge/openfortivpn) config file for your
connection, such as:

`myvpn.conf`:
```
host = vpn.company.net
port = 443
username = myuser
password = mypassword
set-dns = 0
pppd-use-peerdns = 0
pppd-log = /tmp/pppd.log
```

You must set `set-dns` and `pppd-use-peerdns` to `0` so that
OpenFortiVPN does not try to change the global DNS settings (vopono will
set them within the network namespace). You __must__ include the line:
`pppd-log = /tmp/pppd.log` as vopono uses this to read the pppd output
directly.

Then run vopono using this as the custom config file and specifying
`OpenFortiVPN` as the protocol. Note that if you do not specify your
password in the OpenFortiVPN config file then you must enter it when it
is waiting to connect (you will not be prompted).

```bash
vopono -v exec --protocol OpenFortiVPN --custom /home/user/myvpn.conf firefox
```

### Firefox

Note if running multiple Firefox sessions, they need to run separate
Expand Down Expand Up @@ -224,6 +258,27 @@ By default, vopono runs a small TCP proxy to proxy the ports on your
host machine to the ports on the network namespace - if you do not want
this to run use the `--no-proxy` flag.

#### systemd service

For the above you may want to run vopono as a systemd service. If your
user has passwordless sudo access you can use a user service, such as:

`/etc/systemd/user/vopono.service`:
```
[Service]
ExecStart=/usr/bin/vopono -v exec -k -f 9091 --protocol wireguard --provider mullvad --server romania "transmission-daemon -a *.*.*.*"
```

And then start it with (no sudo):
```
systemctl start --user vopono
```

If you do not have passwordless sudo access (i.e. privilege escalation
requires entering the password) then you could use a root service and
set up vopono on the root account. But note [this issue](https://github.com/jamesmcm/vopono/issues/84) currently
makes this problematic for forwarding ports.

#### Privoxy

A popular use case is to run a proxy server like Privoxy inside the
Expand Down
2 changes: 1 addition & 1 deletion src/application_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ impl ApplicationWrapper {
}

// TODO: Could allow user to set custom working directory here
let handle = netns.exec_no_block(app_vec.as_slice(), user, false, None)?;
let handle = netns.exec_no_block(app_vec.as_slice(), user, false, false, None)?;
Ok(Self { handle })
}

Expand Down
5 changes: 5 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ pub struct ExecCommand {
/// before shutting down the namespace
#[structopt(long = "predown")]
pub predown: Option<String>,

/// Path to vopono config TOML file (will be created if it does not exist)
/// Default: ~/.config/vopono/config.toml
#[structopt(long = "vopono-config")]
pub vopono_config: Option<PathBuf>,
}

#[derive(StructOpt)]
Expand Down
12 changes: 11 additions & 1 deletion src/dns_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub struct DnsConfig {
}

impl DnsConfig {
pub fn new(ns_name: String, servers: &[IpAddr]) -> anyhow::Result<Self> {
pub fn new(ns_name: String, servers: &[IpAddr], suffixes: &[&str]) -> anyhow::Result<Self> {
std::fs::create_dir_all(format!("/etc/netns/{}", ns_name))
.with_context(|| format!("Failed to create directory: /etc/netns/{}", ns_name))?;

Expand All @@ -32,6 +32,16 @@ impl DnsConfig {
.join(", ")
);

let suffix = suffixes.join(" ");
if !suffix.is_empty() {
writeln!(f, "search {}", suffix).with_context(|| {
format!(
"Failed to overwrite resolv.conf: /etc/netns/{}/resolv.conf",
ns_name
)
})?;
}

for dns in servers {
writeln!(f, "nameserver {}", dns).with_context(|| {
format!(
Expand Down
33 changes: 28 additions & 5 deletions src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ use super::vpn::{verify_auth, Protocol};
use anyhow::{anyhow, bail};
use log::{debug, error, info, warn};
use signal_hook::{consts::SIGINT, iterator::Signals};
use std::io::{self, Write};
use std::net::{IpAddr, Ipv4Addr};
use std::{
fs::create_dir_all,
io::{self, Write},
};

pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
// this captures all sigint signals
Expand All @@ -27,8 +30,13 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
let protocol: Protocol;

// TODO: Refactor this part - DRY
// Check if we have config file path passed on command line
// Create empty config file if does not exist
let config_path = vopono_dir()?.join("config.toml");
create_dir_all(vopono_dir()?)?;
let config_path = command
.vopono_config
.ok_or_else(|| anyhow!("No config file passed"))
.or_else::<anyhow::Error, _>(|_| Ok(vopono_dir()?.join("config.toml")))?;
{
std::fs::OpenOptions::new()
.write(true)
Expand Down Expand Up @@ -182,6 +190,7 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
Protocol::OpenVpn => provider.get_dyn_openvpn_provider()?.openvpn_dir(),
Protocol::Wireguard => provider.get_dyn_wireguard_provider()?.wireguard_dir(),
Protocol::OpenConnect => bail!("OpenConnect must use Custom provider"),
Protocol::OpenFortiVpn => bail!("OpenFortiVpn must use Custom provider"),
}?;
if !cdir.exists() || cdir.read_dir()?.next().is_none() {
info!(
Expand Down Expand Up @@ -217,6 +226,7 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
Protocol::OpenVpn => provider.get_dyn_openvpn_provider()?.openvpn_dir(),
Protocol::Wireguard => provider.get_dyn_wireguard_provider()?.wireguard_dir(),
Protocol::OpenConnect => bail!("OpenConnect must use Custom provider"),
Protocol::OpenFortiVpn => bail!("OpenFortiVpn must use Custom provider"),
}?;
Some(get_config_from_alias(&cdir, &server_name)?)
} else {
Expand Down Expand Up @@ -276,7 +286,8 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
})
.unwrap_or_else(|| vec![IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))]);

ns.dns_config(&dns)?;
// TODO: DNS suffixes?
ns.dns_config(&dns, &[])?;
// Check if using Shadowsocks
if let Some((ss_host, ss_lport)) =
uses_shadowsocks(config_file.as_ref().expect("No config file provided"))?
Expand Down Expand Up @@ -326,7 +337,8 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
if let Some(newdns) = ns.openvpn.as_ref().unwrap().openvpn_dns {
let old_dns = ns.dns_config.take();
std::mem::forget(old_dns);
ns.dns_config(&[newdns])?;
// TODO: DNS suffixes?
ns.dns_config(&[newdns], &[])?;
}
}
}
Expand All @@ -343,7 +355,8 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
}
Protocol::OpenConnect => {
let dns = base_dns.unwrap_or_else(|| vec![IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))]);
ns.dns_config(&dns)?;
// TODO: DNS suffixes?
ns.dns_config(&dns, &[])?;
ns.run_openconnect(
config_file,
command.open_ports.as_ref(),
Expand All @@ -352,6 +365,15 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
&server_name,
)?;
}
Protocol::OpenFortiVpn => {
// TODO: DNS handled by OpenFortiVpn directly?
ns.run_openfortivpn(
config_file.expect("No OpenFortiVPN config file provided"),
command.open_ports.as_ref(),
command.forward_ports.as_ref(),
firewall,
)?;
}
}

// Run PostUp script (if any)
Expand All @@ -374,6 +396,7 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
let application = ApplicationWrapper::new(&ns, &command.application, user)?;

// Launch TCP proxy server on other threads if forwarding ports
// TODO: Fix when running as root
let mut proxy = Vec::new();
if let Some(f) = command.forward_ports {
if !(command.no_proxy || f.is_empty()) {
Expand Down
1 change: 1 addition & 0 deletions src/list_configs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub fn print_configs(cmd: ServersCommand) -> anyhow::Result<()> {
Protocol::OpenVpn => provider.get_dyn_openvpn_provider()?.openvpn_dir(),
Protocol::Wireguard => provider.get_dyn_wireguard_provider()?.wireguard_dir(),
Protocol::OpenConnect => bail!("Config listing not implemented for OpenConnect"),
Protocol::OpenFortiVpn => bail!("Config listing not implemented for OpenFortiVPN"),
}?;
if !cdir.exists() || cdir.read_dir()?.next().is_none() {
bail!(
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod list_configs;
mod netns;
mod network_interface;
mod openconnect;
mod openfortivpn;
mod openvpn;
mod providers;
mod pulseaudio;
Expand Down Expand Up @@ -75,7 +76,6 @@ fn main() -> anyhow::Result<()> {
output_list(listcmd)?;
}
args::Command::Synch(synchcmd) => {
elevate_privileges()?;
// If provider given then sync that, else prompt with menu
if synchcmd.vpn_provider.is_none() {
sync_menu()?;
Expand Down
32 changes: 29 additions & 3 deletions src/netns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use super::firewall::Firewall;
use super::host_masquerade::HostMasquerade;
use super::network_interface::NetworkInterface;
use super::openconnect::OpenConnect;
use super::openfortivpn::OpenFortiVpn;
use super::openvpn::OpenVpn;
use super::shadowsocks::Shadowsocks;
use super::util::{config_dir, set_config_permissions, sudo_command};
Expand Down Expand Up @@ -33,6 +34,7 @@ pub struct NetworkNamespace {
pub shadowsocks: Option<Shadowsocks>,
pub veth_pair_ips: Option<VethPairIPs>,
pub openconnect: Option<OpenConnect>,
pub openfortivpn: Option<OpenFortiVpn>,
pub provider: VpnProvider,
pub protocol: Protocol,
pub firewall: Firewall,
Expand Down Expand Up @@ -87,6 +89,7 @@ impl NetworkNamespace {
shadowsocks: None,
veth_pair_ips: None,
openconnect: None,
openfortivpn: None,
provider,
protocol,
firewall,
Expand All @@ -100,6 +103,7 @@ impl NetworkNamespace {
command: &[&str],
user: Option<String>,
silent: bool,
capture_output: bool,
set_dir: Option<PathBuf>,
) -> anyhow::Result<std::process::Child> {
let mut handle = Command::new("ip");
Expand All @@ -117,6 +121,10 @@ impl NetworkNamespace {
handle.stdout(Stdio::null());
handle.stderr(Stdio::null());
}
if capture_output {
handle.stdout(Stdio::piped());
handle.stderr(Stdio::piped());
}

debug!(
"ip netns exec {}{} {}",
Expand All @@ -129,7 +137,8 @@ impl NetworkNamespace {
}

pub fn exec(&self, command: &[&str]) -> anyhow::Result<()> {
self.exec_no_block(command, None, false, None)?.wait()?;
self.exec_no_block(command, None, false, false, None)?
.wait()?;
Ok(())
}

Expand Down Expand Up @@ -203,8 +212,8 @@ impl NetworkNamespace {
Ok(())
}

pub fn dns_config(&mut self, server: &[IpAddr]) -> anyhow::Result<()> {
self.dns_config = Some(DnsConfig::new(self.name.clone(), &server)?);
pub fn dns_config(&mut self, server: &[IpAddr], suffixes: &[&str]) -> anyhow::Result<()> {
self.dns_config = Some(DnsConfig::new(self.name.clone(), server, suffixes)?);
Ok(())
}

Expand Down Expand Up @@ -253,6 +262,23 @@ impl NetworkNamespace {
Ok(())
}

pub fn run_openfortivpn(
&mut self,
config_file: PathBuf,
open_ports: Option<&Vec<u16>>,
forward_ports: Option<&Vec<u16>>,
firewall: Firewall,
) -> anyhow::Result<()> {
self.openfortivpn = Some(OpenFortiVpn::run(
self,
config_file,
open_ports,
forward_ports,
firewall,
)?);
Ok(())
}

pub fn run_shadowsocks(
&mut self,
config_file: &Path,
Expand Down
2 changes: 1 addition & 1 deletion src/network_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub fn get_active_interfaces() -> anyhow::Result<Vec<String>> {
.filter(|x| x.contains("state UP"))
.map(|x| x.split_whitespace().nth(1))
.filter(|x| x.is_some())
.map(|x| x.unwrap())
.flatten()
.map(|x| String::from(&x[..x.len() - 1]))
.collect::<Vec<String>>();

Expand Down
Loading

0 comments on commit 515e6ca

Please sign in to comment.