mirror of
https://github.com/erebe/wstunnel.git
synced 2025-09-26 19:21:10 +08:00
Allow restrictions based on Authorization header (#428)
* Allow restrictions based on Authorization header Currently, server has only 2 types of restriction matcher: PathPrefix and Any. Lets augment the RestrictionConfig to also allow an Authorization Header matcher; if such matcher is present, the Auth header in the websocket upgrade request must match the regex set in the matcher. This provides additional security benefit than using the PathPrefix matcher in setups where wstunnel server sits behind a load-balancer or a reverse proxy, where the request's path is logged by such systems. * server/utils tests: Add test_validate_tunnel_with_auth Tests MatchConfig::Authorization based restrictions.
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -23,6 +23,8 @@ pub enum MatchConfig {
|
||||
Any,
|
||||
#[serde(with = "serde_regex")]
|
||||
PathPrefix(Regex),
|
||||
#[serde(with = "serde_regex")]
|
||||
Authorization(Regex),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
|
@@ -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()
|
||||
})?;
|
||||
|
@@ -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<Incoming>) -> Option<&str> {
|
||||
let val = req.headers().get("Authorization")?;
|
||||
val.to_str().ok()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn extract_x_forwarded_for(req: &Request<Incoming>) -> 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<Incoming>) -> anyhow::Result<Tok
|
||||
}
|
||||
|
||||
impl RestrictionConfig {
|
||||
/// Returns true if the path prefix matches the restriction or if the restriction is set to allow any path.
|
||||
/// Returns true if the parameters match the restriction config
|
||||
#[inline]
|
||||
fn for_path(self: &RestrictionConfig, path_prefix: &str) -> 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<impl Body>, 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]
|
||||
|
Reference in New Issue
Block a user