-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #35 from jamesmcm/portforwarding
Add port forwarding and TCP proxying support for daemons/servers
- Loading branch information
Showing
13 changed files
with
426 additions
and
208 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
|
@@ -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" | ||
|
@@ -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" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.