Skip to content

Commit

Permalink
Added option for last-chance split tunneling.
Browse files Browse the repository at this point in the history
If the client doesn't support PAC files, the SOCKS/HTTP request could be
used to check the domain and perform a direct connection, bypassing the
VPN.

Added an option to specify which PAC file to load.
  • Loading branch information
zlogic committed Aug 12, 2024
1 parent 44c14c2 commit 154b1d2
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 9 deletions.
14 changes: 13 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Options:\
\n --log-level=<LOG_LEVEL> Log level [default: info]\
\n --listen-address=<IP> Listen IP address [default: :::5328]\
\n --destination=<HOSTPORT> Destination FortiVPN address, e.g. sslvpn.example.com:443\
\n --pac-file=<PATH> (Optional) Path to pac file (available at /proxy.pac)\
\n --tunnel-domain=<PREFIX> (Optional) Forward only subdomains to VPN, other domains will use direct connection; can be specified multiple times\
\n --help Print help";

struct Config {
Expand All @@ -53,6 +55,8 @@ impl Args {
let mut listen_addr = SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 5328);
let mut destination_addr = None;
let mut destination_hostport = None;
let mut pac_path = None;
let mut tunnel_domains = vec![];

for arg in env::args()
.take(env::args().len().saturating_sub(1))
Expand Down Expand Up @@ -104,6 +108,10 @@ impl Args {
format_args!("Failed to parse destination address: {}", err),
),
};
} else if name == "--pac-file" {
pac_path = Some(value.into());
} else if name == "--tunnel-domain" {
tunnel_domains.push(value.into());
} else {
eprintln!("Unsupported argument {}", arg);
}
Expand Down Expand Up @@ -131,7 +139,11 @@ impl Args {
}
};

let proxy_config = proxy::Config { listen_addr };
let proxy_config = proxy::Config {
listen_addr,
pac_path,
tunnel_domains,
};
let fortivpn_config = fortivpn::Config {
destination_addr,
destination_hostport,
Expand Down
47 changes: 39 additions & 8 deletions src/proxy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{
error, fmt, io,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
sync::Arc,
};

use log::{debug, info, warn};
Expand All @@ -16,10 +17,14 @@ use crate::{http, network};

pub struct Config {
pub listen_addr: SocketAddr,
pub pac_path: Option<String>,
pub tunnel_domains: Vec<String>,
}

pub struct Server {
listen_addr: SocketAddr,
pac_path: Option<String>,
tunnel_domains: Vec<String>,
command_bridge: mpsc::Sender<network::Command>,
}

Expand All @@ -30,6 +35,8 @@ impl Server {
) -> Result<Server, ProxyError> {
Ok(Server {
listen_addr: config.listen_addr,
pac_path: config.pac_path,
tunnel_domains: config.tunnel_domains,
command_bridge,
})
}
Expand All @@ -43,11 +50,16 @@ impl Server {
}
};
info!("Started server on {}", self.listen_addr);
let options = Arc::new(ProxyOptions {
pac_path: self.pac_path.clone(),
tunnel_domains: self.tunnel_domains.clone(),
});
loop {
match listener.accept().await {
Ok((socket, addr)) => {
debug!("Received connection from {}", addr);
let handler = ProxyConnection::new(socket, self.command_bridge.clone());
let handler =
ProxyConnection::new(socket, options.clone(), self.command_bridge.clone());
let rt = runtime::Handle::current();
rt.spawn(async move {
if let Err(err) = handler.handle_connection().await {
Expand All @@ -61,15 +73,26 @@ impl Server {
}
}

struct ProxyOptions {
pac_path: Option<String>,
tunnel_domains: Vec<String>,
}

struct ProxyConnection {
socket: Option<TcpStream>,
options: Arc<ProxyOptions>,
command_bridge: mpsc::Sender<network::Command>,
}

impl ProxyConnection {
fn new(socket: TcpStream, command_bridge: mpsc::Sender<network::Command>) -> ProxyConnection {
fn new(
socket: TcpStream,
options: Arc<ProxyOptions>,
command_bridge: mpsc::Sender<network::Command>,
) -> ProxyConnection {
ProxyConnection {
socket: Some(socket),
options,
command_bridge,
}
}
Expand Down Expand Up @@ -133,7 +156,7 @@ impl ProxyConnection {
let request =
String::from_utf8(request).map_err(|_| "First handshake byte is not utf-8")?;
if request.starts_with("GET /proxy.pac HTTP/1.1\r\n") {
Self::send_pac_file(socket).await?;
self.send_pac_file(socket).await?;
Ok(DestinationConnection::None)
} else if request.starts_with("CONNECT ") {
let host = if let Some(host) = http::extract_connect_host(&request) {
Expand Down Expand Up @@ -170,9 +193,13 @@ impl ProxyConnection {
}
}

async fn send_pac_file(socket: &mut TcpStream) -> Result<(), ProxyError> {
// TODO: allow configuring this
let mut file = File::open("proxy.pac").await?;
async fn send_pac_file(&self, socket: &mut TcpStream) -> Result<(), ProxyError> {
let pac_path = if let Some(pac_path) = self.options.pac_path.as_ref() {
pac_path
} else {
return Err("Path to pac file is not defined".into());
};
let mut file = File::open(pac_path).await?;
let mut contents = vec![];
file.read_to_end(&mut contents).await?;
http::write_pac_response(socket, &contents).await?;
Expand All @@ -191,8 +218,12 @@ impl ProxyConnection {
// Fallback to port 80 (the default).
(host.to_owned() + ":80", host)
};
// TODO: allow configuring this
let direct_connection = domain.ends_with(".home");
let direct_connection = !(self.options.tunnel_domains.is_empty()
|| self
.options
.tunnel_domains
.iter()
.any(|tunnel_domain| domain.ends_with(tunnel_domain)));
let ip = tokio::net::lookup_host(host).await?.next();
match ip {
Some(addr) => {
Expand Down

0 comments on commit 154b1d2

Please sign in to comment.