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 split tunneling for macOS #5844

Merged
merged 14 commits into from
Apr 30, 2024
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ Line wrap the file at 100 chars. Th
### Added
- Add custom bridge settings in GUI.

#### macOS
- Add support for split tunneling (beta).

### Fixed
#### Linux
- Fix GUI not working on Ubuntu 24.04 by adding an AppArmor profile.
Expand Down
104 changes: 100 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 9 additions & 9 deletions docs/split-tunneling.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,17 @@ Some definitions of terms used later to describe behavior:
*: On platforms where we have custom firewall integration. This is currently on desktop operating
systems, and not mobile.

### Windows and Linux
### Desktop platforms (Windows, Linux, and macOS)

| In-app DNS setting | Normal & Excluded app |
|-|-|
| **Default DNS** | In tunnel (to relay) |
| **Private custom DNS** (e.g. 10.0.1.1) | LAN (to 10.0.1.1) |
| **Public custom DNS** (e.g. 8.8.8.8) | In tunnel (to 8.8.8.8) |
| In-app DNS setting | Normal & Excluded app |
|-|------------------------------------------------|
| **Default DNS** | In tunnel (to relay) |
| **Private custom DNS** (e.g. 10.0.1.1) | LAN (to 10.0.1.1)<br/>**macOS**: Not supported |
| **Public custom DNS** (e.g. 8.8.8.8) | In tunnel (to 8.8.8.8) |

In other words: Normal and excluded processes always behave the same. This is because DNS is
typically handled by a service, e.g. DNS cache on Windows or systemd-resolved's resolver on Linux,
which is not an excluded process.
In other words: Normal and excluded processes behave the same. This is because DNS is typically
handled by a service, e.g. DNS cache on Windows or systemd-resolved's resolver on Linux, which is
not an excluded process.

For the sake of simplicity and consistency, requests to public custom DNS resolvers are also sent
inside the tunnel when using a plain old static `resolv.conf`, even though it is technically
Expand Down
86 changes: 86 additions & 0 deletions mullvad-cli/src/cmds/split_tunnel/macos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use anyhow::Result;
use std::path::PathBuf;

use clap::Subcommand;
use mullvad_management_interface::MullvadProxyClient;

use super::super::BooleanOption;

/// Set options for applications to exclude from the tunnel.
#[derive(Subcommand, Debug)]
pub enum SplitTunnel {
/// Display the split tunnel status and apps
Get,

/// Enable or disable split tunnel
Set { policy: BooleanOption },

/// Manage applications to exclude from the tunnel
#[clap(subcommand)]
App(App),
}

#[derive(Subcommand, Debug)]
pub enum App {
Add { path: PathBuf },
Remove { path: PathBuf },
Clear,
}

impl SplitTunnel {
pub async fn handle(self) -> Result<()> {
match self {
SplitTunnel::Get => {
let mut rpc = MullvadProxyClient::new().await?;
let settings = rpc.get_settings().await?.split_tunnel;

let enable_exclusions = BooleanOption::from(settings.enable_exclusions);

println!("Split tunneling state: {enable_exclusions}");

println!("Excluded applications:");
for path in &settings.apps {
println!("{}", path.display());
}

Ok(())
}
SplitTunnel::Set { policy } => {
let mut rpc = MullvadProxyClient::new().await?;
rpc.set_split_tunnel_state(*policy).await?;
println!("Split tunnel policy: {policy}");
Ok(())
}
SplitTunnel::App(subcmd) => Self::app(subcmd).await,
}
}

async fn app(subcmd: App) -> Result<()> {
match subcmd {
App::Add { path } => {
MullvadProxyClient::new()
.await?
.add_split_tunnel_app(path)
.await?;
println!("Added path to excluded apps list");
Ok(())
}
App::Remove { path } => {
MullvadProxyClient::new()
.await?
.remove_split_tunnel_app(path)
.await?;
println!("Stopped excluding app from tunnel");
Ok(())
}
App::Clear => {
MullvadProxyClient::new()
.await?
.clear_split_tunnel_apps()
.await?;
println!("Stopped excluding all apps");
Ok(())
}
}
}
}
5 changes: 4 additions & 1 deletion mullvad-cli/src/cmds/split_tunnel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ mod imp;
#[path = "windows.rs"]
mod imp;

#[cfg(any(target_os = "linux", windows))]
#[cfg(target_os = "macos")]
#[path = "macos.rs"]
mod imp;

pub use imp::*;
2 changes: 0 additions & 2 deletions mullvad-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ enum Cli {
#[clap(subcommand)]
Obfuscation(obfuscation::Obfuscation),

#[cfg(any(target_os = "windows", target_os = "linux"))]
#[clap(subcommand)]
SplitTunnel(split_tunnel::SplitTunnel),

Expand Down Expand Up @@ -171,7 +170,6 @@ async fn main() -> Result<()> {
Cli::FactoryReset => reset::handle().await,
Cli::Relay(cmd) => cmd.handle().await,
Cli::Tunnel(cmd) => cmd.handle().await,
#[cfg(any(target_os = "windows", target_os = "linux"))]
Cli::SplitTunnel(cmd) => cmd.handle().await,
Cli::Status { cmd, args } => status::handle(cmd, args).await,
Cli::CustomList(cmd) => cmd.handle().await,
Expand Down
Loading
Loading