diff --git a/restrictions.yaml b/restrictions.yaml index c86e95a..7074944 100644 --- a/restrictions.yaml +++ b/restrictions.yaml @@ -10,6 +10,9 @@ restrictions: # The regex does a match, so if you want to match exactly you need to bound the pattern with ^ $ # I.e: "tesotron" is going to match "XXXtesotronXXX", but "^tesotron$" is going to match only "tesotron" - !PathPrefix "^.*$" + # This match applies only if it succeeds to match the Authentication Header with the given regex. + # If present, Authentication Header must exists and must match the regex. + # - !Authorization "^[Bb]earer +actual_bearer_token_to_match$" # The only other possible match type for now is !Any, that match everything/any request # - !Any diff --git a/src/restrictions/types.rs b/src/restrictions/types.rs index b4a2069..a55ce22 100644 --- a/src/restrictions/types.rs +++ b/src/restrictions/types.rs @@ -23,6 +23,8 @@ pub enum MatchConfig { Any, #[serde(with = "serde_regex")] PathPrefix(Regex), + #[serde(with = "serde_regex")] + Authorization(Regex), } #[derive(Debug, Clone, Deserialize)] diff --git a/src/tunnel/server/server.rs b/src/tunnel/server/server.rs index 702e138..3d0aad9 100644 --- a/src/tunnel/server/server.rs +++ b/src/tunnel/server/server.rs @@ -31,8 +31,8 @@ use crate::tunnel::server::handler_http2::http_server_upgrade; use crate::tunnel::server::handler_websocket::ws_server_upgrade; use crate::tunnel::server::reverse_tunnel::ReverseTunnelServer; use crate::tunnel::server::utils::{ - HttpResponse, bad_request, extract_path_prefix, extract_tunnel_info, extract_x_forwarded_for, find_mapped_port, - validate_tunnel, + HttpResponse, bad_request, extract_authorization, extract_path_prefix, extract_tunnel_info, + extract_x_forwarded_for, find_mapped_port, validate_tunnel, }; use crate::tunnel::tls_reloader::TlsReloader; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; @@ -121,7 +121,9 @@ impl WsServer { bad_request() })?; - let restriction = validate_tunnel(&remote, path_prefix, &restrictions).ok_or_else(|| { + let authorization = extract_authorization(req); + + let restriction = validate_tunnel(&remote, path_prefix, authorization, &restrictions).ok_or_else(|| { warn!("Rejecting connection with not allowed destination: {remote:?}"); bad_request() })?; diff --git a/src/tunnel/server/utils.rs b/src/tunnel/server/utils.rs index 2294da2..da59b5c 100644 --- a/src/tunnel/server/utils.rs +++ b/src/tunnel/server/utils.rs @@ -49,6 +49,12 @@ pub(super) fn find_mapped_port(req_port: u16, restriction: &RestrictionConfig) - remote_port } +#[inline] +pub(super) fn extract_authorization(req: &Request) -> Option<&str> { + let val = req.headers().get("Authorization")?; + val.to_str().ok() +} + #[inline] pub(super) fn extract_x_forwarded_for(req: &Request) -> Option<(IpAddr, &str)> { let x_forward_for = req.headers().get("X-Forwarded-For")?; @@ -104,12 +110,13 @@ pub(super) fn extract_tunnel_info(req: &Request) -> anyhow::Result bool { + fn filter(self: &RestrictionConfig, path_prefix: &str, authorization: Option<&str>) -> bool { self.r#match.iter().all(|m| match m { MatchConfig::Any => true, MatchConfig::PathPrefix(path) => path.is_match(path_prefix), + MatchConfig::Authorization(auth) => authorization.is_some_and(|val| auth.is_match(val)), }) } } @@ -186,12 +193,13 @@ impl AllowConfig { pub(super) fn validate_tunnel<'a>( remote: &RemoteAddr, path_prefix: &str, + authorization: Option<&str>, restrictions: &'a RestrictionsRules, ) -> Option<&'a RestrictionConfig> { restrictions .restrictions .iter() - .filter(|restriction| restriction.for_path(path_prefix)) + .filter(|restriction| restriction.filter(path_prefix, authorization)) .find(|restriction| restriction.allow.iter().any(|allow| allow.is_allowed(remote))) } @@ -208,7 +216,7 @@ pub(super) fn inject_cookie(response: &mut http::Response, remote_add #[cfg(test)] mod tests { use super::*; - use crate::restrictions::types::{AllowReverseTunnelConfig, AllowTunnelConfig}; + use crate::restrictions::types::{AllowReverseTunnelConfig, AllowTunnelConfig, default_cidr, default_host}; use crate::tunnel::LocalProtocol; use ipnet::{IpNet, Ipv4Net}; use regex::Regex; @@ -249,7 +257,9 @@ mod tests { port: 80, }; assert_eq!( - validate_tunnel(&remote, "/doesnt/matter", &restrictions).unwrap().name, + validate_tunnel(&remote, "/doesnt/matter", None, &restrictions) + .unwrap() + .name, restrictions.restrictions[0].name ); @@ -259,7 +269,9 @@ mod tests { port: 80, }; assert_eq!( - validate_tunnel(&remote, "/doesnt/matter", &restrictions).unwrap().name, + validate_tunnel(&remote, "/doesnt/matter", None, &restrictions) + .unwrap() + .name, restrictions.restrictions[1].name ); @@ -268,14 +280,14 @@ mod tests { host: Host::Ipv4([127, 0, 0, 1].into()), port: 81, }; - assert!(validate_tunnel(&remote, "/doesnt/matter", &restrictions).is_none()); + assert!(validate_tunnel(&remote, "/doesnt/matter", None, &restrictions).is_none()); let remote = RemoteAddr { protocol: LocalProtocol::Tcp { proxy_protocol: false }, host: Host::Ipv4([127, 0, 1, 1].into()), port: 80, }; - assert!(validate_tunnel(&remote, "/doesnt/matter", &restrictions).is_none()); + assert!(validate_tunnel(&remote, "/doesnt/matter", None, &restrictions).is_none()); let remote = RemoteAddr { protocol: LocalProtocol::Tcp { proxy_protocol: false }, @@ -283,7 +295,9 @@ mod tests { port: 80, }; assert_eq!( - validate_tunnel(&remote, "/doesnt/matter", &restrictions).unwrap().name, + validate_tunnel(&remote, "/doesnt/matter", None, &restrictions) + .unwrap() + .name, restrictions.restrictions[0].name ); @@ -292,14 +306,46 @@ mod tests { host: Host::Domain("not.com".into()), port: 80, }; - assert!(validate_tunnel(&remote, "/doesnt/matter", &restrictions).is_none()); + assert!(validate_tunnel(&remote, "/doesnt/matter", None, &restrictions).is_none()); let remote = RemoteAddr { protocol: LocalProtocol::Tcp { proxy_protocol: false }, host: Host::Ipv6(Ipv6Addr::LOCALHOST), port: 80, }; - assert!(validate_tunnel(&remote, "/doesnt/matter", &restrictions).is_none()); + assert!(validate_tunnel(&remote, "/doesnt/matter", None, &restrictions).is_none()); + } + + #[test] + fn test_validate_tunnel_with_auth() { + let restrictions = RestrictionsRules { + restrictions: vec![RestrictionConfig { + name: "restrict1".into(), + r#match: vec![MatchConfig::Authorization( + Regex::new("^[Bb]earer +the-bearer-token$").unwrap(), + )], + allow: vec![AllowConfig::Tunnel(AllowTunnelConfig { + protocol: vec![], + port: vec![], + cidr: default_cidr(), + host: default_host(), + })], + }], + }; + + let remote = RemoteAddr { + protocol: LocalProtocol::Tcp { proxy_protocol: false }, + host: Host::Ipv4([127, 0, 0, 1].into()), + port: 80, + }; + assert_eq!( + validate_tunnel(&remote, "/doesnt/matter", Some("Bearer the-bearer-token"), &restrictions) + .unwrap() + .name, + restrictions.restrictions[0].name + ); + assert!(validate_tunnel(&remote, "/doesnt/matter", Some("Bearer other-bearer-token"), &restrictions).is_none()); + assert!(validate_tunnel(&remote, "/doesnt/matter", None, &restrictions).is_none()); } #[test]