Allow multiple ports in restriction file

This commit is contained in:
Σrebe - Romain GERARD 2024-04-28 20:45:41 +02:00
parent 135fcb5127
commit 3c84c59a11
No known key found for this signature in database
GPG key ID: 7A42B4B97E0332F4
5 changed files with 72 additions and 58 deletions

View file

@ -18,7 +18,10 @@ restrictions:
- Tcp - Tcp
- Udp - Udp
# Port that are allowed. Can be a single port or an inclusive range (i.e. 80..90) # Port that are allowed. Can be a single port or an inclusive range (i.e. 80..90)
port: 9999 port:
- 80
- 443
- 8080..8089
# if the tunnel wants to connect to a specific host, this regex must match # if the tunnel wants to connect to a specific host, this regex must match
host: ^.*$ host: ^.*$
@ -35,7 +38,8 @@ restrictions:
- Udp - Udp
- Socks5 - Socks5
- Unix - Unix
port: 1..65535 port:
- 1..65535
cidr: cidr:
- 0.0.0.0/0 - 0.0.0.0/0
- ::/0 - ::/0
@ -48,7 +52,8 @@ restrictions:
match: !PathPrefix "^.*$" match: !PathPrefix "^.*$"
allow: allow:
- !Tunnel - !Tunnel
port: 443 port:
- 443
--- ---
restrictions: restrictions:
- name: "example 2" - name: "example 2"
@ -58,7 +63,8 @@ restrictions:
- !Tunnel - !Tunnel
protocol: protocol:
- Tcp - Tcp
port: 22 port:
- 22
host: ^localhost$ host: ^localhost$
cidr: cidr:
- 127.0.0.1/32 - 127.0.0.1/32
@ -71,7 +77,8 @@ restrictions:
- !ReverseTunnel - !ReverseTunnel
protocol: protocol:
- Socks5 - Socks5
port: 1080..1443 port:
- 1080..1443
cidr: cidr:
- 192.168.0.0/16 - 192.168.0.0/16
--- ---

View file

@ -259,12 +259,6 @@ struct Server {
#[arg(long, default_value = "false", verbatim_doc_comment)] #[arg(long, default_value = "false", verbatim_doc_comment)]
websocket_mask_frame: bool, websocket_mask_frame: bool,
/// Server will only accept connection from the specified tunnel information.
/// Can be specified multiple time
/// Example: --restrict-to "google.com:443" --restrict-to "localhost:22"
#[arg(long, value_name = "DEST:PORT", verbatim_doc_comment)]
restrict_to: Option<Vec<String>>,
/// Dns resolver to use to lookup ips of domain name /// Dns resolver to use to lookup ips of domain name
/// This option is not going to work if you use transparent proxy /// This option is not going to work if you use transparent proxy
/// Can be specified multiple time /// Can be specified multiple time
@ -277,6 +271,12 @@ struct Server {
#[arg(long, verbatim_doc_comment)] #[arg(long, verbatim_doc_comment)]
dns_resolver: Option<Vec<Url>>, dns_resolver: Option<Vec<Url>>,
/// Server will only accept connection from the specified tunnel information.
/// Can be specified multiple time
/// Example: --restrict-to "google.com:443" --restrict-to "localhost:22"
#[arg(long, value_name = "DEST:PORT", verbatim_doc_comment)]
restrict_to: Option<Vec<String>>,
/// Server will only accept connection from if this specific path prefix is used during websocket upgrade. /// Server will only accept connection from if this specific path prefix is used during websocket upgrade.
/// Useful if you specify in the client a custom path prefix, and you want the server to only allow this one. /// Useful if you specify in the client a custom path prefix, and you want the server to only allow this one.
/// The path prefix act as a secret to authenticate clients /// The path prefix act as a secret to authenticate clients
@ -292,7 +292,7 @@ struct Server {
/// Path to the location of the restriction yaml config file. /// Path to the location of the restriction yaml config file.
/// Restriction file is automatically reloaded if it changes /// Restriction file is automatically reloaded if it changes
#[arg(long, verbatim_doc_comment)] #[arg(long, verbatim_doc_comment)]
restriction_file: Option<PathBuf>, restrict_config: Option<PathBuf>,
/// [Optional] Use custom certificate (pem) instead of the default embedded self-signed certificate. /// [Optional] Use custom certificate (pem) instead of the default embedded self-signed certificate.
/// The certificate will be automatically reloaded if it changes /// The certificate will be automatically reloaded if it changes
@ -1260,7 +1260,7 @@ async fn main() {
} }
}; };
let restrictions = if let Some(path) = &args.restriction_file { let restrictions = if let Some(path) = &args.restrict_config {
RestrictionsRules::from_config_file(path).expect("Cannot parse restriction file") RestrictionsRules::from_config_file(path).expect("Cannot parse restriction file")
} else { } else {
let restrict_to: Vec<(String, u16)> = args let restrict_to: Vec<(String, u16)> = args

View file

@ -1,4 +1,4 @@
use crate::restrictions::types::{default_cidr, default_host, default_port}; use crate::restrictions::types::{default_cidr, default_host};
use regex::Regex; use regex::Regex;
use std::fs::File; use std::fs::File;
use std::io::BufReader; use std::io::BufReader;
@ -21,7 +21,7 @@ impl RestrictionsRules {
let mut tunnels_restrictions = if restrict_to.is_empty() { let mut tunnels_restrictions = if restrict_to.is_empty() {
let r = types::AllowConfig::Tunnel(types::AllowTunnelConfig { let r = types::AllowConfig::Tunnel(types::AllowTunnelConfig {
protocol: vec![], protocol: vec![],
port: default_port(), port: vec![],
host: default_host(), host: default_host(),
cidr: default_cidr(), cidr: default_cidr(),
}); });
@ -30,21 +30,20 @@ impl RestrictionsRules {
restrict_to restrict_to
.iter() .iter()
.map(|(host, port)| { .map(|(host, port)| {
// Fixme: Remove the unwrap let reg = Regex::new(&format!("^{}$", regex::escape(host)))?;
let reg = Regex::new(&format!("^{}$", regex::escape(host))).unwrap(); Ok(types::AllowConfig::Tunnel(types::AllowTunnelConfig {
types::AllowConfig::Tunnel(types::AllowTunnelConfig {
protocol: vec![], protocol: vec![],
port: RangeInclusive::new(*port, *port), port: vec![RangeInclusive::new(*port, *port)],
host: reg, host: reg,
cidr: default_cidr(), cidr: default_cidr(),
}) }))
}) })
.collect() .collect::<Result<Vec<_>, anyhow::Error>>()?
}; };
tunnels_restrictions.push(types::AllowConfig::ReverseTunnel(types::AllowReverseTunnelConfig { tunnels_restrictions.push(types::AllowConfig::ReverseTunnel(types::AllowReverseTunnelConfig {
protocol: vec![], protocol: vec![],
port: default_port(), port: vec![],
cidr: default_cidr(), cidr: default_cidr(),
})); }));
@ -61,19 +60,16 @@ impl RestrictionsRules {
path_prefixes path_prefixes
.iter() .iter()
.map(|path_prefix| { .map(|path_prefix| {
// Fixme: Remove the unwrap let reg = Regex::new(&format!("^{}$", regex::escape(path_prefix)))?;
let reg = Regex::new(&format!("^{}$", regex::escape(path_prefix))).unwrap(); Ok(types::RestrictionConfig {
types::RestrictionConfig {
name: format!("Allow path prefix {}", path_prefix), name: format!("Allow path prefix {}", path_prefix),
r#match: types::MatchConfig::PathPrefix(reg), r#match: types::MatchConfig::PathPrefix(reg),
allow: tunnels_restrictions.clone(), allow: tunnels_restrictions.clone(),
} })
}) })
.collect() .collect::<Result<Vec<_>, anyhow::Error>>()?
}; };
let restrictions = RestrictionsRules { restrictions }; Ok(RestrictionsRules { restrictions })
Ok(restrictions)
} }
} }

View file

@ -35,8 +35,8 @@ pub struct AllowTunnelConfig {
pub protocol: Vec<TunnelConfigProtocol>, pub protocol: Vec<TunnelConfigProtocol>,
#[serde(deserialize_with = "deserialize_port_range")] #[serde(deserialize_with = "deserialize_port_range")]
#[serde(default = "default_port")] #[serde(default)]
pub port: RangeInclusive<u16>, pub port: Vec<RangeInclusive<u16>>,
#[serde(with = "serde_regex")] #[serde(with = "serde_regex")]
#[serde(default = "default_host")] #[serde(default = "default_host")]
@ -52,8 +52,8 @@ pub struct AllowReverseTunnelConfig {
pub protocol: Vec<ReverseTunnelConfigProtocol>, pub protocol: Vec<ReverseTunnelConfigProtocol>,
#[serde(deserialize_with = "deserialize_port_range")] #[serde(deserialize_with = "deserialize_port_range")]
#[serde(default = "default_port")] #[serde(default)]
pub port: RangeInclusive<u16>, pub port: Vec<RangeInclusive<u16>>,
#[serde(default = "default_cidr")] #[serde(default = "default_cidr")]
pub cidr: Vec<IpNet>, pub cidr: Vec<IpNet>,
@ -75,10 +75,6 @@ pub enum ReverseTunnelConfigProtocol {
Unknown, Unknown,
} }
pub fn default_port() -> RangeInclusive<u16> {
RangeInclusive::new(1, 65535)
}
pub fn default_host() -> Regex { pub fn default_host() -> Regex {
Regex::new("^.*$").unwrap() Regex::new("^.*$").unwrap()
} }
@ -87,22 +83,30 @@ pub fn default_cidr() -> Vec<IpNet> {
vec![IpNet::V4(Ipv4Net::default()), IpNet::V6(Ipv6Net::default())] vec![IpNet::V4(Ipv4Net::default()), IpNet::V6(Ipv6Net::default())]
} }
fn deserialize_port_range<'de, D>(deserializer: D) -> Result<RangeInclusive<u16>, D::Error> fn deserialize_port_range<'de, D>(deserializer: D) -> Result<Vec<RangeInclusive<u16>>, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
let s = String::deserialize(deserializer)?; let s = Vec::<String>::deserialize(deserializer)?;
let range = if let Some((l, r)) = s.split_once("..") { let ranges = s
RangeInclusive::new( .into_iter()
l.parse().map_err(serde::de::Error::custom)?, .map(|s| {
r.parse().map_err(serde::de::Error::custom)?, let range: Result<RangeInclusive<u16>, D::Error> = if let Some((l, r)) = s.split_once("..") {
) Ok(RangeInclusive::new(
} else { l.parse().map_err(<D::Error as serde::de::Error>::custom)?,
let port = s.parse::<u16>().map_err(serde::de::Error::custom)?; r.parse().map_err(<D::Error as serde::de::Error>::custom)?,
RangeInclusive::new(port, port) ))
}; } else {
let port = s.parse::<u16>().map_err(serde::de::Error::custom)?;
Ok(RangeInclusive::new(port, port))
};
range
})
.collect::<Vec<_>>()
.into_iter()
.collect::<Result<Vec<RangeInclusive<u16>>, D::Error>>()?;
Ok(range) Ok(ranges)
} }
impl From<&LocalProtocol> for ReverseTunnelConfigProtocol { impl From<&LocalProtocol> for ReverseTunnelConfigProtocol {

View file

@ -211,7 +211,7 @@ where
} }
Some(Ok(cnx)) => { Some(Ok(cnx)) => {
if tx.send_timeout(cnx, send_timeout).await.is_err() { if tx.send_timeout(cnx, send_timeout).await.is_err() {
info!("New remote connection failed to be picked by client after {}s. Closing remote tunnel server", send_timeout.as_secs()); info!("New reverse connection failed to be picked by client after {}s. Closing reverse tunnel server", send_timeout.as_secs());
break; break;
} }
} }
@ -223,7 +223,7 @@ where
} }
} }
} }
info!("Stopping listening server"); info!("Stopping listening reverse server");
}; };
tokio::spawn(fut.instrument(Span::current())); tokio::spawn(fut.instrument(Span::current()));
@ -233,7 +233,7 @@ where
let cnx = listening_server let cnx = listening_server
.recv() .recv()
.await .await
.ok_or_else(|| anyhow!("listening server stopped"))?; .ok_or_else(|| anyhow!("listening reverse server stopped"))?;
servers.lock().insert(local_srv.clone(), listening_server); servers.lock().insert(local_srv.clone(), listening_server);
Ok(cnx) Ok(cnx)
} }
@ -314,7 +314,6 @@ fn extract_tunnel_info(req: &Request<Incoming>) -> Result<TokenData<JwtTunnelCon
#[inline] #[inline]
fn validate_tunnel<'a>( fn validate_tunnel<'a>(
_req: &Request<Incoming>,
remote: &RemoteAddr, remote: &RemoteAddr,
path_prefix: &str, path_prefix: &str,
restrictions: &'a RestrictionsRules, restrictions: &'a RestrictionsRules,
@ -332,7 +331,11 @@ fn validate_tunnel<'a>(
for allow in &restriction.allow { for allow in &restriction.allow {
match allow { match allow {
AllowConfig::ReverseTunnel(allow) => { AllowConfig::ReverseTunnel(allow) => {
if !remote.protocol.is_reverse_tunnel() || !allow.port.contains(&remote.port) { if !remote.protocol.is_reverse_tunnel() {
continue;
}
if !allow.port.is_empty() && !allow.port.iter().any(|range| range.contains(&remote.port)) {
continue; continue;
} }
@ -366,7 +369,11 @@ fn validate_tunnel<'a>(
} }
AllowConfig::Tunnel(allow) => { AllowConfig::Tunnel(allow) => {
if remote.protocol.is_reverse_tunnel() || !allow.port.contains(&remote.port) { if remote.protocol.is_reverse_tunnel() {
continue;
}
if !allow.port.is_empty() && !allow.port.iter().any(|range| range.contains(&remote.port)) {
continue; continue;
} }
@ -458,7 +465,7 @@ async fn ws_server_upgrade(
} }
}; };
match validate_tunnel(&req, &remote, path_prefix, &server_config.restrictions) { match validate_tunnel(&remote, path_prefix, &server_config.restrictions) {
Ok(matched_restriction) => { Ok(matched_restriction) => {
info!("Tunnel accepted due to matched restriction: {}", matched_restriction.name); info!("Tunnel accepted due to matched restriction: {}", matched_restriction.name);
} }
@ -573,7 +580,7 @@ async fn http_server_upgrade(
} }
}; };
match validate_tunnel(&req, &remote, path_prefix, &server_config.restrictions) { match validate_tunnel(&remote, path_prefix, &server_config.restrictions) {
Ok(matched_restriction) => { Ok(matched_restriction) => {
info!("Tunnel accepted due to matched restriction: {}", matched_restriction.name); info!("Tunnel accepted due to matched restriction: {}", matched_restriction.name);
} }