diff options
| author | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-04-28 16:17:07 +0200 |
|---|---|---|
| committer | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-04-30 12:58:01 +0200 |
| commit | 4165dacc7f2c2b6f85fc6ec1abc665b0d85a0b30 (patch) | |
| tree | 3b9a7247a6200590345c884cf2479bb61c759eef | |
| parent | 9c60042e85b37143cd9e0742f591f0a01c6bc165 (diff) | |
| download | mullvadvpn-4165dacc7f2c2b6f85fc6ec1abc665b0d85a0b30.tar.xz mullvadvpn-4165dacc7f2c2b6f85fc6ec1abc665b0d85a0b30.zip | |
Validate HTTP hostname in masque-server
| -rw-r--r-- | mullvad-masque-proxy/examples/masque-server.rs | 8 | ||||
| -rw-r--r-- | mullvad-masque-proxy/src/server/mod.rs | 164 | ||||
| -rw-r--r-- | mullvad-masque-proxy/tests/proxy.rs | 1 |
3 files changed, 134 insertions, 39 deletions
diff --git a/mullvad-masque-proxy/examples/masque-server.rs b/mullvad-masque-proxy/examples/masque-server.rs index 560a750e0a..e884e91c93 100644 --- a/mullvad-masque-proxy/examples/masque-server.rs +++ b/mullvad-masque-proxy/examples/masque-server.rs @@ -26,6 +26,13 @@ pub struct ServerArgs { #[arg(long = "allowed-ip", short = 'a', required = false)] allowed_ips: Vec<IpAddr>, + /// Server hostname. + /// + /// If set, the client must provide the correct hostname when connecting. If they don't, the + /// server will provide an HTTP 308 redirect to the correct URI. + #[arg(long)] + hostname: Option<String>, + /// Maximum packet size #[arg(long, short = 'm', default_value = "1700")] mtu: u16, @@ -46,6 +53,7 @@ async fn main() { let server = mullvad_masque_proxy::server::Server::bind( args.bind_addr, args.allowed_ips.iter().cloned().collect(), + args.hostname, tls_config.into(), args.mtu, ) diff --git a/mullvad-masque-proxy/src/server/mod.rs b/mullvad-masque-proxy/src/server/mod.rs index 30a43b6872..9a8a088892 100644 --- a/mullvad-masque-proxy/src/server/mod.rs +++ b/mullvad-masque-proxy/src/server/mod.rs @@ -2,10 +2,11 @@ use std::{ collections::HashSet, io, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + str::FromStr, sync::Arc, }; -use anyhow::{ensure, Context}; +use anyhow::{anyhow, ensure, Context}; use bytes::{Bytes, BytesMut}; use h3::{ proto::varint::VarInt, @@ -13,7 +14,7 @@ use h3::{ server::{self, Connection, RequestStream}, }; use h3_datagram::{datagram::Datagram, datagram_traits::HandleDatagramsExt}; -use http::{Request, StatusCode}; +use http::{Request, StatusCode, Uri}; use quinn::{crypto::rustls::QuicServerConfig, Endpoint, Incoming}; use tokio::{net::UdpSocket, select, sync::mpsc, task}; @@ -41,7 +42,17 @@ const MASQUE_WELL_KNOWN_PATH: &str = "/.well-known/masque/udp/"; pub struct Server { endpoint: Endpoint, + params: Arc<ServerParams>, +} + +struct ServerParams { + /// Allowed target IPs for the proxy connection allowed_hosts: AllowedIps, + + /// Server hostname (optional) + hostname: Option<String>, + + /// Maximum transfer unit mtu: u16, } @@ -60,6 +71,7 @@ impl Server { pub fn bind( bind_addr: SocketAddr, allowed_hosts: HashSet<IpAddr>, + hostname: Option<String>, tls_config: Arc<rustls::ServerConfig>, mtu: u16, ) -> Result<Self> { @@ -73,10 +85,13 @@ impl Server { Ok(Self { endpoint, - allowed_hosts: AllowedIps { - hosts: Arc::new(allowed_hosts), - }, - mtu, + params: Arc::new(ServerParams { + allowed_hosts: AllowedIps { + hosts: Arc::new(allowed_hosts), + }, + hostname, + mtu, + }), }) } @@ -101,14 +116,13 @@ impl Server { while let Some(new_connection) = self.endpoint.accept().await { tokio::spawn(Self::handle_incoming_connection( new_connection, - self.allowed_hosts.clone(), - self.mtu, + Arc::clone(&self.params), )); } Ok(()) } - async fn handle_incoming_connection(connection: Incoming, allowed_hosts: AllowedIps, mtu: u16) { + async fn handle_incoming_connection(connection: Incoming, server_params: Arc<ServerParams>) { match connection.await { Ok(conn) => { log::debug!("new connection established"); @@ -131,8 +145,7 @@ impl Server { quinn_conn, req, stream, - allowed_hosts.clone(), - mtu, + server_params, )); } @@ -155,21 +168,38 @@ impl Server { quinn_conn: quinn::Connection, request: Request<()>, mut stream: RequestStream<T, Bytes>, - allowed_hosts: AllowedIps, - mtu: u16, + server_params: Arc<ServerParams>, ) { - let Some(target_addr) = get_target_socketaddr(request.uri().path()) else { - return; + let proxy_uri = match ProxyUri::try_from(request.uri()) { + Ok(proxy_uri) => proxy_uri, + Err(e) => { + log::debug!("Bad proxy URI: {e}"); + return; + } }; - if !allowed_hosts.ip_allowed(target_addr.ip()) { + + if let Some(hostname) = &server_params.hostname { + if &proxy_uri.hostname != hostname { + let valid_uri = ProxyUri { + hostname: hostname.to_string(), + ..proxy_uri + }; + return handle_invalid_hostname(stream, valid_uri).await; + } + } + + if !server_params + .allowed_hosts + .ip_allowed(proxy_uri.target_addr.ip()) + { return handle_disallowed_ip(stream).await; } - let bind_addr = SocketAddr::new(unspecified_addr(target_addr.ip()), 0); + let bind_addr = SocketAddr::new(unspecified_addr(proxy_uri.target_addr.ip()), 0); let Ok(udp_socket) = UdpSocket::bind(bind_addr).await else { return handle_failed_socket(stream).await; }; - if let Err(err) = udp_socket.connect(target_addr).await { + if let Err(err) = udp_socket.connect(proxy_uri.target_addr).await { log::error!("Failed to set destination for UDP socket: {err}"); return handle_failed_socket(stream).await; }; @@ -188,8 +218,8 @@ impl Server { let mut proxy_rx_task = task::spawn(proxy_rx_task( stream_id, quinn_conn, - target_addr, - mtu, + proxy_uri.target_addr, + server_params.mtu, Arc::clone(&udp_socket), send_tx, )); @@ -370,21 +400,74 @@ async fn handle_failed_socket<T: BidiStream<Bytes>>(mut stream: RequestStream<T, let _ = stream.send_response(response).await; } -fn get_target_socketaddr(request_path: &str) -> Option<SocketAddr> { - // Establish if the URL path looks like `/.well-known/masque/udp/{ip}/{port}` - if !request_path.starts_with(MASQUE_WELL_KNOWN_PATH) { - return None; - }; - let (addr_str, port_str) = request_path - .strip_prefix(MASQUE_WELL_KNOWN_PATH)? - .trim_start_matches('/') - .split_once('/')?; - let port_str = port_str.trim_end_matches('/'); +async fn handle_invalid_hostname<T: BidiStream<Bytes>>( + mut stream: RequestStream<T, Bytes>, + valid_uri: ProxyUri, +) { + let uri = Uri::from(valid_uri).to_string(); + let response = http::Response::builder() + .status(StatusCode::PERMANENT_REDIRECT) + .header("Location", uri) + .body(()) + .unwrap(); + let _ = stream.send_response(response).await; +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct ProxyUri { + hostname: String, + target_addr: SocketAddr, +} + +impl From<ProxyUri> for Uri { + fn from(proxy_uri: ProxyUri) -> Self { + Uri::builder() + .scheme("https") + .authority(proxy_uri.hostname) + .path_and_query(format!( + "{MASQUE_WELL_KNOWN_PATH}/{ip}/{port}", + ip = proxy_uri.target_addr.ip(), + port = proxy_uri.target_addr.port(), + )) + .build() + .unwrap() + } +} + +impl TryFrom<&Uri> for ProxyUri { + type Error = anyhow::Error; + + fn try_from(uri: &Uri) -> std::result::Result<Self, Self::Error> { + let host = uri.host().context("Expected a URI containing a host")?; + + let path = uri.path(); + let anyhow_path_err = + || anyhow!("Expected `/.well-known/masque/udp/<ip>/<port>`, found `{path}`"); + let (addr_str, port_str) = path + .strip_prefix(MASQUE_WELL_KNOWN_PATH) + .with_context(anyhow_path_err)? + .trim_start_matches('/') + .split_once('/') + .with_context(anyhow_path_err)?; + + let port_str = port_str.trim_end_matches('/'); + + Ok(ProxyUri { + hostname: host.to_string(), + target_addr: SocketAddr::new( + addr_str.parse().with_context(anyhow_path_err)?, + port_str.parse().with_context(anyhow_path_err)?, + ), + }) + } +} + +impl FromStr for ProxyUri { + type Err = anyhow::Error; - Some(SocketAddr::new( - addr_str.trim_start_matches('/').parse().ok()?, - port_str.parse().ok()?, - )) + fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { + ProxyUri::try_from(&Uri::from_str(s)?) + } } fn unspecified_addr(addr: IpAddr) -> IpAddr { @@ -402,18 +485,21 @@ mod test { fn test_get_good_slashy_ocketaddr() { let addr: IpAddr = "192.168.1.1".parse().unwrap(); let port: u16 = 7979; - let expected_addr = SocketAddr::new(addr, port); - let good_path = format!("{MASQUE_WELL_KNOWN_PATH}///{addr}/{port}////"); + let expected = ProxyUri { + hostname: "foo".to_string(), + target_addr: SocketAddr::new(addr, port), + }; + let good_path = format!("https://foo{MASQUE_WELL_KNOWN_PATH}///{addr}/{port}////"); - assert_eq!(get_target_socketaddr(&good_path).unwrap(), expected_addr) + assert_eq!(ProxyUri::from_str(&good_path).unwrap(), expected) } #[test] fn test_get_bad_socketaddr() { let addr: IpAddr = "192.168.1.1".parse().unwrap(); let port: u16 = 7979; - let good_path = format!("{MASQUE_WELL_KNOWN_PATH}{addr}adsfasd/asdfasdf/{port}"); + let bad_path = format!("{MASQUE_WELL_KNOWN_PATH}{addr}adsfasd/asdfasdf/{port}"); - assert_eq!(get_target_socketaddr(&good_path), None) + assert!(ProxyUri::from_str(&bad_path).is_err()) } } diff --git a/mullvad-masque-proxy/tests/proxy.rs b/mullvad-masque-proxy/tests/proxy.rs index 04eacfe78f..29a90ca6fb 100644 --- a/mullvad-masque-proxy/tests/proxy.rs +++ b/mullvad-masque-proxy/tests/proxy.rs @@ -148,6 +148,7 @@ async fn setup_masque(mtu: u16) -> anyhow::Result<(UdpSocket, UdpSocket)> { let server = server::Server::bind( any_localhost_addr, Default::default(), + None, Arc::new(server_tls_config), mtu, ) |
