Skip to content

Commit

Permalink
Merge pull request #35 from jamesmcm/portforwarding
Browse files Browse the repository at this point in the history
Add port forwarding and TCP proxying support for daemons/servers
  • Loading branch information
jamesmcm authored Oct 10, 2020
2 parents ceb2d06 + 0e5b3d4 commit cdcba23
Show file tree
Hide file tree
Showing 13 changed files with 426 additions and 208 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: PullRequest

on:
pull_request:
branches: [ master ]

env:
CARGO_TERM_COLOR: always

jobs:
quickcheck:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.rustversion.outputs.rustversion }}
steps:
- uses: actions/checkout@v2
- run: cargo check
- run: cargo pkgid
- run: 'echo "$(cargo pkgid | cut -d# -f2)"'
- id: rustversion
run: 'echo "::set-output name=rustversion::$(cargo pkgid | cut -d# -f2)"'
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Clippy
run: rustup component add clippy rustfmt
- name: Clippy
run: cargo clippy -- -D warnings
- name: Rustfmt
run: cargo fmt --all -- --check
- name: Run tests
run: cargo test
- name: Build
run: cargo build --verbose --release
2 changes: 0 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ name: Rust
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

env:
CARGO_TERM_COLOR: always
Expand Down
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
[package]
name = "vopono"
description = "Launch applications via VPN tunnels using temporary network namespaces"
version = "0.4.1"
version = "0.5.0"
authors = ["James McMurray <[email protected]>"]
edition = "2018"
license = "GPL-3.0-or-later"
repository = "https://github.com/jamesmcm/vopono"
homepage = "https://github.com/jamesmcm/vopono"
readme = "README.md"
keywords = ["vopono", "vpn", "wireguard", "openvpn", "namespace", "netns"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
Expand All @@ -18,7 +19,7 @@ log = "0.4"
pretty_env_logger = "0.4"
clap = "2"
which = "4"
users = "0.10"
users = "0.11"
nix = "0.18"
serde = {version = "1", features = ["derive", "std"]}
csv = "1"
Expand All @@ -40,6 +41,8 @@ strum_macros = "0.19"
zip = "0.5"
maplit = "1"
webbrowser = "0.5"
basic_tcp_proxy = "0.1"
ctrlc = "3"

[package.metadata.rpm]
package = "vopono"
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,30 @@ You may also wish to disable WebRTC - see
Similar issues apply to Chromium and Google Chrome.


### Daemons and servers

If running servers and daemons inside of vopono, you can you use the
`-f $PORT` argument to allow incoming connections to a TCP port inside the namespace, by default this
port will also be proxied to your host machine at the same port number.
Note for same daemons you may need to use the `-k` keep-alive option in
case the process ID changes.

For example, to launch `transmission-daemon` that is externally
accessible at `127.0.0.1:9091` (with outward connections via AzireVPN with Wireguard and a VPN server in Norway):

```bash
$ vopono -v exec -k -f 9091 --provider azirevpn --server norway "transmission-daemon -a *.*.*.*"
```

Note in the case of `transmission-daemon` the `-a *.*.*.*` argument is
required to allow external connections to the daemon's web portal (your
host machine will now count as external to the network namespace).

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.


## Installation

### AUR (Arch Linux)
Expand Down
2 changes: 1 addition & 1 deletion src/application_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::util::get_all_running_process_names;
use log::warn;

pub struct ApplicationWrapper {
handle: std::process::Child,
pub handle: std::process::Child,
}

impl ApplicationWrapper {
Expand Down
12 changes: 12 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ pub struct ExecCommand {
/// Disable killswitch
#[structopt(long = "no-killswitch")]
pub no_killswitch: bool,

/// Keep-alive - do not close network namespace when launched process terminates
#[structopt(long = "keep-alive", short = "k")]
pub keep_alive: bool,

/// List of ports to forward from network namespace - usefuel for running servers and daemons
#[structopt(long = "forward", short = "f")]
pub forward_ports: Option<Vec<u16>>,

/// Disable proxying to host machine when forwarding ports
#[structopt(long = "no-proxy")]
pub no_proxy: bool,
}

#[derive(StructOpt)]
Expand Down
253 changes: 253 additions & 0 deletions src/exec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
use super::application_wrapper::ApplicationWrapper;
use super::args::ExecCommand;
use super::netns::NetworkNamespace;
use super::network_interface::{get_active_interfaces, NetworkInterface};
use super::providers::VpnProvider;
use super::shadowsocks::uses_shadowsocks;
use super::sync::synch;
use super::sysctl::SysCtl;
use super::util::{get_config_file_protocol, get_config_from_alias};
use super::util::{get_existing_namespaces, get_target_subnet};
use super::vpn::{verify_auth, Protocol};
use anyhow::{anyhow, bail};
use log::{debug, error, info, warn};
use std::io::{self, Write};
use std::net::{IpAddr, Ipv4Addr};

pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
let provider: VpnProvider;
let server_name: String;
let protocol: Protocol;

if let Some(path) = &command.custom_config {
protocol = command
.protocol
.unwrap_or_else(|| get_config_file_protocol(path));
provider = VpnProvider::Custom;
// Could hash filename with CRC and use base64 but chars are limited
server_name = String::from(
&path
.as_path()
.file_name()
.unwrap()
.to_str()
.unwrap()
.chars()
.filter(|&x| x != ' ' && x != '-')
.collect::<String>()[0..4],
);
} else {
// Get server and provider
// TODO: Handle default case and remove expect()
provider = command.vpn_provider.expect("Enter a VPN provider");
if provider == VpnProvider::Custom {
bail!("Must provide config file if using custom VPN Provider");
}
server_name = command.server.expect("Enter a VPN server prefix");

// Check protocol is valid for provider
protocol = command
.protocol
.unwrap_or_else(|| provider.get_dyn_provider().default_protocol());
}

if provider != VpnProvider::Custom {
// Check config files exist for provider
let cdir = match protocol {
Protocol::OpenVpn => provider.get_dyn_openvpn_provider()?.openvpn_dir(),
Protocol::Wireguard => provider.get_dyn_wireguard_provider()?.wireguard_dir(),
}?;
if !cdir.exists() || cdir.read_dir()?.next().is_none() {
info!(
"Config files for {} {} do not exist, running vopono sync",
provider, protocol
);
synch(provider.clone(), Some(protocol.clone()))?;
}
}

let alias = match provider {
VpnProvider::Custom => "custom".to_string(),
_ => provider.get_dyn_provider().alias(),
};

let ns_name = format!("vopono_{}_{}", alias, server_name);

let mut ns;
let _sysctl;
let interface: NetworkInterface = match command.interface {
Some(x) => anyhow::Result::<NetworkInterface>::Ok(x),
None => Ok(NetworkInterface::new(
get_active_interfaces()?
.into_iter()
.next()
.ok_or_else(|| anyhow!("No active network interface"))?,
)?),
}?;
debug!("Interface: {}", &interface.name);

let config_file = if provider != VpnProvider::Custom {
let cdir = match protocol {
Protocol::OpenVpn => provider.get_dyn_openvpn_provider()?.openvpn_dir(),
Protocol::Wireguard => provider.get_dyn_wireguard_provider()?.wireguard_dir(),
}?;
get_config_from_alias(&cdir, &server_name)?
} else {
command.custom_config.expect("No custom config provided")
};

// Better to check for lockfile exists?
if get_existing_namespaces()?.contains(&ns_name) {
// If namespace exists, read its lock config
ns = NetworkNamespace::from_existing(ns_name)?;
} else {
ns = NetworkNamespace::new(ns_name.clone(), provider.clone(), protocol.clone())?;
let target_subnet = get_target_subnet()?;
ns.add_loopback()?;
ns.add_veth_pair()?;
ns.add_routing(target_subnet)?;
ns.add_iptables_rule(target_subnet, interface)?;
_sysctl = SysCtl::enable_ipv4_forwarding();
match protocol {
Protocol::OpenVpn => {
// Handle authentication check
let auth_file = if provider != VpnProvider::Custom {
Some(verify_auth(provider.get_dyn_openvpn_provider()?)?)
} else {
None
};

let dns = command
.dns
.or_else(|| {
provider
.get_dyn_openvpn_provider()
.ok()
.map(|x| x.provider_dns())
.flatten()
})
.unwrap_or_else(|| vec![IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))]);

ns.dns_config(&dns)?;

// Check if using Shadowsocks
if let Some((ss_host, ss_lport)) = uses_shadowsocks(&config_file)? {
if provider == VpnProvider::Custom {
warn!("Custom provider specifies socks-proxy, if this is local you must run it yourself (e.g. shadowsocks)");
} else {
let dyn_ss_provider = provider.get_dyn_shadowsocks_provider()?;
let password = dyn_ss_provider.password();
let encrypt_method = dyn_ss_provider.encrypt_method();
ns.run_shadowsocks(
&config_file,
ss_host,
ss_lport,
&password,
&encrypt_method,
)?;
}
}

ns.run_openvpn(
config_file,
auth_file,
&dns,
!command.no_killswitch,
command.forward_ports.as_ref(),
)?;
debug!(
"Checking that OpenVPN is running in namespace: {}",
&ns_name
);
if !ns.check_openvpn_running() {
error!(
"OpenVPN not running in network namespace {}, probable dead lock file or authentication error",
&ns_name
);
return Err(anyhow!(
"OpenVPN not running in network namespace, probable dead lock file authentication error"
));
}
}
Protocol::Wireguard => {
ns.run_wireguard(
config_file,
!command.no_killswitch,
command.forward_ports.as_ref(),
)?;
}
}
}

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

// User for application command, if None will use root
let user = if command.user.is_none() {
std::env::var("SUDO_USER").ok()
} else {
command.user
};

let application = ApplicationWrapper::new(&ns, &command.application, user)?;
let mut proxy = Vec::new();
if let Some(f) = command.forward_ports {
if !(command.no_proxy || f.is_empty()) {
for p in f {
debug!(
"Forwarding port: {}, {:?}",
p,
ns.veth_pair_ips.as_ref().unwrap().namespace_ip
);
proxy.push(basic_tcp_proxy::TcpProxy::new(
p,
std::net::SocketAddr::new(ns.veth_pair_ips.as_ref().unwrap().namespace_ip, p),
));
}
}
}
let pid = application.handle.id();
info!(
"Application {} launched in network namespace {} with pid {}",
&command.application, &ns.name, pid
);
let output = application.wait_with_output()?;
io::stdout().write_all(output.stdout.as_slice())?;

// Allow daemons to leave namespace open
if crate::util::check_process_running(pid) {
info!(
"Process {} still running, assumed to be daemon - will leave network namespace alive until ctrl+C received",
pid
);
stay_alive(pid)?;
} else if command.keep_alive {
info!("Keep-alive flag active - will leave network namespace alive until ctrl+C received");
stay_alive(pid)?;
}

Ok(())
}

fn stay_alive(pid: u32) -> anyhow::Result<()> {
let recv = ctrl_channel(pid);
recv?.recv().unwrap();
Ok(())
}

fn ctrl_channel(pid: u32) -> Result<std::sync::mpsc::Receiver<()>, ctrlc::Error> {
let (sender, receiver) = std::sync::mpsc::channel();
ctrlc::set_handler(move || {
let _ = sender.send(());
info!(
"SIGINT received, killing process {} and terminating...",
pid
);
nix::sys::signal::kill(
nix::unistd::Pid::from_raw(pid as i32),
nix::sys::signal::Signal::SIGKILL,
)
.ok();
})?;

Ok(receiver)
}
Loading

0 comments on commit cdcba23

Please sign in to comment.