From 9ab8af6b81fa432a58d83cb25e6cef9580111fef Mon Sep 17 00:00:00 2001 From: James McMurray Date: Sun, 24 Jul 2022 11:04:14 +0200 Subject: [PATCH 1/2] vopono 0.10.1 --- .github/workflows/rust.yml | 56 +++++++++++++++++- src/args.rs | 9 +++ src/exec.rs | 57 +++++++++++++++++-- .../src/network/application_wrapper.rs | 2 +- vopono_core/src/network/netns.rs | 34 +++++++++-- vopono_core/src/network/openconnect.rs | 2 +- vopono_core/src/network/openfortivpn.rs | 2 +- vopono_core/src/network/openvpn.rs | 22 ++++++- vopono_core/src/network/shadowsocks.rs | 2 +- vopono_core/src/util/mod.rs | 28 ++++++--- 10 files changed, 190 insertions(+), 24 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b37e25c..a57e8d4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -112,6 +112,20 @@ jobs: with: name: armv5-deb path: ./target/armv5te-unknown-linux-musleabi/debian/* + aarch64deb: + needs: [build] + runs-on: ubuntu-latest + name: Aarch64Deb + steps: + - uses: actions/checkout@v2 + - name: BuildDeb + id: debbuild + uses: jamesmcm/cargo-deb-aarch64-debian@master + - name: Upload Deb Artifact + uses: actions/upload-artifact@v2 + with: + name: aarch64-deb + path: ./target/aarch64-unknown-linux-musl/debian/* amd64binaries: needs: [build, quickcheck] runs-on: ubuntu-latest @@ -168,8 +182,28 @@ jobs: with: name: armv5 path: ./target/armv5te-unknown-linux-musleabi/release/vopono + aarch64binaries: + needs: [build, quickcheck] + runs-on: ubuntu-latest + name: Aarch64StaticBinaries + steps: + - uses: actions/checkout@v2 + - id: cargoversion + run: cargo --version + - id: rustcversion + run: rustc --version + - name: StaticBinaryBuild + id: aarch64staticbuild + uses: jamesmcm/cargo-deb-aarch64-debian@master + with: + cmd: cargo build --release --target=aarch64-unknown-linux-musl + - name: Upload Vopono Artifact + uses: actions/upload-artifact@v2 + with: + name: aarch64 + path: ./target/aarch64-unknown-linux-musl/release/vopono update_release_draft: - needs: [quickcheck, build, arm7binaries, arm5binaries, amd64binaries, raspbianbuild, armv5deb, debbuild, opensuseleaprpmbuild, fedorarpmbuild] + needs: [quickcheck, build, arm7binaries, arm5binaries, aarch64binaries, amd64binaries, raspbianbuild, armv5deb, aarch64deb, debbuild, opensuseleaprpmbuild, fedorarpmbuild] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -193,6 +227,8 @@ jobs: - run: ls -lha armv7 - run: ls -lha armv5-deb - run: ls -lha armv5 + - run: ls -lha aarch64-deb + - run: ls -lha aarch64 - run: ls -lha fedorarpm - run: ls -lha opensuserpm - name: Upload amd64 deb Release Asset @@ -222,6 +258,15 @@ jobs: asset_path: ./armv5-deb/vopono_${{needs.quickcheck.outputs.version}}_armel.deb asset_name: 'vopono_${{needs.quickcheck.outputs.version}}_armel.deb' asset_content_type: application/vnd.debian.binary-package + - name: Upload aarch64 deb Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./aarch64-deb/vopono_${{needs.quickcheck.outputs.version}}_aarch64.deb + asset_name: 'vopono_${{needs.quickcheck.outputs.version}}_aarch64.deb' + asset_content_type: application/vnd.debian.binary-package - name: Upload amd64 rpm fedora Release Asset uses: actions/upload-release-asset@v1 env: @@ -258,6 +303,15 @@ jobs: asset_path: ./armv5/vopono asset_name: 'vopono_${{needs.quickcheck.outputs.version}}_linux_armv5' asset_content_type: application/octet-stream + - name: Upload Aarch64 Static Binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./aarch64/vopono + asset_name: 'vopono_${{needs.quickcheck.outputs.version}}_linux_aarch64' + asset_content_type: application/octet-stream - name: Upload Amd64 Static Binary uses: actions/upload-release-asset@v1 env: diff --git a/src/args.rs b/src/args.rs index 2853e46..525c5b8 100644 --- a/src/args.rs +++ b/src/args.rs @@ -194,6 +194,15 @@ pub struct ExecCommand { /// Default: ~/.config/vopono/config.toml #[clap(long = "vopono-config")] pub vopono_config: Option, + + /// Custom name for the generated network namespace + /// Will use this network namespace directly if it exists + #[clap(long = "custom-netns-name")] + pub custom_netns_name: Option, + /// Allow access to host from network namespace + /// Useful for accessing services on the host locally + #[clap(long = "allow-host-access")] + pub allow_host_access: bool, } #[derive(Parser)] diff --git a/src/exec.rs b/src/exec.rs index 966cd6a..c0a157d 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -30,7 +30,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> let server_name: String; let protocol: Protocol; - // TODO: Refactor this part - DRY + // TODO: Refactor this part - DRY - macro_rules ? // Check if we have config file path passed on command line // Create empty config file if does not exist create_dir_all(vopono_dir()?)?; @@ -73,6 +73,36 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> .ok() }); + // Assign custom_config from args or vopono config file + let custom_netns_name = command.custom_netns_name.clone().or_else(|| { + vopono_config_settings + .get("custom_netns_name") + .map_err(|e| { + debug!("vopono config.toml: {:?}", e); + anyhow!("Failed to read config file") + }) + .ok() + }); + + // Assign open_hosts from args or vopono config file + let mut open_hosts = command.open_hosts.clone().or_else(|| { + vopono_config_settings + .get("open_hosts") + .map_err(|e| { + debug!("vopono config.toml: {:?}", e); + anyhow!("Failed to read config file") + }) + .ok() + }); + let allow_host_access = command.allow_host_access + || vopono_config_settings + .get("allow_host_access") + .map_err(|e| { + debug!("vopono config.toml: {:?}", e); + anyhow!("Failed to read config file") + }) + .unwrap_or(false); + // Assign postup script from args or vopono config file let postup = command.postup.clone().or_else(|| { vopono_config_settings @@ -224,7 +254,11 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> _ => provider.get_dyn_provider().alias(), }; - let ns_name = format!("vopono_{}_{}", alias, server_name); + let ns_name = if let Some(c_ns_name) = custom_netns_name { + c_ns_name + } else { + format!("vopono_{}_{}", alias, server_name) + }; let mut ns; let _sysctl; @@ -298,7 +332,22 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> let target_subnet = get_target_subnet()?; ns.add_loopback()?; ns.add_veth_pair()?; - ns.add_routing(target_subnet, command.open_hosts.as_ref())?; + ns.add_routing(target_subnet, open_hosts.as_ref(), allow_host_access)?; + + // Add local host to open hosts if allow_host_access enabled + if allow_host_access { + let host_ip = ns.veth_pair_ips.as_ref().unwrap().host_ip; + warn!( + "Allowing host access from network namespace, host IP address is: {}", + host_ip + ); + if let Some(oh) = open_hosts.iter_mut().next() { + oh.push(host_ip); + } else { + open_hosts = Some(vec![host_ip]); + } + } + ns.add_host_masquerade(target_subnet, interface.clone(), firewall)?; ns.add_firewall_exception( interface, @@ -418,7 +467,7 @@ pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> } } - if let Some(ref hosts) = command.open_hosts { + if let Some(ref hosts) = open_hosts { vopono_core::util::open_hosts(&ns, hosts.to_vec(), firewall)?; } diff --git a/vopono_core/src/network/application_wrapper.rs b/vopono_core/src/network/application_wrapper.rs index 0640c80..8398dba 100644 --- a/vopono_core/src/network/application_wrapper.rs +++ b/vopono_core/src/network/application_wrapper.rs @@ -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, false, None)?; + let handle = netns.exec_no_block(app_vec.as_slice(), user, false, false, false, None)?; Ok(Self { handle }) } diff --git a/vopono_core/src/network/netns.rs b/vopono_core/src/network/netns.rs index 3fc23d4..5e4e221 100644 --- a/vopono_core/src/network/netns.rs +++ b/vopono_core/src/network/netns.rs @@ -105,6 +105,7 @@ impl NetworkNamespace { user: Option, silent: bool, capture_output: bool, + capture_input: bool, set_dir: Option, ) -> anyhow::Result { let mut handle = Command::new("ip"); @@ -126,7 +127,9 @@ impl NetworkNamespace { handle.stdout(Stdio::piped()); handle.stderr(Stdio::piped()); } - handle.stdin(Stdio::piped()); + if capture_input { + handle.stdin(Stdio::piped()); + } debug!( "ip netns exec {}{} {}", @@ -139,7 +142,7 @@ impl NetworkNamespace { } pub fn exec(&self, command: &[&str]) -> anyhow::Result<()> { - self.exec_no_block(command, None, false, false, None)? + self.exec_no_block(command, None, false, false, false, None)? .wait()?; Ok(()) } @@ -165,6 +168,7 @@ impl NetworkNamespace { &mut self, target_subnet: u8, hosts: Option<&Vec>, + allow_host_access: bool, ) -> anyhow::Result<()> { // TODO: Handle case where IP address taken in better way i.e. don't just change subnet let veth_dest = &self @@ -213,7 +217,7 @@ impl NetworkNamespace { "ip", "route", "add", - &format!("{}", host), + &host.to_string(), "via", &ip_nosub, "dev", @@ -221,13 +225,32 @@ impl NetworkNamespace { ]) .with_context(|| { format!( - "Failed to assign hosts route to veth source: {}", - veth_source + "Failed to assign hosts route {} to veth source: {}", + host, veth_source ) })?; } } + if allow_host_access { + self.exec(&[ + "ip", + "route", + "add", + &ip_nosub, + "via", + &ip_nosub, + "dev", + veth_source, + ]) + .with_context(|| { + format!( + "Failed to assign hosts route for local host {} to veth source: {}", + ip_nosub, veth_source + ) + })?; + } + info!( "IP address of namespace as seen from host: {}", veth_source_ip_nosub @@ -446,6 +469,7 @@ impl Drop for NetworkNamespace { lockfile_path.push(format!("vopono/locks/{}", self.name)); // Drop if lock directory doesn't exist, or it exists but is empty + // TODO: How can we make this check that no _other_ PIDs exist (aside from ones we have spawned) if !lockfile_path.exists() || (lockfile_path.read_dir().is_ok() && lockfile_path.read_dir().unwrap().next().is_none()) diff --git a/vopono_core/src/network/openconnect.rs b/vopono_core/src/network/openconnect.rs index a51dc9e..f7c9572 100644 --- a/vopono_core/src/network/openconnect.rs +++ b/vopono_core/src/network/openconnect.rs @@ -50,7 +50,7 @@ impl OpenConnect { } let handle = netns - .exec_no_block(&command_vec, None, false, false, None) + .exec_no_block(&command_vec, None, false, false, true, None) .context("Failed to launch OpenConnect - is openconnect installed?")?; handle diff --git a/vopono_core/src/network/openfortivpn.rs b/vopono_core/src/network/openfortivpn.rs index e7840c5..1458716 100644 --- a/vopono_core/src/network/openfortivpn.rs +++ b/vopono_core/src/network/openfortivpn.rs @@ -48,7 +48,7 @@ impl OpenFortiVpn { // TODO - better handle forwarding output when blocking on password entry (no newline!) let mut handle = netns - .exec_no_block(&command_vec, None, false, true, None) + .exec_no_block(&command_vec, None, false, true, false, None) .context("Failed to launch OpenFortiVPN - is openfortivpn installed?")?; let stdout = handle.stdout.take().unwrap(); let id = handle.id(); diff --git a/vopono_core/src/network/openvpn.rs b/vopono_core/src/network/openvpn.rs index 67c0f2e..604070a 100644 --- a/vopono_core/src/network/openvpn.rs +++ b/vopono_core/src/network/openvpn.rs @@ -1,7 +1,7 @@ use super::firewall::Firewall; use super::netns::NetworkNamespace; use crate::config::vpn::OpenVpnProtocol; -use crate::util::check_process_running; +use crate::util::{check_process_running, vopono_dir}; use anyhow::{anyhow, Context}; use log::{debug, error, info}; use regex::Regex; @@ -16,6 +16,7 @@ use std::str::FromStr; pub struct OpenVpn { pid: u32, pub openvpn_dns: Option, + pub logfile: PathBuf, } impl OpenVpn { @@ -42,7 +43,9 @@ impl OpenVpn { )); } - let log_file_str = format!("/etc/netns/{}/openvpn.log", &netns.name); + std::fs::create_dir_all(vopono_dir()?.join("logs"))?; + let log_file_path = vopono_dir()?.join(format!("logs/{}_openvpn.log", &netns.name)); + let log_file_str: String = log_file_path.as_os_str().to_string_lossy().to_string(); { File::create(&log_file_str)?; } @@ -95,7 +98,7 @@ impl OpenVpn { let working_dir = PathBuf::from(config_file_path.parent().unwrap()); let handle = netns - .exec_no_block(&command_vec, None, true, false, Some(working_dir)) + .exec_no_block(&command_vec, None, true, false, false, Some(working_dir)) .context("Failed to launch OpenVPN - is openvpn installed?")?; let id = handle.id(); let mut buffer = String::with_capacity(16384); @@ -173,6 +176,7 @@ impl OpenVpn { Ok(Self { pid: id, openvpn_dns, + logfile: log_file_path, }) } @@ -190,6 +194,18 @@ impl Drop for OpenVpn { Ok(_) => debug!("Killed OpenVPN (pid: {})", self.pid), Err(e) => error!("Failed to kill OpenVPN (pid: {}): {:?}", self.pid, e), } + + match std::fs::remove_file(&self.logfile) { + Ok(_) => debug!( + "Deleted OpenVPN logfile: {}", + self.logfile.as_os_str().to_string_lossy() + ), + Err(e) => error!( + "Failed to delete OpenVPN logfile: {}: {:?}", + self.logfile.as_os_str().to_string_lossy(), + e + ), + } } } diff --git a/vopono_core/src/network/shadowsocks.rs b/vopono_core/src/network/shadowsocks.rs index 85ad646..420e49e 100644 --- a/vopono_core/src/network/shadowsocks.rs +++ b/vopono_core/src/network/shadowsocks.rs @@ -67,7 +67,7 @@ impl Shadowsocks { ]; let handle = netns - .exec_no_block(&command_vec, None, true, false, None) + .exec_no_block(&command_vec, None, true, false, false, None) .context("Failed to launch Shadowsocks - is shadowsocks-libev installed?")?; Ok(Self { pid: handle.id() }) diff --git a/vopono_core/src/util/mod.rs b/vopono_core/src/util/mod.rs index 76dcf3e..d7c6353 100644 --- a/vopono_core/src/util/mod.rs +++ b/vopono_core/src/util/mod.rs @@ -191,6 +191,21 @@ pub fn get_existing_namespaces() -> anyhow::Result> { Ok(output) } +pub fn get_pids_in_namespace(ns_name: &str) -> anyhow::Result> { + let output = Command::new("ip") + .args(&["netns", "pids", ns_name]) + .output()? + .stdout; + let output = std::str::from_utf8(&output)? + .split('\n') + .filter_map(|x| x.split_whitespace().next()) + .filter_map(|x| x.parse::().ok()) + .collect(); + debug!("PIDs active in {}: {:?}", &ns_name, output); + + Ok(output) +} + pub fn check_process_running(pid: u32) -> bool { let s = System::new_with_specifics(RefreshKind::new().with_processes(ProcessRefreshKind::new())); @@ -261,8 +276,7 @@ pub fn clean_dead_locks() -> anyhow::Result<()> { std::fs::create_dir_all(&lockfile_path)?; WalkDir::new(&lockfile_path) .into_iter() - .filter(|x| x.is_ok()) - .map(|x| x.unwrap()) + .filter_map(|x| x.ok()) .filter(|x| x.path().is_file()) .map(|x| { ( @@ -285,8 +299,7 @@ pub fn clean_dead_locks() -> anyhow::Result<()> { // Delete subdirectories if they contain no locks (ignore errors) WalkDir::new(&lockfile_path) .into_iter() - .filter(|x| x.is_ok()) - .map(|x| x.unwrap()) + .filter_map(|x| x.ok()) .filter(|x| x.path().is_dir()) .try_for_each(|x| std::fs::remove_dir(x.path())) .ok(); @@ -301,7 +314,9 @@ pub fn clean_dead_namespaces() -> anyhow::Result<()> { existing_namespaces .into_iter() - .filter(|x| !lock_namespaces.contains_key(x)) + .filter(|x| { + !lock_namespaces.contains_key(x) && get_pids_in_namespace(x).unwrap().is_empty() + }) .try_for_each(|x| { debug!("Removing dead namespace: {}", x); let path = format!("/etc/netns/{}", x); @@ -366,8 +381,7 @@ pub fn delete_all_files_in_dir(dir: &Path) -> anyhow::Result<()> { pub fn get_configs_from_alias(list_path: &Path, alias: &str) -> Vec { WalkDir::new(&list_path) .into_iter() - .filter(|x| x.is_ok()) - .map(|x| x.unwrap()) + .filter_map(|x| x.ok()) .filter(|x| { x.path().is_file() && x.path().extension().is_some() From 36e6b6d87ea3fe5f9c3a2c0fc17a2cb04a32dcbc Mon Sep 17 00:00:00 2001 From: James McMurray Date: Sun, 24 Jul 2022 11:31:05 +0200 Subject: [PATCH 2/2] Fix handling of short namespace names --- vopono_core/src/network/netns.rs | 3 ++- vopono_core/src/network/wireguard.rs | 15 +++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/vopono_core/src/network/netns.rs b/vopono_core/src/network/netns.rs index 5e4e221..8d3d24f 100644 --- a/vopono_core/src/network/netns.rs +++ b/vopono_core/src/network/netns.rs @@ -157,7 +157,8 @@ impl NetworkNamespace { pub fn add_veth_pair(&mut self) -> anyhow::Result<()> { // TODO: Handle if name taken? - let basename = &self.name[(self.name.len() - 13).max(0)..self.name.len()]; + // Use bs58 here? + let basename = &self.name[((self.name.len() as i32) - 13).max(0) as usize..self.name.len()]; let source = format!("{}_s", basename); let dest = format!("{}_d", basename); self.veth_pair = Some(VethPair::new(source, dest, self)?); diff --git a/vopono_core/src/network/wireguard.rs b/vopono_core/src/network/wireguard.rs index ef7ff61..1018df9 100644 --- a/vopono_core/src/network/wireguard.rs +++ b/vopono_core/src/network/wireguard.rs @@ -17,6 +17,7 @@ pub struct Wireguard { ns_name: String, config_file: PathBuf, firewall: Firewall, + if_name: String, } impl Wireguard { @@ -85,7 +86,10 @@ impl Wireguard { ) })?; debug!("TOML config: {:?}", config); - let if_name = namespace.name[7..namespace.name.len().min(20)].to_string(); + // TODO: Use bs58 here? + let if_name = namespace.name + [((namespace.name.len() as i32) - 13).max(0) as usize..namespace.name.len()] + .to_string(); assert!( if_name.len() <= 15, "ifname must be <= 15 chars: {}", @@ -352,6 +356,7 @@ impl Wireguard { config_file, ns_name: namespace.name.clone(), firewall, + if_name, }) } } @@ -450,7 +455,6 @@ pub fn killswitch( impl Drop for Wireguard { fn drop(&mut self) { - let if_name = &self.ns_name[7..self.ns_name.len().min(20)]; match sudo_command(&[ "ip", "netns", @@ -459,10 +463,13 @@ impl Drop for Wireguard { "ip", "link", "del", - if_name, + &self.if_name, ]) { Ok(_) => {} - Err(e) => warn!("Failed to delete ip link {}: {:?}", &self.ns_name, e), + Err(e) => warn!( + "Failed to delete ip link {}, {}: {:?}", + &self.ns_name, &self.if_name, e + ), }; if let Firewall::NfTables = self.firewall {