From 154b1d2027736f8dc69f65bfe23a529127705484 Mon Sep 17 00:00:00 2001 From: Dmitry Zolotukhin Date: Sun, 28 Jul 2024 20:12:18 +0200 Subject: [PATCH] Added option for last-chance split tunneling. 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. --- src/main.rs | 14 +++++++++++++- src/proxy.rs | 47 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index c9e0857..2ecacbd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,8 @@ Options:\ \n --log-level= Log level [default: info]\ \n --listen-address= Listen IP address [default: :::5328]\ \n --destination= Destination FortiVPN address, e.g. sslvpn.example.com:443\ +\n --pac-file= (Optional) Path to pac file (available at /proxy.pac)\ +\n --tunnel-domain= (Optional) Forward only subdomains to VPN, other domains will use direct connection; can be specified multiple times\ \n --help Print help"; struct Config { @@ -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)) @@ -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); } @@ -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, diff --git a/src/proxy.rs b/src/proxy.rs index ee71995..85beebe 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -1,6 +1,7 @@ use std::{ error, fmt, io, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + sync::Arc, }; use log::{debug, info, warn}; @@ -16,10 +17,14 @@ use crate::{http, network}; pub struct Config { pub listen_addr: SocketAddr, + pub pac_path: Option, + pub tunnel_domains: Vec, } pub struct Server { listen_addr: SocketAddr, + pac_path: Option, + tunnel_domains: Vec, command_bridge: mpsc::Sender, } @@ -30,6 +35,8 @@ impl Server { ) -> Result { Ok(Server { listen_addr: config.listen_addr, + pac_path: config.pac_path, + tunnel_domains: config.tunnel_domains, command_bridge, }) } @@ -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 { @@ -61,15 +73,26 @@ impl Server { } } +struct ProxyOptions { + pac_path: Option, + tunnel_domains: Vec, +} + struct ProxyConnection { socket: Option, + options: Arc, command_bridge: mpsc::Sender, } impl ProxyConnection { - fn new(socket: TcpStream, command_bridge: mpsc::Sender) -> ProxyConnection { + fn new( + socket: TcpStream, + options: Arc, + command_bridge: mpsc::Sender, + ) -> ProxyConnection { ProxyConnection { socket: Some(socket), + options, command_bridge, } } @@ -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) { @@ -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?; @@ -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) => {