From 368f6657fdf3c24bb75f8b06383805c48c792152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=A3rebe=20-=20Romain=20GERARD?= Date: Tue, 30 Apr 2024 22:21:47 +0200 Subject: [PATCH] Turn match in restriction config into a list --- restrictions.yaml | 41 ++++++++++++++++------ src/main.rs | 3 +- src/restrictions/mod.rs | 72 ++++++++++++++++++++++++++++----------- src/restrictions/types.rs | 16 ++++++++- src/tunnel/server.rs | 12 +++---- 5 files changed, 105 insertions(+), 39 deletions(-) diff --git a/restrictions.yaml b/restrictions.yaml index 795b73a..368cb4c 100644 --- a/restrictions.yaml +++ b/restrictions.yaml @@ -3,21 +3,28 @@ restrictions: - name: "Allow all" description: "This restriction allows all requests" - # This restriction apply only if it matches the prefix that match the given regex - # 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" - match: !PathPrefix "^.*$" + # This restriction apply only and only if all matchers match/are evaluated to true + # It is a logical AND + match: + # This match apply only if it succeeds to match the path prefix with the given regex + # 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 "^.*$" + # The only other possible match type for now is !Any, that match everything/any request + # - !Any - # This is th list of tunnels your restriction is going to allow - # The list is going to be checked in order, the first match is going to allow the request + # This is the list of tunnels your restriction is going to allow + # The list is checked in order, the first match is going to allow the request allow: # !Tunnel allows forward tunnels - !Tunnel # Protocol that are allowed. Empty list means all protocols are allowed + # Logical OR protocol: - Tcp - Udp # Port that are allowed. Can be a single port or an inclusive range (i.e. 80..90) + # Logical OR port: - 80 - 443 @@ -25,7 +32,8 @@ restrictions: # if the tunnel wants to connect to a specific host, this regex must match host: ^.*$ - # if the tunnel wants to connect to a specific IP, it must match one of the network cidr + # if the tunnel wants to connect to a specific IP, it must be included in one of the network cidr + # Logical OR cidr: - 0.0.0.0/0 - ::/0 @@ -49,7 +57,8 @@ restrictions: restrictions: - name: "example 1" description: "Only allow forward tunnels to port 443 and forbid reverse tunnels" - match: !PathPrefix "^.*$" + match: + - !PathPrefix "^.*$" allow: - !Tunnel port: @@ -58,7 +67,8 @@ restrictions: restrictions: - name: "example 2" description: "Only allow forward tunnels to local ssh and forbid reverse tunnels" - match: !PathPrefix "^.*$" + match: + - !PathPrefix "^.*$" allow: - !Tunnel protocol: @@ -72,7 +82,8 @@ restrictions: restrictions: - name: "example 3" description: "Only allow socks5 reverse tunnels listening on port between 1080..1443 on lan network" - match: !PathPrefix "^.*$" + match: + - !PathPrefix "^.*$" allow: - !ReverseTunnel protocol: @@ -85,7 +96,15 @@ restrictions: restrictions: - name: "example 4" description: "Allow everything for client using path prefix my-super-secret-path" - match: !PathPrefix "^my-super-secret-path$" + match: + - !PathPrefix "^my-super-secret-path$" allow: - !Tunnel - !ReverseTunnel +--- +restrictions: + - name: "example 5" + description: "Forbid everything ..." + match: + - !Any + allow: [] diff --git a/src/main.rs b/src/main.rs index 0421739..b0cc1a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1278,9 +1278,10 @@ async fn main() { args.restrict_http_upgrade_path_prefix.as_deref().unwrap_or(&[]), &restrict_to, ) - .expect("Cannot covertion restriction rules from path-prefix and restric-to"); + .expect("Cannot convert restriction rules from path-prefix and restric-to"); restriction_cfg }; + debug!("Restriction rules: {:?}", restrictions); let server_config = WsServerConfig { socket_so_mark: args.socket_so_mark, diff --git a/src/restrictions/mod.rs b/src/restrictions/mod.rs index 5581099..e19edf0 100644 --- a/src/restrictions/mod.rs +++ b/src/restrictions/mod.rs @@ -1,11 +1,18 @@ -use crate::restrictions::types::{default_cidr, default_host}; -use regex::Regex; +use ipnet::IpNet; use std::fs::File; use std::io::BufReader; +use std::net::IpAddr; use std::ops::RangeInclusive; use std::path::Path; +use std::str::FromStr; +use std::vec; + +use regex::Regex; + use types::RestrictionsRules; +use crate::restrictions::types::{default_cidr, default_host}; + pub mod types; impl RestrictionsRules { @@ -18,41 +25,68 @@ impl RestrictionsRules { path_prefixes: &[String], restrict_to: &[(String, u16)], ) -> anyhow::Result { - let mut tunnels_restrictions = if restrict_to.is_empty() { + let tunnels_restrictions = if restrict_to.is_empty() { let r = types::AllowConfig::Tunnel(types::AllowTunnelConfig { protocol: vec![], port: vec![], host: default_host(), cidr: default_cidr(), }); - vec![r] + let reverse_tunnel = types::AllowConfig::ReverseTunnel(types::AllowReverseTunnelConfig { + protocol: vec![], + port: vec![], + cidr: default_cidr(), + }); + + vec![r, reverse_tunnel] } else { restrict_to .iter() .map(|(host, port)| { let reg = Regex::new(&format!("^{}$", regex::escape(host)))?; - Ok(types::AllowConfig::Tunnel(types::AllowTunnelConfig { - protocol: vec![], - port: vec![RangeInclusive::new(*port, *port)], - host: reg, - cidr: default_cidr(), - })) + let tunnels = if let Ok(ip) = IpAddr::from_str(host) { + vec![ + types::AllowConfig::Tunnel(types::AllowTunnelConfig { + protocol: vec![], + port: vec![RangeInclusive::new(*port, *port)], + host: reg, + cidr: default_cidr(), + }), + types::AllowConfig::ReverseTunnel(types::AllowReverseTunnelConfig { + protocol: vec![], + port: vec![RangeInclusive::new(*port, *port)], + cidr: vec![IpNet::new(ip, if ip.is_ipv4() { 32 } else { 128 })?], + }), + ] + } else { + vec![ + types::AllowConfig::Tunnel(types::AllowTunnelConfig { + protocol: vec![], + port: vec![RangeInclusive::new(*port, *port)], + host: reg, + cidr: default_cidr(), + }), + types::AllowConfig::ReverseTunnel(types::AllowReverseTunnelConfig { + protocol: vec![], + port: vec![], + cidr: default_cidr(), + }), + ] + }; + + Ok(tunnels) }) .collect::, anyhow::Error>>()? + .into_iter() + .flatten() + .collect() }; - tunnels_restrictions.push(types::AllowConfig::ReverseTunnel(types::AllowReverseTunnelConfig { - protocol: vec![], - port: vec![], - cidr: default_cidr(), - })); - let restrictions = if path_prefixes.is_empty() { // if no path prefixes are provided, we allow all - let reg = Regex::new(".").unwrap(); let r = types::RestrictionConfig { name: "Allow All".to_string(), - r#match: types::MatchConfig::PathPrefix(reg), + r#match: vec![types::MatchConfig::Any], allow: tunnels_restrictions, }; vec![r] @@ -63,7 +97,7 @@ impl RestrictionsRules { let reg = Regex::new(&format!("^{}$", regex::escape(path_prefix)))?; Ok(types::RestrictionConfig { name: format!("Allow path prefix {}", path_prefix), - r#match: types::MatchConfig::PathPrefix(reg), + r#match: vec![types::MatchConfig::PathPrefix(reg)], allow: tunnels_restrictions.clone(), }) }) diff --git a/src/restrictions/types.rs b/src/restrictions/types.rs index f20e5e2..b7eefde 100644 --- a/src/restrictions/types.rs +++ b/src/restrictions/types.rs @@ -12,7 +12,8 @@ pub struct RestrictionsRules { #[derive(Debug, Clone, Deserialize)] pub struct RestrictionConfig { pub name: String, - pub r#match: MatchConfig, + #[serde(deserialize_with = "deserialize_non_empty_vec")] + pub r#match: Vec, pub allow: Vec, } @@ -109,6 +110,19 @@ where Ok(ranges) } +fn deserialize_non_empty_vec<'de, D, T>(d: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + let vec = >::deserialize(d)?; + if vec.is_empty() { + Err(serde::de::Error::custom("List must not be empty")) + } else { + Ok(vec) + } +} + impl From<&LocalProtocol> for ReverseTunnelConfigProtocol { fn from(value: &LocalProtocol) -> Self { match value { diff --git a/src/tunnel/server.rs b/src/tunnel/server.rs index 6a3fcd0..fd382ea 100644 --- a/src/tunnel/server.rs +++ b/src/tunnel/server.rs @@ -319,13 +319,11 @@ fn validate_tunnel<'a>( restrictions: &'a RestrictionsRules, ) -> Result<&'a RestrictionConfig, Response> { for restriction in &restrictions.restrictions { - match &restriction.r#match { - MatchConfig::Any => {} - MatchConfig::PathPrefix(path) => { - if !path.is_match(path_prefix) { - continue; - } - } + if !restriction.r#match.iter().all(|m| match m { + MatchConfig::Any => true, + MatchConfig::PathPrefix(path) => path.is_match(path_prefix), + }) { + continue; } for allow in &restriction.allow {