diff --git a/src/enterprise/firewall/iprange.rs b/src/enterprise/firewall/iprange.rs index 33eab39d..b1b4acd6 100644 --- a/src/enterprise/firewall/iprange.rs +++ b/src/enterprise/firewall/iprange.rs @@ -76,6 +76,23 @@ impl IpAddrRange { Self::V6(_) => true, } } + + /// Returns the start of the range. + pub fn start(&self) -> IpAddr { + match self { + Self::V4(range) => IpAddr::V4(*range.start()), + Self::V6(range) => IpAddr::V6(*range.start()), + } + } + + /// Returns the end of the range. + /// If the range is empty, returns the start of the range. + pub fn end(&self) -> IpAddr { + match self { + Self::V4(range) => IpAddr::V4(*range.end()), + Self::V6(range) => IpAddr::V6(*range.end()), + } + } } impl Iterator for IpAddrRange { diff --git a/src/enterprise/firewall/mod.rs b/src/enterprise/firewall/mod.rs index 5f564697..ef4304d2 100644 --- a/src/enterprise/firewall/mod.rs +++ b/src/enterprise/firewall/mod.rs @@ -26,6 +26,24 @@ pub(crate) enum Address { } impl Address { + // FIXME: remove after merging nft hotfix into dev + #[allow(dead_code)] + pub fn first(&self) -> IpAddr { + match self { + Address::Network(network) => network.ip(), + Address::Range(range) => range.start(), + } + } + + // FIXME: remove after merging nft hotfix into dev + #[allow(dead_code)] + pub fn last(&self) -> IpAddr { + match self { + Address::Network(network) => max_address(network), + Address::Range(range) => range.end(), + } + } + pub fn from_proto(ip: &proto::enterprise::firewall::IpAddress) -> Result { match &ip.address { Some(proto::enterprise::firewall::ip_address::Address::Ip(ip)) => { @@ -322,3 +340,84 @@ pub enum FirewallError { #[error("Firewall transaction failed: {0}")] TransactionFailed(String), } + +/// Get the max address in a network. +/// +/// - In IPv4 this is the broadcast address. +/// - In IPv6 this is just the last address in the network. +pub fn max_address(network: &IpNetwork) -> IpAddr { + match network { + IpNetwork::V4(network) => { + let addr = network.ip().to_bits(); + let mask = network.mask().to_bits(); + + IpAddr::V4(Ipv4Addr::from(addr | !mask)) + } + IpNetwork::V6(network) => { + let addr = network.ip().to_bits(); + let mask = network.mask().to_bits(); + + IpAddr::V6(Ipv6Addr::from(addr | !mask)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_max_address_ipv4_24() { + let network = IpNetwork::V4(Ipv4Network::from_str("192.168.1.0/24").unwrap()); + let max = max_address(&network); + assert_eq!(max, IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255))); + } + + #[test] + fn test_max_address_ipv4_16() { + let network = IpNetwork::V4(Ipv4Network::from_str("10.1.0.0/16").unwrap()); + let max = max_address(&network); + assert_eq!(max, IpAddr::V4(Ipv4Addr::new(10, 1, 255, 255))); + } + + #[test] + fn test_max_address_ipv4_8() { + let network = IpNetwork::V4(Ipv4Network::from_str("172.16.0.0/8").unwrap()); + let max = max_address(&network); + assert_eq!(max, IpAddr::V4(Ipv4Addr::new(172, 255, 255, 255))); + } + + #[test] + fn test_max_address_ipv4_32() { + let network = IpNetwork::V4(Ipv4Network::from_str("192.168.1.1/32").unwrap()); + let max = max_address(&network); + assert_eq!(max, IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))); + } + + #[test] + fn test_max_address_ipv6_64() { + let network = IpNetwork::V6(Ipv6Network::from_str("2001:db8::/64").unwrap()); + let max = max_address(&network); + assert_eq!( + max, + IpAddr::V6(Ipv6Addr::from_str("2001:db8::ffff:ffff:ffff:ffff").unwrap()) + ); + } + + #[test] + fn test_max_address_ipv6_128() { + let network = IpNetwork::V6(Ipv6Network::from_str("2001:db8::1/128").unwrap()); + let max = max_address(&network); + assert_eq!(max, IpAddr::V6(Ipv6Addr::from_str("2001:db8::1").unwrap())); + } + + #[test] + fn test_max_address_ipv6_48() { + let network = IpNetwork::V6(Ipv6Network::from_str("2001:db8:1234::/48").unwrap()); + let max = max_address(&network); + assert_eq!( + max, + IpAddr::V6(Ipv6Addr::from_str("2001:db8:1234:ffff:ffff:ffff:ffff:ffff").unwrap()) + ); + } +} diff --git a/src/enterprise/firewall/nftables/mod.rs b/src/enterprise/firewall/nftables/mod.rs index d5e5574b..614b9831 100644 --- a/src/enterprise/firewall/nftables/mod.rs +++ b/src/enterprise/firewall/nftables/mod.rs @@ -1,6 +1,9 @@ pub mod netfilter; -use std::sync::atomic::{AtomicU32, Ordering}; +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + sync::atomic::{AtomicU32, Ordering}, +}; use netfilter::{ allow_established_traffic, apply_filter_rules, drop_table, ignore_unrelated_traffic, @@ -10,8 +13,10 @@ use nftnl::Batch; use super::{ api::{FirewallApi, FirewallManagementApi}, + iprange::IpAddrRangeError, Address, FirewallError, FirewallRule, Policy, Port, Protocol, }; +use crate::enterprise::firewall::iprange::IpAddrRange; static SET_ID_COUNTER: AtomicU32 = AtomicU32::new(0); @@ -51,29 +56,110 @@ struct FilterRule<'a> { negated_iifname: bool, } +/// Merges any contiguous subets or addres ranges into an address range. +/// +/// This reflects the way `nft` CLI handles such cases. +/// Otherwise first address in any subnet after the first is not matched. +/// For example if we use `172.30.0.2/31, 172.30.0.4/31` as `saddr` in a rule, +/// then 172.30.0.4 will not be matched. +fn merge_addrs(addrs: Vec
) -> Result, IpAddrRangeError> { + debug!("Merging any contiguous subnets and ranges found within address list: {addrs:?}"); + + if addrs.is_empty() { + debug!("No addresses provided, returning empty vector."); + return Ok(Vec::new()); + } + + let mut merged_addrs = Vec::new(); + let mut current_address = None; + + // we can assume addresses coming from the core + // are already sorted and non-overlapping + for next_address in addrs { + match ¤t_address { + None => { + debug!("Initializing current address with: {next_address:?}"); + current_address = Some(next_address); + } + Some(previous_address) => { + let previous_range_start = previous_address.first(); + let previous_range_end = previous_address.last(); + let next_ip = next_ip(previous_range_end); + + let next_range_start = next_address.first(); + let next_range_end = next_address.last(); + + // check if range is adjacent to current address + if next_range_start == next_ip { + // replace current address with a combined range + debug!("Merging {next_address:?} with {current_address:?}"); + current_address = Some(Address::Range(IpAddrRange::new( + previous_range_start, + next_range_end, + )?)); + } else { + // push previous address to result and replace with next address + merged_addrs.push(previous_address.clone()); + current_address = Some(next_address); + }; + } + } + } + + // push last remaining address to results + if let Some(address) = current_address { + debug!("Pushing last remaining address into results: {address:?}"); + merged_addrs.push(address) + } + + debug!("Prepared addresses: {merged_addrs:?}"); + + Ok(merged_addrs) +} + +/// Returns the next IP address in sequence, handling overflow via wrapping +fn next_ip(ip: IpAddr) -> IpAddr { + match ip { + IpAddr::V4(ipv4) => { + let ip_u32 = ipv4.to_bits(); + let next_ip_u32 = ip_u32.wrapping_add(1); + IpAddr::V4(Ipv4Addr::from(next_ip_u32)) + } + IpAddr::V6(ipv6) => { + let ip_u128 = ipv6.to_bits(); + let next_ip_u128 = ip_u128.wrapping_add(1); + IpAddr::V6(Ipv6Addr::from(next_ip_u128)) + } + } +} + impl FirewallApi { fn add_rule(&mut self, rule: FirewallRule) -> Result<(), FirewallError> { debug!("Applying the following Defguard ACL rule: {rule:?}"); - let mut rules = Vec::new(); let batch = if let Some(ref mut batch) = self.batch { batch } else { return Err(FirewallError::TransactionNotStarted); }; + let mut filter_rules = Vec::new(); debug!( "The rule will be split into multiple nftables rules based on the specified \ destination ports and protocols." ); + + let source_addrs = merge_addrs(rule.source_addrs)?; + let dest_addrs = merge_addrs(rule.destination_addrs)?; + if rule.destination_ports.is_empty() { debug!( "No destination ports specified, applying single aggregate nftables rule for \ every protocol." ); let rule = FilterRule { - src_ips: &rule.source_addrs, - dest_ips: &rule.destination_addrs, - protocols: rule.protocols, + src_ips: &source_addrs, + dest_ips: &dest_addrs, + protocols: rule.protocols.clone(), action: rule.verdict, counter: true, defguard_rule_id: rule.id, @@ -81,47 +167,35 @@ impl FirewallApi { comment: rule.comment.clone(), ..Default::default() }; - rules.push(rule); + filter_rules.push(rule); } else if !rule.protocols.is_empty() { debug!( "Destination ports and protocols specified, applying individual nftables rules \ for each protocol." ); - for protocol in rule.protocols { + for protocol in rule.protocols.clone() { debug!("Applying rule for protocol: {protocol:?}"); + let mut filter_rule = FilterRule { + src_ips: &source_addrs, + dest_ips: &dest_addrs, + protocols: vec![protocol], + action: rule.verdict, + counter: true, + defguard_rule_id: rule.id, + v4: rule.ipv4, + comment: rule.comment.clone(), + ..Default::default() + }; if protocol.supports_ports() { debug!("Protocol supports ports, rule."); - let rule = FilterRule { - src_ips: &rule.source_addrs, - dest_ips: &rule.destination_addrs, - dest_ports: &rule.destination_ports, - protocols: vec![protocol], - action: rule.verdict, - counter: true, - defguard_rule_id: rule.id, - v4: rule.ipv4, - comment: rule.comment.clone(), - ..Default::default() - }; - rules.push(rule); + filter_rule.dest_ports = &rule.destination_ports; } else { debug!( "Protocol does not support ports, applying nftables rule and ignoring \ destination ports." ); - let rule = FilterRule { - src_ips: &rule.source_addrs, - dest_ips: &rule.destination_addrs, - protocols: vec![protocol], - action: rule.verdict, - counter: true, - defguard_rule_id: rule.id, - v4: rule.ipv4, - comment: rule.comment.clone(), - ..Default::default() - }; - rules.push(rule); } + filter_rules.push(filter_rule); } } else { debug!( @@ -131,8 +205,8 @@ impl FirewallApi { for protocol in [Protocol::Tcp, Protocol::Udp] { debug!("Applying nftables rule for protocol: {protocol:?}"); let rule = FilterRule { - src_ips: &rule.source_addrs, - dest_ips: &rule.destination_addrs, + src_ips: &source_addrs, + dest_ips: &dest_addrs, dest_ports: &rule.destination_ports, protocols: vec![protocol], action: rule.verdict, @@ -142,11 +216,11 @@ impl FirewallApi { comment: rule.comment.clone(), ..Default::default() }; - rules.push(rule); + filter_rules.push(rule); } } - apply_filter_rules(rules, batch, &self.ifname)?; + apply_filter_rules(filter_rules, batch, &self.ifname)?; debug!( "Applied firewall rules for Defguard ACL rule ID: {}", @@ -257,3 +331,265 @@ impl FirewallManagementApi for FirewallApi { } } } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use ipnetwork::IpNetwork; + + use super::*; + + #[test] + fn test_sorting() { + let mut addrs = vec![ + Address::Network(IpNetwork::from_str("10.10.10.11/24").unwrap()), + Address::Network(IpNetwork::from_str("10.10.10.12/24").unwrap()), + Address::Network(IpNetwork::from_str("10.10.11.10/32").unwrap()), + Address::Network(IpNetwork::from_str("10.10.11.11/32").unwrap()), + Address::Network(IpNetwork::from_str("10.10.10.10/24").unwrap()), + Address::Network(IpNetwork::from_str("10.10.11.12/32").unwrap()), + ]; + + addrs.sort_by(|a, b| { + a.first() + .partial_cmp(&b.first()) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + assert_eq!( + addrs, + vec![ + Address::Network(IpNetwork::from_str("10.10.10.10/24").unwrap()), + Address::Network(IpNetwork::from_str("10.10.10.11/24").unwrap()), + Address::Network(IpNetwork::from_str("10.10.10.12/24").unwrap()), + Address::Network(IpNetwork::from_str("10.10.11.10/32").unwrap()), + Address::Network(IpNetwork::from_str("10.10.11.11/32").unwrap()), + Address::Network(IpNetwork::from_str("10.10.11.12/32").unwrap()), + ] + ); + + let _prepared_addrs = merge_addrs(addrs).unwrap(); + } + + #[test] + fn test_merge_addrs_empty() { + let addrs: Vec
= vec![]; + let result = merge_addrs(addrs).unwrap(); + assert!(result.is_empty()); + } + + #[test] + fn test_merge_addrs_single_address() { + let addrs = vec![Address::Network( + IpNetwork::from_str("192.168.1.10/32").unwrap(), + )]; + let result = merge_addrs(addrs.clone()).unwrap(); + + assert_eq!(result, addrs); + } + + #[test] + fn test_merge_addrs_adjacent_ranges() { + let addrs = vec![ + Address::Range( + IpAddrRange::new( + IpAddr::from_str("192.168.1.10").unwrap(), + IpAddr::from_str("192.168.1.20").unwrap(), + ) + .unwrap(), + ), + Address::Range( + IpAddrRange::new( + IpAddr::from_str("192.168.1.21").unwrap(), + IpAddr::from_str("192.168.1.30").unwrap(), + ) + .unwrap(), + ), + ]; + let result = merge_addrs(addrs).unwrap(); + + assert_eq!(result.len(), 1); + if let Address::Range(range) = &result[0] { + assert_eq!(range.start(), IpAddr::from_str("192.168.1.10").unwrap()); + assert_eq!(range.end(), IpAddr::from_str("192.168.1.30").unwrap()); + } else { + panic!("Expected Address::Range"); + } + } + + #[test] + fn test_merge_addrs_adjacent_single_addresses() { + let addrs = vec![ + Address::Network(IpNetwork::from_str("192.168.1.10/32").unwrap()), + Address::Network(IpNetwork::from_str("192.168.1.11/32").unwrap()), + Address::Network(IpNetwork::from_str("192.168.1.12/32").unwrap()), + ]; + let result = merge_addrs(addrs).unwrap(); + + assert_eq!(result.len(), 1); + if let Address::Range(range) = &result[0] { + assert_eq!(range.start(), IpAddr::from_str("192.168.1.10").unwrap()); + assert_eq!(range.end(), IpAddr::from_str("192.168.1.12").unwrap()); + } else { + panic!("Expected Address::Range"); + } + } + + #[test] + fn test_merge_addrs_non_adjacent_ranges() { + let addrs = vec![ + Address::Range( + IpAddrRange::new( + IpAddr::from_str("192.168.1.10").unwrap(), + IpAddr::from_str("192.168.1.20").unwrap(), + ) + .unwrap(), + ), + Address::Range( + IpAddrRange::new( + IpAddr::from_str("192.168.1.30").unwrap(), + IpAddr::from_str("192.168.1.40").unwrap(), + ) + .unwrap(), + ), + ]; + let result = merge_addrs(addrs).unwrap(); + + assert_eq!(result.len(), 2); + if let Address::Range(range1) = &result[0] { + assert_eq!(range1.start(), IpAddr::from_str("192.168.1.10").unwrap()); + assert_eq!(range1.end(), IpAddr::from_str("192.168.1.20").unwrap()); + } else { + panic!("Expected Address::Range"); + } + if let Address::Range(range2) = &result[1] { + assert_eq!(range2.start(), IpAddr::from_str("192.168.1.30").unwrap()); + assert_eq!(range2.end(), IpAddr::from_str("192.168.1.40").unwrap()); + } else { + panic!("Expected Address::Range"); + } + } + + #[test] + fn test_merge_addrs_mixed_networks_and_ranges() { + let addrs = vec![ + Address::Network(IpNetwork::from_str("192.168.1.10/32").unwrap()), + Address::Range( + IpAddrRange::new( + IpAddr::from_str("192.168.1.11").unwrap(), + IpAddr::from_str("192.168.1.15").unwrap(), + ) + .unwrap(), + ), + Address::Network(IpNetwork::from_str("192.168.1.16/32").unwrap()), + ]; + let result = merge_addrs(addrs).unwrap(); + + assert_eq!(result.len(), 1); + if let Address::Range(range) = &result[0] { + assert_eq!(range.start(), IpAddr::from_str("192.168.1.10").unwrap()); + assert_eq!(range.end(), IpAddr::from_str("192.168.1.16").unwrap()); + } else { + panic!("Expected Address::Range"); + } + } + + #[test] + fn test_merge_addrs_non_adjacent_singles() { + let addrs = vec![ + Address::Network(IpNetwork::from_str("192.168.1.10/32").unwrap()), + Address::Network(IpNetwork::from_str("192.168.1.11/32").unwrap()), + Address::Network(IpNetwork::from_str("192.168.1.15/32").unwrap()), + Address::Network(IpNetwork::from_str("192.168.1.20/32").unwrap()), + ]; + let result = merge_addrs(addrs).unwrap(); + + // These should result in 3 separate ranges: 10-11, 15, 20 + let expected_addrs = vec![ + Address::Range( + IpAddrRange::new( + IpAddr::from_str("192.168.1.10").unwrap(), + IpAddr::from_str("192.168.1.11").unwrap(), + ) + .unwrap(), + ), + Address::Network(IpNetwork::from_str("192.168.1.15/32").unwrap()), + Address::Network(IpNetwork::from_str("192.168.1.20/32").unwrap()), + ]; + assert_eq!(result, expected_addrs); + } + + #[test] + fn test_merge_addrs_ipv6() { + let addrs = vec![ + Address::Network(IpNetwork::from_str("2001:db8::1/128").unwrap()), + Address::Network(IpNetwork::from_str("2001:db8::2/128").unwrap()), + Address::Network(IpNetwork::from_str("2001:db8::3/128").unwrap()), + ]; + let result = merge_addrs(addrs).unwrap(); + + assert_eq!(result.len(), 1); + if let Address::Range(range) = &result[0] { + assert_eq!(range.start(), IpAddr::from_str("2001:db8::1").unwrap()); + assert_eq!(range.end(), IpAddr::from_str("2001:db8::3").unwrap()); + } else { + panic!("Expected Address::Range"); + } + } + + #[test] + fn test_next_ip_ipv4() { + assert_eq!( + next_ip(IpAddr::from_str("192.168.1.10").unwrap()), + IpAddr::from_str("192.168.1.11").unwrap() + ); + + // Test overflow + assert_eq!( + next_ip(IpAddr::from_str("255.255.255.255").unwrap()), + IpAddr::from_str("0.0.0.0").unwrap() + ); + } + + #[test] + fn test_next_ip_ipv6() { + assert_eq!( + next_ip(IpAddr::from_str("2001:db8::1").unwrap()), + IpAddr::from_str("2001:db8::2").unwrap() + ); + + // Test overflow + assert_eq!( + next_ip(IpAddr::from_str("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff").unwrap()), + IpAddr::from_str("::").unwrap() + ); + } + + #[test] + fn test_merge_addrs_gap() { + let addrs = vec![ + Address::Network(IpNetwork::from_str("192.168.1.1/32").unwrap()), + Address::Network(IpNetwork::from_str("192.168.1.100/32").unwrap()), + ]; + let result = merge_addrs(addrs.clone()).unwrap(); + + // Should not merge since there's a gap + assert_eq!(result, addrs); + + let addrs = vec![ + Address::Range( + IpAddrRange::new( + IpAddr::from_str("192.168.1.10").unwrap(), + IpAddr::from_str("192.168.1.20").unwrap(), + ) + .unwrap(), + ), + Address::Network(IpNetwork::from_str("192.168.1.100/32").unwrap()), + ]; + let result = merge_addrs(addrs.clone()).unwrap(); + + // Should not merge since there's a gap + assert_eq!(result, addrs); + } +} diff --git a/src/enterprise/firewall/nftables/netfilter.rs b/src/enterprise/firewall/nftables/netfilter.rs index 7174d9b0..b5d8505c 100644 --- a/src/enterprise/firewall/nftables/netfilter.rs +++ b/src/enterprise/firewall/nftables/netfilter.rs @@ -1,13 +1,8 @@ -#[cfg(test)] -use std::str::FromStr; use std::{ ffi::{CStr, CString}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, }; -use ipnetwork::IpNetwork; -#[cfg(test)] -use ipnetwork::{Ipv4Network, Ipv6Network}; use nftnl::{ expr::{Expression, InterfaceName}, nft_expr, nftnl_sys, @@ -16,7 +11,7 @@ use nftnl::{ }; use super::{get_set_id, Address, FilterRule, Policy, Port, Protocol, State}; -use crate::enterprise::firewall::{iprange::IpAddrRange, FirewallError}; +use crate::enterprise::firewall::{iprange::IpAddrRange, max_address, FirewallError}; const FILTER_TABLE: &str = "filter"; const NAT_TABLE: &str = "nat"; @@ -809,27 +804,6 @@ fn socket_recv<'a>( } } -/// Get the max address in a network. -/// -/// - In IPv4 this is the broadcast address. -/// - In IPv6 this is just the last address in the network. -fn max_address(network: &IpNetwork) -> IpAddr { - match network { - IpNetwork::V4(network) => { - let addr = network.ip().to_bits(); - let mask = network.mask().to_bits(); - - IpAddr::V4(Ipv4Addr::from(addr | !mask)) - } - IpNetwork::V6(network) => { - let addr = network.ip().to_bits(); - let mask = network.mask().to_bits(); - - IpAddr::V6(Ipv6Addr::from(addr | !mask)) - } - } -} - fn new_anon_set( table: &Table, family: ProtoFamily, @@ -991,59 +965,4 @@ mod tests { increment_bytes(&mut ip); assert_eq!(ip, [0, 0, 0, 0, 0, 0, 0, 0]); } - - #[test] - fn test_max_address_ipv4_24() { - let network = IpNetwork::V4(Ipv4Network::from_str("192.168.1.0/24").unwrap()); - let max = max_address(&network); - assert_eq!(max, IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255))); - } - - #[test] - fn test_max_address_ipv4_16() { - let network = IpNetwork::V4(Ipv4Network::from_str("10.1.0.0/16").unwrap()); - let max = max_address(&network); - assert_eq!(max, IpAddr::V4(Ipv4Addr::new(10, 1, 255, 255))); - } - - #[test] - fn test_max_address_ipv4_8() { - let network = IpNetwork::V4(Ipv4Network::from_str("172.16.0.0/8").unwrap()); - let max = max_address(&network); - assert_eq!(max, IpAddr::V4(Ipv4Addr::new(172, 255, 255, 255))); - } - - #[test] - fn test_max_address_ipv4_32() { - let network = IpNetwork::V4(Ipv4Network::from_str("192.168.1.1/32").unwrap()); - let max = max_address(&network); - assert_eq!(max, IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))); - } - - #[test] - fn test_max_address_ipv6_64() { - let network = IpNetwork::V6(Ipv6Network::from_str("2001:db8::/64").unwrap()); - let max = max_address(&network); - assert_eq!( - max, - IpAddr::V6(Ipv6Addr::from_str("2001:db8::ffff:ffff:ffff:ffff").unwrap()) - ); - } - - #[test] - fn test_max_address_ipv6_128() { - let network = IpNetwork::V6(Ipv6Network::from_str("2001:db8::1/128").unwrap()); - let max = max_address(&network); - assert_eq!(max, IpAddr::V6(Ipv6Addr::from_str("2001:db8::1").unwrap())); - } - - #[test] - fn test_max_address_ipv6_48() { - let network = IpNetwork::V6(Ipv6Network::from_str("2001:db8:1234::/48").unwrap()); - let max = max_address(&network); - assert_eq!( - max, - IpAddr::V6(Ipv6Addr::from_str("2001:db8:1234:ffff:ffff:ffff:ffff:ffff").unwrap()) - ); - } }