Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ resolver = "2"

[workspace.dependencies]
# internal crates

defguard_setup = { path = "./crates/defguard_setup", version = "0.0.0" }
defguard_common = { path = "./crates/defguard_common", version = "2.0.0" }
defguard_static_ip = { path = "./crates/defguard_static_ip", version = "0.0.0" }
defguard_core = { path = "./crates/defguard_core", version = "0.0.0" }
defguard_event_logger = { path = "./crates/defguard_event_logger", version = "0.0.0" }
defguard_event_router = { path = "./crates/defguard_event_router", version = "0.0.0" }
Expand All @@ -26,7 +29,6 @@ defguard_vpn_stats_purge = { path = "./crates/defguard_vpn_stats_purge", version
defguard_web_ui = { path = "./crates/defguard_web_ui", version = "0.0.0" }
defguard_certs = { path = "./crates/defguard_certs", version = "0.0.0" }
defguard_grpc_tls = { path = "./crates/defguard_grpc_tls", version = "0.0.0" }
defguard_setup = { path = "./crates/defguard_setup", version = "0.0.0" }
model_derive = { path = "./crates/model_derive", version = "0.0.0" }

# external dependencies
Expand Down Expand Up @@ -58,7 +60,11 @@ ipnetwork = "0.20"
jsonwebkey = { version = "0.4", features = ["pkcs-convert"] }
jsonwebtoken = { version = "10.3", features = ["rust_crypto"] }
ldap3 = { version = "0.12", default-features = false, features = ["tls"] }
lettre = { version = "0.11", default-features = false, features = ["builder", "smtp-transport", "tokio1-rustls-tls"] }
lettre = { version = "0.11", default-features = false, features = [
"builder",
"smtp-transport",
"tokio1-rustls-tls",
] }
matches = "0.1"
md4 = "0.10"
openidconnect = { version = "4.0", default-features = false, features = [
Expand Down
2 changes: 1 addition & 1 deletion crates/defguard_common/src/db/models/wireguard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ pub enum NetworkAddressError {
"Location {0} has no network that could contain IP address {1}, available networks: {2:?}"
)]
NoContainingNetwork(String, IpAddr, Vec<IpNetwork>),
#[error("IP address {1} is reserved for gateway in location {0}")]
#[error("IP address {1} is reserved for Gateway in location {0}")]
ReservedForGateway(String, IpAddr),
#[error("IP address {1} is network broadcast address in location {0}")]
IsBroadcastAddress(String, IpAddr),
Expand Down
133 changes: 133 additions & 0 deletions crates/defguard_common/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

use ipnetwork::IpNetwork;
use serde::Serialize;

/// Parse a string with comma-separated IP addresses.
/// Invalid addresses will be silently ignored.
Expand All @@ -23,3 +26,133 @@ pub fn parse_network_address_list(ips: &str) -> Vec<IpNetwork> {
})
.collect()
}

#[derive(Debug, Serialize, PartialEq)]
pub struct SplitIp {
network_part: String,
modifiable_part: String,
network_prefix: String,
ip: String,
}

/// Splits the IP address (IPv4 or IPv6) into three parts: network part, modifiable part and prefix
/// The network part is the part that can't be changed by the user.
/// This is to display an IP address in the UI like this: 192.168.(1.1)/16, where the part in the parenthesis can be changed by the user.
/// The algorithm works as follows:
/// 1. Get the network address, last address and IP address segments, e.g. 192.1.1.1 would be [192, 1, 1, 1]
/// 2. Iterate over the segments and compare the last address and network segments, as long as the current segments are equal, append the segment to the network part.
/// If they are not equal, we found the first modifiable segment (one of the segments of an address that may change between hosts in the same network),
/// append the rest of the segments to the modifiable part.
/// 3. Join the segments with the delimiter and return the network part, modifiable part and the network prefix
pub fn split_ip(ip: &IpAddr, network: &IpNetwork) -> SplitIp {
let network_addr = network.network();
let network_prefix = network.prefix();

let ip_segments = match ip {
IpAddr::V4(ip) => ip.octets().iter().map(|x| u16::from(*x)).collect(),
IpAddr::V6(ip) => ip.segments().to_vec(),
};

let last_addr_segments = match network {
IpNetwork::V4(net) => {
let last_ip = u32::from(net.ip()) | (!u32::from(net.mask()));
let last_ip: Ipv4Addr = last_ip.into();
last_ip.octets().iter().map(|x| u16::from(*x)).collect()
}
IpNetwork::V6(net) => {
let last_ip = u128::from(net.ip()) | (!u128::from(net.mask()));
let last_ip: Ipv6Addr = last_ip.into();
last_ip.segments().to_vec()
}
};

let network_segments = match network_addr {
IpAddr::V4(ip) => ip.octets().iter().map(|x| u16::from(*x)).collect(),
IpAddr::V6(ip) => ip.segments().to_vec(),
};

let mut network_part = String::new();
let mut modifiable_part = String::new();
let delimiter = if ip.is_ipv4() { "." } else { ":" };
let formatter = |x: &u16| {
if ip.is_ipv4() {
x.to_string()
} else {
format!("{x:04x}")
}
};

for (i, ((last_addr_segment, network_segment), ip_segment)) in last_addr_segments
.iter()
.zip(network_segments.iter())
.zip(ip_segments.iter())
.enumerate()
{
if last_addr_segment != network_segment {
let parts = ip_segments.split_at(i).1;
let joined = parts
.iter()
.map(formatter)
.collect::<Vec<String>>()
.join(delimiter);
modifiable_part.push_str(&joined);
break;
}
let formatted = formatter(ip_segment);
network_part.push_str(&formatted);
network_part.push_str(delimiter);
}

SplitIp {
ip: ip.to_string(),
network_part,
modifiable_part,
network_prefix: network_prefix.to_string(),
}
}

#[cfg(test)]
mod test {
use std::str::FromStr;

use super::*;

#[test]
fn test_ip_splitter() {
let net = split_ip(
&IpAddr::from_str("192.168.3.1").unwrap(),
&IpNetwork::from_str("192.168.3.1/30").unwrap(),
);

assert_eq!(net.network_part, "192.168.3.");
assert_eq!(net.modifiable_part, "1");
assert_eq!(net.network_prefix, "30");

let net = split_ip(
&IpAddr::from_str("192.168.5.7").unwrap(),
&IpNetwork::from_str("192.168.3.1/24").unwrap(),
);

assert_eq!(net.network_part, "192.168.5.");
assert_eq!(net.modifiable_part, "7");
assert_eq!(net.network_prefix, "24");

let net = split_ip(
&IpAddr::from_str("2001:0db8:85a3::8a2e:0370:7334").unwrap(),
&IpNetwork::from_str("2001:0db8:85a3::8a2e:0370:7334/64").unwrap(),
);

assert_eq!(net.network_part, "2001:0db8:85a3:0000:");
assert_eq!(net.modifiable_part, "0000:8a2e:0370:7334");
assert_eq!(net.network_prefix, "64");

let net = split_ip(
&IpAddr::from_str("2001:0db8::0010:8a2e:0370:aaaa").unwrap(),
&IpNetwork::from_str("2001:db8::10:8a2e:370:aaa8/125").unwrap(),
);

assert_eq!(net.network_part, "2001:0db8:0000:0000:0010:8a2e:0370:");
assert_eq!(net.modifiable_part, "aaaa");
assert_eq!(net.network_prefix, "125");
}
}
1 change: 1 addition & 0 deletions crates/defguard_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defguard_web_ui = { workspace = true }
defguard_version = { workspace = true }
model_derive = { workspace = true }
defguard_certs = { workspace = true }
defguard_static_ip = { workspace = true }

# external dependencies
anyhow = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions crates/defguard_core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use defguard_common::{
types::UrlParseError,
};
use defguard_mail::templates::TemplateError;
use defguard_static_ip::error::StaticIpError;
use thiserror::Error;
use tokio::sync::mpsc::error::SendError;
use utoipa::ToSchema;
Expand Down Expand Up @@ -85,6 +86,9 @@ pub enum WebError {
#[error(transparent)]
#[schema(value_type=Object)]
UrlParseError(#[from] UrlParseError),
#[error(transparent)]
#[schema(value_type=Object)]
StaticIpError(#[from] StaticIpError),
}

impl From<tonic::Status> for WebError {
Expand Down
18 changes: 18 additions & 0 deletions crates/defguard_core/src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use defguard_common::{
},
types::user_info::UserInfo,
};
use defguard_static_ip::error::StaticIpError;
use serde_json::{Value, json};
use sqlx::PgPool;
use utoipa::ToSchema;
Expand Down Expand Up @@ -41,6 +42,7 @@ pub(crate) mod pagination;
pub mod proxy;
pub mod settings;
pub(crate) mod ssh_authorized_keys;
pub(crate) mod static_ips;
pub(crate) mod support;
pub(crate) mod updates;
pub mod user;
Expand Down Expand Up @@ -121,6 +123,22 @@ impl From<WebError> for ApiResponse {
StatusCode::INTERNAL_SERVER_ERROR,
)
}
WebError::StaticIpError(err) => match err {
StaticIpError::InvalidIpAssignment(err) => {
ApiResponse::new(json!({"msg": err.to_string()}), StatusCode::BAD_REQUEST)
}
StaticIpError::NetworkNotFound(_) | StaticIpError::DeviceNotInNetwork(_, _) => {
error!("{err}");
ApiResponse::new(json!({"msg": err.to_string()}), StatusCode::BAD_REQUEST)
}
StaticIpError::SqlxError(_) => {
error!("{err}");
ApiResponse::new(
json!({"msg": "Internal server error"}),
StatusCode::INTERNAL_SERVER_ERROR,
)
}
},
WebError::AclError(err) => match err {
AclError::ParseIntError(_)
| AclError::IpNetworkError(_)
Expand Down
Loading
Loading