From 5b2e6750109ee2ed3bddae5afb82c5de523068ae Mon Sep 17 00:00:00 2001 From: Hasan Date: Fri, 27 Sep 2024 16:02:41 +0200 Subject: [PATCH] Modify firewall filtering logic Firewall was only filtering based on incoming connections. This change enables to have more fine grained control over packet filtering. For eg, to allow only routing eariler user had to allow incoming connections as well, and this change will enable it to only turn on routing to allow routing. --- .unreleased/LLT-4749 | 1 + Cargo.lock | 5 + Cargo.toml | 1 + crates/telio-firewall/Cargo.toml | 7 +- .../telio-firewall/benches/firewall_bench.rs | 163 ++- crates/telio-firewall/src/firewall.rs | 1114 ++++++++++++----- crates/telio-model/src/config.rs | 10 + crates/telio-model/src/event.rs | 4 + crates/telio-model/src/features.rs | 8 +- crates/telio-model/src/mesh.rs | 6 + crates/telio-utils/src/lru_cache.rs | 12 +- crates/telio-wg/src/wg.rs | 3 +- nat-lab/tests/mesh_api.py | 18 +- nat-lab/tests/test_dns_through_exit.py | 16 +- nat-lab/tests/test_events.py | 5 + nat-lab/tests/test_features_builder.py | 29 +- nat-lab/tests/test_lana.py | 16 +- nat-lab/tests/test_mesh_api.py | 2 + nat-lab/tests/test_mesh_exit_through_peer.py | 27 +- nat-lab/tests/test_mesh_firewall.py | 12 +- nat-lab/tests/test_vpn.py | 6 +- nat-lab/tests/utils/bindings.py | 11 +- src/device.rs | 15 +- src/device/wg_controller.rs | 234 +++- src/ffi/defaults_builder.rs | 24 +- src/lib.rs | 17 +- src/libtelio.udl | 15 +- 27 files changed, 1363 insertions(+), 418 deletions(-) create mode 100644 .unreleased/LLT-4749 diff --git a/.unreleased/LLT-4749 b/.unreleased/LLT-4749 new file mode 100644 index 000000000..228e32168 --- /dev/null +++ b/.unreleased/LLT-4749 @@ -0,0 +1 @@ +Modify incoming firewall handler \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9b6bb2f17..2541fd8b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4654,7 +4654,10 @@ name = "telio-firewall" version = "0.1.0" dependencies = [ "criterion", + "enum-map", "hashlink", + "if-addrs", + "ipnet", "mockall", "pnet_packet", "proptest", @@ -4663,6 +4666,8 @@ dependencies = [ "rustc-hash 1.1.0", "sn_fake_clock", "telio-crypto", + "telio-model", + "telio-network-monitors", "telio-utils", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index ca226d126..23fdf6242 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,6 +89,7 @@ telio-proxy = { workspace = true, features = ["mockall"] } telio-pq = { workspace = true, features = ["mockall"] } telio-test.workspace = true telio-traversal = { workspace = true, features = ["mockall"] } +telio-utils.workspace = true telio-wg = { workspace = true, features = ["mockall", "test-adapter"] } [build-dependencies] diff --git a/crates/telio-firewall/Cargo.toml b/crates/telio-firewall/Cargo.toml index c3a100ba7..1e6331aa2 100644 --- a/crates/telio-firewall/Cargo.toml +++ b/crates/telio-firewall/Cargo.toml @@ -10,7 +10,10 @@ publish = false test_utils = [] # For use in benchmarks to avoid duplication of the 'setup' code needed to create buffers with valid packets [dependencies] +enum-map = "2.7.3" hashlink.workspace = true +if-addrs.workspace = true +ipnet.workspace = true tracing.workspace = true mockall = { workspace = true, optional = true } pnet_packet.workspace = true @@ -18,6 +21,8 @@ rustc-hash.workspace = true telio-crypto.workspace = true telio-utils.workspace = true +telio-model.workspace = true +telio-network-monitors.workspace = true [dev-dependencies] criterion = "0.5.1" @@ -28,8 +33,6 @@ proptest-derive.workspace = true rand.workspace = true sn_fake_clock.workspace = true -telio-utils = { workspace = true, features = ["sn_fake_clock"] } - [[bench]] name = "firewall_bench" harness = false diff --git a/crates/telio-firewall/benches/firewall_bench.rs b/crates/telio-firewall/benches/firewall_bench.rs index e16969f6d..d61d3115a 100644 --- a/crates/telio-firewall/benches/firewall_bench.rs +++ b/crates/telio-firewall/benches/firewall_bench.rs @@ -1,5 +1,5 @@ use std::fmt::{Debug, Display, Formatter}; -use std::net::SocketAddr; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::ops::Deref; use std::rc::Rc; @@ -7,7 +7,8 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use pnet_packet::tcp::TcpFlags; use telio_crypto::SecretKey; use telio_firewall::firewall::tests::{make_tcp, make_tcp6, make_udp, make_udp6}; -use telio_firewall::firewall::{Firewall, StatefullFirewall}; +use telio_firewall::firewall::{Firewall, Permissions, StatefullFirewall}; +use telio_model::features::FeatureFirewall; const PEER_COUNTS: [usize; 8] = [0, 1, 2, 4, 8, 16, 32, 64]; // max 60 peers: https://meshnet.nordvpn.com/getting-started/meshnet-explained#meshnet-scalability const PACKET_COUNT: u64 = 100_000; @@ -90,10 +91,17 @@ pub fn firewall_tcp_inbound_benchmarks(c: &mut Criterion) { BenchmarkId::from_parameter(parameter.clone()), ¶meter, |b, param| { - let firewall = StatefullFirewall::new(true, false); + let firewall = StatefullFirewall::new(true, FeatureFirewall::default()); + firewall.set_ip_addresses(vec![ + (IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); for _ in 0..param.peers { let public_key = SecretKey::gen().public(); - firewall.add_to_peer_whitelist(public_key); + firewall.add_to_peer_whitelist( + public_key, + Permissions::IncomingConnections, + ); firewall.add_to_port_whitelist(public_key, 42); } let other_peer_pk1 = SecretKey::gen().public(); @@ -127,11 +135,18 @@ pub fn firewall_tcp_inbound_benchmarks(c: &mut Criterion) { BenchmarkId::from_parameter(parameter.clone()), ¶meter, |b, param| { - let firewall = StatefullFirewall::new(true, false); + let firewall = StatefullFirewall::new(true, FeatureFirewall::default()); + firewall.set_ip_addresses(vec![ + (IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); let mut peers = vec![]; for _ in 0..param.peers { let public_key = SecretKey::gen().public(); - firewall.add_to_peer_whitelist(public_key); + firewall.add_to_peer_whitelist( + public_key, + Permissions::IncomingConnections, + ); peers.push(public_key.0); } let mut which_peer = 0usize; @@ -162,7 +177,11 @@ pub fn firewall_tcp_inbound_benchmarks(c: &mut Criterion) { BenchmarkId::from_parameter(parameter.clone()), ¶meter, |b, param| { - let firewall = StatefullFirewall::new(true, false); + let firewall = StatefullFirewall::new(true, FeatureFirewall::default()); + firewall.set_ip_addresses(vec![ + (IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); let mut peers_and_packets = vec![]; let port_base = 1111; for i in 0..param.peers { @@ -191,6 +210,42 @@ pub fn firewall_tcp_inbound_benchmarks(c: &mut Criterion) { } } } + + { + let mut group = c.benchmark_group("process inbound tcp from vpn peer packet - accepted"); + for packet in &packets { + for peers in PEER_COUNTS[1..].iter().copied() { + group.throughput(criterion::Throughput::Elements(PACKET_COUNT)); + let parameter = Parameter { + peers, + packet: packet.clone(), + }; + group.bench_with_input( + BenchmarkId::from_parameter(parameter.clone()), + ¶meter, + |b, param| { + let firewall = StatefullFirewall::new(true, FeatureFirewall::default()); + firewall.set_ip_addresses(vec![ + (IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); + + let public_key = SecretKey::gen().public(); + firewall.add_vpn_peer(public_key); + + b.iter(|| { + for _ in 0..PACKET_COUNT { + assert!( + firewall.process_inbound_packet(&public_key.0, ¶m.packet) + ); + } + assert_eq!((0, 0), firewall.get_state()); + }); + }, + ); + } + } + } } pub fn firewall_tcp_outbound_benchmarks(c: &mut Criterion) { @@ -215,10 +270,13 @@ pub fn firewall_tcp_outbound_benchmarks(c: &mut Criterion) { BenchmarkId::from_parameter(parameter.clone()), ¶meter, |b, param| { - let firewall = StatefullFirewall::new(true, false); + let firewall = StatefullFirewall::new(true, FeatureFirewall::default()); for _ in 0..param.peers { let public_key = SecretKey::gen().public(); - firewall.add_to_peer_whitelist(public_key); + firewall.add_to_peer_whitelist( + public_key, + Permissions::IncomingConnections, + ); firewall.add_to_port_whitelist(public_key, 42); } let peer_pk1 = SecretKey::gen().public(); @@ -257,10 +315,13 @@ pub fn firewall_tcp_outbound_benchmarks(c: &mut Criterion) { BenchmarkId::from_parameter(parameter.clone()), ¶meter, |b, param| { - let firewall = StatefullFirewall::new(true, false); + let firewall = StatefullFirewall::new(true, FeatureFirewall::default()); for _ in 0..param.peers { let public_key = SecretKey::gen().public(); - firewall.add_to_peer_whitelist(public_key); + firewall.add_to_peer_whitelist( + public_key, + Permissions::IncomingConnections, + ); } let other_peer_pk1 = SecretKey::gen().public(); let other_peer_pk2 = SecretKey::gen().public(); @@ -292,11 +353,14 @@ pub fn firewall_tcp_outbound_benchmarks(c: &mut Criterion) { BenchmarkId::from_parameter(parameter.clone()), ¶meter, |b, param| { - let firewall = StatefullFirewall::new(true, false); + let firewall = StatefullFirewall::new(true, FeatureFirewall::default()); let mut peers = vec![]; for _ in 0..param.peers { let public_key = SecretKey::gen().public(); - firewall.add_to_peer_whitelist(public_key); + firewall.add_to_peer_whitelist( + public_key, + Permissions::IncomingConnections, + ); peers.push(public_key.0); } let mut which_peer = 0usize; @@ -332,10 +396,17 @@ pub fn firewall_udp_inbound_benchmarks(c: &mut Criterion) { BenchmarkId::from_parameter(parameter.clone()), ¶meter, |b, param| { - let firewall = StatefullFirewall::new(true, false); + let firewall = StatefullFirewall::new(true, FeatureFirewall::default()); + firewall.set_ip_addresses(vec![ + (IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); for _ in 0..param.peers { let public_key = SecretKey::gen().public(); - firewall.add_to_peer_whitelist(public_key); + firewall.add_to_peer_whitelist( + public_key, + Permissions::IncomingConnections, + ); firewall.add_to_port_whitelist(public_key, 42); } let other_peer_pk1 = SecretKey::gen().public(); @@ -369,11 +440,18 @@ pub fn firewall_udp_inbound_benchmarks(c: &mut Criterion) { BenchmarkId::from_parameter(parameter.clone()), ¶meter, |b, param| { - let firewall = StatefullFirewall::new(true, false); + let firewall = StatefullFirewall::new(true, FeatureFirewall::default()); + firewall.set_ip_addresses(vec![ + (IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); let mut peers = vec![]; for _ in 0..param.peers { let public_key = SecretKey::gen().public(); - firewall.add_to_peer_whitelist(public_key); + firewall.add_to_peer_whitelist( + public_key, + Permissions::IncomingConnections, + ); peers.push(public_key.0); } let mut which_peer = 0usize; @@ -404,7 +482,11 @@ pub fn firewall_udp_inbound_benchmarks(c: &mut Criterion) { BenchmarkId::from_parameter(parameter.clone()), ¶meter, |b, param| { - let firewall = StatefullFirewall::new(true, false); + let firewall = StatefullFirewall::new(true, FeatureFirewall::default()); + firewall.set_ip_addresses(vec![ + (IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); let mut peers_and_packets = vec![]; let port_base = 42; for i in 0..param.peers { @@ -433,6 +515,42 @@ pub fn firewall_udp_inbound_benchmarks(c: &mut Criterion) { } } } + + { + let mut group = c.benchmark_group("process inbound udp from vpn peer packet - accepted"); + for packet in &packets { + for peers in PEER_COUNTS[1..].iter().copied() { + group.throughput(criterion::Throughput::Elements(PACKET_COUNT)); + let parameter = Parameter { + peers, + packet: packet.clone(), + }; + group.bench_with_input( + BenchmarkId::from_parameter(parameter.clone()), + ¶meter, + |b, param| { + let firewall = StatefullFirewall::new(true, FeatureFirewall::default()); + firewall.set_ip_addresses(vec![ + (IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); + + let public_key = SecretKey::gen().public(); + firewall.add_vpn_peer(public_key); + + b.iter(|| { + for _ in 0..PACKET_COUNT { + assert!( + firewall.process_inbound_packet(&public_key.0, ¶m.packet) + ); + } + assert_eq!((0, 0), firewall.get_state()); + }); + }, + ); + } + } + } } pub fn firewall_udp_outbound_benchmarks(c: &mut Criterion) { @@ -453,11 +571,14 @@ pub fn firewall_udp_outbound_benchmarks(c: &mut Criterion) { BenchmarkId::from_parameter(parameter.clone()), ¶meter, |b, param| { - let firewall = StatefullFirewall::new(true, false); + let firewall = StatefullFirewall::new(true, FeatureFirewall::default()); let mut peers = vec![]; for _ in 0..param.peers { let public_key = SecretKey::gen().public(); - firewall.add_to_peer_whitelist(public_key); + firewall.add_to_peer_whitelist( + public_key, + Permissions::IncomingConnections, + ); peers.push(public_key.0); } let mut which_peer = 0usize; @@ -487,7 +608,7 @@ pub fn firewall_udp_outbound_benchmarks(c: &mut Criterion) { BenchmarkId::from_parameter(parameter.clone()), ¶meter, |b, param| { - let firewall = StatefullFirewall::new(true, false); + let firewall = StatefullFirewall::new(true, FeatureFirewall::default()); let mut peers = vec![]; for _ in 0..param.peers { let public_key = SecretKey::gen().public(); diff --git a/crates/telio-firewall/src/firewall.rs b/crates/telio-firewall/src/firewall.rs index 7d990299d..671818768 100644 --- a/crates/telio-firewall/src/firewall.rs +++ b/crates/telio-firewall/src/firewall.rs @@ -1,6 +1,9 @@ //! Telio firewall component used to track open connections //! with other devices§ +use core::fmt; +use enum_map::{Enum, EnumMap}; +use ipnet::Ipv4Net; use pnet_packet::{ icmp::{ destination_unreachable::IcmpCodes, IcmpPacket, IcmpType, IcmpTypes, MutableIcmpPacket, @@ -18,9 +21,12 @@ use std::{ fmt::{Debug, Formatter}, io, net::{IpAddr as StdIpAddr, Ipv4Addr as StdIpv4Addr, Ipv6Addr as StdIpv6Addr}, - sync::{Mutex, RwLock}, + sync::{Mutex, RwLock, RwLockReadGuard}, time::Duration, }; + +use telio_model::features::FeatureFirewall; +use telio_network_monitors::monitor::LOCAL_ADDRS_CACHE; use telio_utils::{ lru_cache::{Entry, LruCache}, telio_log_error, @@ -177,16 +183,22 @@ pub trait Firewall { fn get_port_whitelist(&self) -> HashMap; /// Clears the peer whitelist - fn clear_peer_whitelist(&self); + fn clear_peer_whitelists(&self); /// Add peer to whitelist - fn add_to_peer_whitelist(&self, peer: PublicKey); + fn add_to_peer_whitelist(&self, peer: PublicKey, permissions: Permissions); /// Remove peer from whitelist - fn remove_from_peer_whitelist(&self, peer: PublicKey); + fn remove_from_peer_whitelist(&self, peer: PublicKey, permissions: Permissions); /// Returns a whitelist of peers - fn get_peer_whitelist(&self) -> HashSet; + fn get_peer_whitelist(&self, permissions: Permissions) -> HashSet; + + /// Adds vpn peer public key + fn add_vpn_peer(&self, vpn_peer: PublicKey); + + /// Removes vpn peer + fn remove_vpn_peer(&self); /// For new connections it opens a pinhole for incoming connection /// If connection is already cached, it resets its timer and extends its lifetime @@ -208,6 +220,45 @@ pub trait Firewall { sink4: &mut dyn io::Write, sink6: &mut dyn io::Write, ) -> io::Result<()>; + + /// Saves local node Ip address into firewall object + fn set_ip_addresses(&self, ip_addrs: Vec); +} + +/// Possible permissions of the peer +#[derive(Clone, Copy, Enum, Debug, PartialEq)] +pub enum Permissions { + /// Permission to allow incoming connections + IncomingConnections, + /// Permission for peer to access local network + LocalAreaConnections, + /// Permission for peer to route traffic + RoutingConnections, +} + +impl Permissions { + /// Array of permissions to iterate over + pub const VALUES: [Self; 3] = [ + Self::IncomingConnections, + Self::LocalAreaConnections, + Self::RoutingConnections, + ]; +} + +impl fmt::Display for Permissions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Permissions::IncomingConnections => write!(f, "Incoming Connections"), + Permissions::LocalAreaConnections => write!(f, "Local Area Connections"), + Permissions::RoutingConnections => write!(f, "Routing Connections"), + } + } +} + +enum PacketAction { + PassThrough, + Drop, + HandleLocally, } #[derive(Default)] @@ -215,13 +266,15 @@ struct Whitelist { /// List of whitelisted source peer and destination port pairs port_whitelist: HashMap, - /// List of whitelisted peers identified by public key from which any packet will be accepted - peer_whitelist: HashSet, + /// Whitelisted peers of different permissions + peer_whitelists: EnumMap>, + + /// Public key of vpn peer + vpn_peer: Option, } impl Whitelist { fn is_port_whitelisted(&self, peer: &PublicKey, port: u16) -> bool { - debug_assert!(!self.peer_whitelist.contains(peer)); self.port_whitelist .get(peer) .map(|p| *p == port) @@ -232,9 +285,9 @@ impl Whitelist { /// Statefull packet-filter firewall. pub struct StatefullFirewall { /// Recent udp connections - udp: Mutex>, + udp: Mutex>, /// Recent tcp connections - tcp: Mutex>, + tcp: Mutex>, /// Recent icmp connections icmp: Mutex>, /// Whitelist of networks/peers allowed to connect @@ -244,6 +297,10 @@ pub struct StatefullFirewall { /// Wheter to still keep track of whitelisted TCP/UDP connections. /// Used for connection reset mechanism record_whitelisted: bool, + /// Local node ip addresses + ip_addresses: RwLock>, + /// Custom IPv4 range to check against + custom_ip_range: Option, } #[derive(Debug)] @@ -314,19 +371,13 @@ impl Debug for IpConnWithPort { } #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] -struct TcpConn { +struct Connection { link: IpConnWithPort, // This public key refers to source peer for inbound connections and // destination peer for outbound connections pubkey: PublicKey, } -#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] -struct UdpConn { - link: IpConnWithPort, - pubkey: PublicKey, -} - #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] struct IcmpConn { remote_addr: IpAddr, @@ -388,8 +439,8 @@ macro_rules! unwrap_lock_or_return { impl StatefullFirewall { /// Constructs firewall with default timeout (2 mins) and capacity (4096 entries). - pub fn new(use_ipv6: bool, tcp_record_whitelisted: bool) -> Self { - StatefullFirewall::new_custom(LRU_CAPACITY, LRU_TIMEOUT, use_ipv6, tcp_record_whitelisted) + pub fn new(use_ipv6: bool, feature: FeatureFirewall) -> Self { + StatefullFirewall::new_custom(LRU_CAPACITY, LRU_TIMEOUT, use_ipv6, feature) } #[cfg(feature = "test_utils")] @@ -401,7 +452,7 @@ impl StatefullFirewall { } /// Constructs firewall with custom capacity and timeout in ms (for testing only). - fn new_custom(capacity: usize, ttl: u64, use_ipv6: bool, record_whitelisted: bool) -> Self { + fn new_custom(capacity: usize, ttl: u64, use_ipv6: bool, feature: FeatureFirewall) -> Self { let ttl = Duration::from_millis(ttl); Self { tcp: Mutex::new(LruCache::new(ttl, capacity)), @@ -409,7 +460,9 @@ impl StatefullFirewall { icmp: Mutex::new(LruCache::new(ttl, capacity)), whitelist: RwLock::new(Whitelist::default()), allow_ipv6: use_ipv6, - record_whitelisted, + record_whitelisted: feature.boringtun_reset_conns, + ip_addresses: RwLock::new(Vec::::new()), + custom_ip_range: feature.custom_private_ip_range, } } @@ -426,7 +479,8 @@ impl StatefullFirewall { let whitelist = unwrap_lock_or_return!(self.whitelist.read(), false); // If peer is whitelisted - allow immediately - if whitelist.peer_whitelist.contains(&peer) { + #[allow(index_access_check)] + if whitelist.peer_whitelists[Permissions::IncomingConnections].contains(&peer) { telio_log_trace!( "Outbound IP packet is for whitelisted peer, forwarding: {:?}", ip @@ -485,28 +539,26 @@ impl StatefullFirewall { let whitelist = unwrap_lock_or_return!(self.whitelist.read(), false); // Fasttrack, if peer is whitelisted - skip any conntrack and allow immediately - if whitelist.peer_whitelist.contains(&peer) { - telio_log_trace!( - "Inbound IP packet is for whitelisted peer, forwarding: {:?}", - ip - ); - - // We still need to track the state of TCP and UDP connections - if self.record_whitelisted { - let check_port = |_port| true; - - match proto { - IpNextHeaderProtocols::Udp => { - self.handle_inbound_udp(check_port, &peer, &ip); - } - IpNextHeaderProtocols::Tcp => { - self.handle_inbound_tcp(check_port, peer, &ip); + if let Some(vpn_peer) = whitelist.vpn_peer { + if vpn_peer == peer { + telio_log_trace!("Inbound IP packet is for vpn peer, forwarding: {:?}", ip); + // We still need to track the state of TCP and UDP connections + if self.record_whitelisted { + let check_connection_policy = |_pubkey, _port| PacketAction::HandleLocally; + + match proto { + IpNextHeaderProtocols::Udp => { + self.handle_inbound_udp(check_connection_policy, &peer, &ip); + } + IpNextHeaderProtocols::Tcp => { + self.handle_inbound_tcp(check_connection_policy, &peer, &ip); + } + _ => (), } - _ => (), } - } - return true; + return true; + } } if !ip.check_valid() { @@ -514,11 +566,32 @@ impl StatefullFirewall { return false; } - let check_port = |port| whitelist.is_port_whitelisted(&peer, port); + let check_connection_policy = |pubkey, port| self.decide_connection_handling(pubkey, port); + + // Check packet policy first + match self.decide_packet_handling( + peer, + From::::from(ip.get_destination().into()), + whitelist, + ) { + PacketAction::Drop => { + telio_log_trace!("Dropping packet {:?} {:?}", ip.get_source().into(), peer); + return false; + } + PacketAction::HandleLocally => (), + PacketAction::PassThrough => { + telio_log_trace!("Accepting packet {:?} {:?}", ip.get_source().into(), peer); + return true; + } + } match proto { - IpNextHeaderProtocols::Udp => self.handle_inbound_udp(check_port, &peer, &ip), - IpNextHeaderProtocols::Tcp => self.handle_inbound_tcp(check_port, peer, &ip), + IpNextHeaderProtocols::Udp => { + self.handle_inbound_udp(check_connection_policy, &peer, &ip) + } + IpNextHeaderProtocols::Tcp => { + self.handle_inbound_tcp(check_connection_policy, &peer, &ip) + } IpNextHeaderProtocols::Icmp => self.handle_inbound_icmp(peer, &ip), IpNextHeaderProtocols::Icmpv6 if self.allow_ipv6 => self.handle_inbound_icmp(peer, &ip), _ => false, @@ -526,8 +599,8 @@ impl StatefullFirewall { } fn handle_outbound_udp<'a>(&self, pubkey: PublicKey, ip: &impl IpPacket<'a>) { - let link = unwrap_option_or_return!(Self::build_udp_conn_info(ip, false)); - let key = UdpConn { link, pubkey }; + let link = unwrap_option_or_return!(Self::build_conn_info(ip, false).map(|(key, _)| key)); + let key = Connection { link, pubkey }; let Some(pkg_chunk) = ip .packet() @@ -540,7 +613,7 @@ impl StatefullFirewall { let mut udp_cache = unwrap_lock_or_return!(self.udp.lock()); // If key already exists, dont change the value, just update timer with entry - match udp_cache.entry(key) { + match udp_cache.entry(key, true) { Entry::Vacant(e) => { let mut last_chunk = Vec::with_capacity(UdpConnectionInfo::LAST_PKG_MAX_CHUNK_LEN); last_chunk.extend_from_slice(pkg_chunk); @@ -569,11 +642,9 @@ impl StatefullFirewall { } fn handle_outbound_tcp<'a>(&self, pubkey: PublicKey, ip: &impl IpPacket<'a>) { - let (link, packet) = unwrap_option_or_return!(Self::build_tcp_conn_info(ip, false)); - let key = TcpConn { link, pubkey }; - - let flags = packet.get_flags(); - + let (link, packet) = unwrap_option_or_return!(Self::build_conn_info(ip, false)); + let key = Connection { link, pubkey }; + let flags = packet.map(|p| p.get_flags()).unwrap_or(0); let mut tcp_cache = unwrap_lock_or_return!(self.tcp.lock()); if flags & TCP_FIRST_PKT_MASK == TcpFlags::SYN { @@ -592,7 +663,7 @@ impl StatefullFirewall { tcp_cache.remove(&key); } else if flags & TcpFlags::FIN == TcpFlags::FIN { telio_log_trace!("Connection {:?} closing", key); - if let Entry::Occupied(mut e) = tcp_cache.entry(key) { + if let Entry::Occupied(mut e) = tcp_cache.entry(key, false) { let TcpConnectionInfo { tx_alive, .. } = e.get_mut(); *tx_alive = false; } @@ -603,7 +674,7 @@ impl StatefullFirewall { let mut icmp_cache = unwrap_lock_or_return!(self.icmp.lock()); if let Ok(key) = Self::build_icmp_key(peer, ip, false) { // If key already exists, dont change the value, just update timer with entry - if let Entry::Vacant(e) = icmp_cache.entry(key) { + if let Entry::Vacant(e) = icmp_cache.entry(key, true) { telio_log_trace!("Inserting new ICMP conntrack entry {:?}", e.key()); e.insert(()); } @@ -612,18 +683,20 @@ impl StatefullFirewall { fn handle_inbound_udp<'a>( &self, - is_conn_whitelisted: impl Fn(u16) -> bool, + decide_connection_handling: impl Fn(PublicKey, u16) -> PacketAction, peer: &PublicKey, ip: &impl IpPacket<'a>, ) -> bool { - let link = unwrap_option_or_return!(Self::build_udp_conn_info(ip, true), false); - let key = UdpConn { + let (link, _) = unwrap_option_or_return!(Self::build_conn_info(ip, true), false); + let key = Connection { link, pubkey: *peer, }; + let local_port = key.link.local_port; + let mut udp_cache = unwrap_lock_or_return!(self.udp.lock(), false); - match udp_cache.entry(key) { + match udp_cache.entry(key, true) { Entry::Occupied(mut occ) => { let connection_info = occ.get(); let key = occ.key(); @@ -634,20 +707,30 @@ impl StatefullFirewall { connection_info ); - if connection_info.is_remote_initiated && !is_conn_whitelisted(key.link.local_port) - { - telio_log_trace!("Removing UDP conntrack entry {:?}", key); - occ.remove(); - return false; + if connection_info.is_remote_initiated { + match decide_connection_handling(*peer, local_port) { + PacketAction::Drop => { + telio_log_trace!("Removing UDP conntrack entry {:?}", key); + occ.remove(); + return false; + } + PacketAction::PassThrough | PacketAction::HandleLocally => (), + } } } Entry::Vacant(vacc) => { let key = vacc.key(); - // no value in cache, insert and allow only if ip is whitelisted - if !is_conn_whitelisted(key.link.local_port) { - telio_log_trace!("Dropping UDP packet {:?} {:?}", key, peer); - return false; + match decide_connection_handling(*peer, local_port) { + PacketAction::PassThrough => { + telio_log_trace!("Accepting UDP packet {:?} {:?}", key, peer); + return true; + } + PacketAction::Drop => { + telio_log_trace!("Dropping UDP packet {:?} {:?}", key, peer); + return false; + } + PacketAction::HandleLocally => (), } telio_log_trace!("Updating UDP conntrack entry {:?} {:?}", key, peer,); @@ -664,82 +747,116 @@ impl StatefullFirewall { fn handle_inbound_tcp<'a>( &self, - is_conn_whitelisted: impl Fn(u16) -> bool, - pubkey: PublicKey, + is_conn_policy_compliant: impl Fn(PublicKey, u16) -> PacketAction, + pubkey: &PublicKey, ip: &impl IpPacket<'a>, ) -> bool { - let (link, packet) = unwrap_option_or_return!(Self::build_tcp_conn_info(ip, true), false); - let key = TcpConn { link, pubkey }; - let flags = packet.get_flags(); - - let mut tcp_cache = unwrap_lock_or_return!(self.tcp.lock(), false); - - if let Some(connection_info) = tcp_cache.peek(&key) { - telio_log_trace!( - "Matched TCP conntrack entry {:?} {:?}", - key, - connection_info - ); - if connection_info.conn_remote_initiated && !is_conn_whitelisted(key.link.local_port) { - telio_log_trace!("Removing TCP conntrack entry {:?}", key); - tcp_cache.remove(&key); - return false; - } + let (link, packet) = unwrap_option_or_return!(Self::build_conn_info(ip, true), false); + let key = Connection { + link, + pubkey: *pubkey, + }; + let local_port = key.link.local_port; + telio_log_trace!("Processing TCP packet with {:?}", key); - if flags & TcpFlags::RST == TcpFlags::RST { - telio_log_trace!("Removing TCP conntrack entry {:?}", key); - tcp_cache.remove(&key); - } else if flags & TcpFlags::FIN == TcpFlags::FIN { - telio_log_trace!("Connection {:?} closing", key); - if let Some(connection) = tcp_cache.get_mut(&key) { - connection.rx_alive = false; + let mut cache = unwrap_lock_or_return!(self.tcp.lock(), false); + // Dont update last access time in tcp, as it is handled by tcp flags + match cache.entry(key, false) { + Entry::Occupied(mut occ) => { + let connection_info = occ.get_mut(); + telio_log_trace!("Matched conntrack entry {:?} {:?}", pubkey, connection_info); + + if connection_info.conn_remote_initiated { + match is_conn_policy_compliant(*pubkey, local_port) { + PacketAction::Drop => { + telio_log_trace!("Removing TCP conntrack entry {:?}", pubkey); + occ.remove(); + return false; + } + PacketAction::PassThrough | PacketAction::HandleLocally => (), + } } - } else if !connection_info.tx_alive - && !connection_info.rx_alive - && !connection_info.conn_remote_initiated - { - if flags & TcpFlags::ACK == TcpFlags::ACK { - telio_log_trace!("Removing TCP conntrack entry {:?}", key); - tcp_cache.remove(&key); + + if let Some(pkt) = packet { + let flags = pkt.get_flags(); + let mut is_conn_reset = false; + if flags & TcpFlags::RST == TcpFlags::RST { + telio_log_trace!( + "Removing TCP conntrack entry with {:?} for {:?}", + pubkey, + local_port + ); + is_conn_reset = true; + } else if (flags & TcpFlags::FIN) == TcpFlags::FIN { + telio_log_trace!( + "Connection with {:?} for {:?} closing", + pubkey, + local_port + ); + connection_info.rx_alive = false; + } else if !connection_info.tx_alive + && !connection_info.rx_alive + && !connection_info.conn_remote_initiated + { + if (flags & TcpFlags::ACK) == TcpFlags::ACK { + telio_log_trace!( + "Removing TCP conntrack entry with {:?} for {:?}", + pubkey, + local_port + ); + occ.remove(); + return true; + } + return false; + } + // restarts cache entry timeout + let next_seq = if (flags & TcpFlags::SYN) == TcpFlags::SYN { + pkt.get_sequence() + 1 + } else { + pkt.get_sequence() + pkt.payload().len() as u32 + }; + + connection_info.next_seq = Some(next_seq); + if is_conn_reset { + occ.remove(); + } + telio_log_trace!("Accepting TCP packet {:?} {:?}", ip, pubkey); return true; } - return false; } - // restarts cache entry timeout - if let Some(val) = tcp_cache.get_mut(&key) { - let next_seq = if flags & TcpFlags::SYN == TcpFlags::SYN { - packet.get_sequence() + 1 - } else { - packet.get_sequence() + packet.payload().len() as u32 - }; + Entry::Vacant(vacc) => { + let key = vacc.key(); + match is_conn_policy_compliant(*pubkey, local_port) { + PacketAction::PassThrough => { + telio_log_trace!("Accepting UDP packet {:?} {:?}", ip, pubkey); + return true; + } + PacketAction::Drop => { + telio_log_trace!("Dropping UDP packet {:?} {:?}", key, pubkey); + return false; + } + PacketAction::HandleLocally => (), + } - val.next_seq = Some(next_seq); + if let Some(pkt) = packet { + let flags = pkt.get_flags(); + if flags & TCP_FIRST_PKT_MASK == TcpFlags::SYN { + let conn_info = TcpConnectionInfo { + tx_alive: true, + rx_alive: true, + conn_remote_initiated: true, + next_seq: Some(pkt.get_sequence() + 1), + }; + telio_log_trace!( + "Updating TCP conntrack entry {:?} {:?} {:?}", + key, + pubkey, + conn_info + ); + vacc.insert(conn_info); + } + } } - telio_log_trace!("Accepting TCP packet {:?} {:?}", ip, pubkey); - return true; - } - - if !is_conn_whitelisted(key.link.local_port) { - telio_log_trace!("Dropping TCP packet {:?} {:?}", key, pubkey); - return false; - } - - // not in cache but connection is allowed - if flags & TCP_FIRST_PKT_MASK == TcpFlags::SYN { - let conninfo = TcpConnectionInfo { - tx_alive: true, - rx_alive: true, - conn_remote_initiated: true, - next_seq: Some(packet.get_sequence() + 1), - }; - - telio_log_trace!( - "Updating TCP conntrack entry {:?} {:?} {:?}", - key, - pubkey, - conninfo - ); - tcp_cache.insert(key, conninfo); } telio_log_trace!("Accepting TCP packet {:?} {:?}", ip, pubkey); @@ -748,8 +865,13 @@ impl StatefullFirewall { fn handle_inbound_icmp<'a, P: IpPacket<'a>>(&self, peer: PublicKey, ip: &P) -> bool { let icmp_packet = unwrap_option_or_return!(IcmpPacket::new(ip.payload()), false); + let whitelist = unwrap_lock_or_return!(self.whitelist.read(), false); - if P::Icmp::BLOCKED_TYPES.contains(&icmp_packet.get_icmp_type().0) { + #[allow(index_access_check)] + if whitelist.peer_whitelists[Permissions::IncomingConnections].contains(&peer) { + telio_log_trace!("Accepting ICMP packet {:?} {:?}", ip, peer); + return true; + } else if P::Icmp::BLOCKED_TYPES.contains(&icmp_packet.get_icmp_type().0) { telio_log_trace!("Dropping ICMP packet {:?} {:?}", ip, peer); return false; } @@ -777,59 +899,50 @@ impl StatefullFirewall { } } - fn build_udp_conn_info<'a, P: IpPacket<'a>>(ip: &P, inbound: bool) -> Option { - let udp_packet = match UdpPacket::new(ip.payload()) { - Some(packet) => packet, - _ => { - telio_log_trace!("Could not create UDP packet from IP packet {:?}", ip); - return None; - } - }; - let key = if inbound { - IpConnWithPort { - remote_addr: ip.get_source().into(), - remote_port: udp_packet.get_source(), - local_addr: ip.get_destination().into(), - local_port: udp_packet.get_destination(), - } - } else { - IpConnWithPort { - remote_addr: ip.get_destination().into(), - remote_port: udp_packet.get_destination(), - local_addr: ip.get_source().into(), - local_port: udp_packet.get_source(), - } - }; - Some(key) - } - - fn build_tcp_conn_info<'a, P: IpPacket<'a>>( + fn build_conn_info<'a, P: IpPacket<'a>>( ip: &P, inbound: bool, - ) -> Option<(IpConnWithPort, TcpPacket)> { - let tcp_packet = match TcpPacket::new(ip.payload()) { - Some(packet) => packet, - _ => { - telio_log_trace!("Could not create TCP packet from IP packet {:?}", ip); - return None; - } + ) -> Option<(IpConnWithPort, Option)> { + let proto = ip.get_next_level_protocol(); + let (src, dest, tcp_packet) = match proto { + IpNextHeaderProtocols::Udp => match UdpPacket::new(ip.payload()) { + Some(packet) => (packet.get_source(), packet.get_destination(), None), + _ => { + telio_log_trace!("Could not create UDP packet from IP packet {:?}", ip); + return None; + } + }, + IpNextHeaderProtocols::Tcp => match TcpPacket::new(ip.payload()) { + Some(packet) => (packet.get_source(), packet.get_destination(), Some(packet)), + _ => { + telio_log_trace!("Could not create TCP packet from IP packet {:?}", ip); + return None; + } + }, + _ => return None, }; + let key = if inbound { IpConnWithPort { remote_addr: ip.get_source().into(), - remote_port: tcp_packet.get_source(), + remote_port: src, local_addr: ip.get_destination().into(), - local_port: tcp_packet.get_destination(), + local_port: dest, } } else { IpConnWithPort { remote_addr: ip.get_destination().into(), - remote_port: tcp_packet.get_destination(), + remote_port: dest, local_addr: ip.get_source().into(), - local_port: tcp_packet.get_source(), + local_port: src, } }; - Some((key, tcp_packet)) + + match proto { + IpNextHeaderProtocols::Udp => Some((key, None)), + IpNextHeaderProtocols::Tcp => Some((key, tcp_packet)), + _ => None, + } } fn build_icmp_key<'a, P: IpPacket<'a>>(pubkey: PublicKey, ip: &P, inbound: bool) -> IcmpKey { @@ -928,10 +1041,10 @@ impl StatefullFirewall { }; match packet.get_next_level_protocol() { IpNextHeaderProtocols::Udp => { - IcmpErrorKey::Udp(Self::build_udp_conn_info(&packet, false)) + IcmpErrorKey::Udp(Self::build_conn_info(&packet, false).map(|(key, _)| key)) } IpNextHeaderProtocols::Tcp => { - IcmpErrorKey::Tcp(Self::build_tcp_conn_info(&packet, false).map(|(key, _)| key)) + IcmpErrorKey::Tcp(Self::build_conn_info(&packet, false).map(|(key, _)| key)) } IpNextHeaderProtocols::Icmp => { IcmpErrorKey::Icmp(Self::build_icmp_key(pubkey, &packet, false).ok()) @@ -948,10 +1061,10 @@ impl StatefullFirewall { }; match packet.get_next_level_protocol() { IpNextHeaderProtocols::Udp => { - IcmpErrorKey::Udp(Self::build_udp_conn_info(&packet, false)) + IcmpErrorKey::Udp(Self::build_conn_info(&packet, false).map(|(key, _)| key)) } IpNextHeaderProtocols::Tcp => { - IcmpErrorKey::Tcp(Self::build_tcp_conn_info(&packet, false).map(|(key, _)| key)) + IcmpErrorKey::Tcp(Self::build_conn_info(&packet, false).map(|(key, _)| key)) } IpNextHeaderProtocols::Icmpv6 => { IcmpErrorKey::Icmp(Self::build_icmp_key(pubkey, &packet, false).ok()) @@ -978,7 +1091,7 @@ impl StatefullFirewall { is_in_cache } IcmpErrorKey::Tcp(Some(link)) => { - let tcp_key = TcpConn { link, pubkey }; + let tcp_key = Connection { link, pubkey }; let mut tcp_cache = unwrap_lock_or_return!(self.tcp.lock(), false); @@ -994,7 +1107,7 @@ impl StatefullFirewall { is_in_cache } IcmpErrorKey::Udp(Some(link)) => { - let udp_key = UdpConn { link, pubkey }; + let udp_key = Connection { link, pubkey }; let mut udp_cache = unwrap_lock_or_return!(self.udp.lock(), false); let is_in_cache = udp_cache.get(&udp_key).is_some(); @@ -1208,6 +1321,81 @@ impl StatefullFirewall { Ok(()) } + + fn is_private_address_except_local_interface(&self, ip: &StdIpAddr) -> bool { + let local_addrs_cache = LOCAL_ADDRS_CACHE.lock(); + if local_addrs_cache.iter().any(|itf| itf.addr.ip().eq(ip)) { + return false; + } + + match ip { + StdIpAddr::V4(ipv4) => { + // Check if IPv4 address falls into the local range + ipv4.is_private() + && !self + .custom_ip_range + .map_or(false, |range| range.contains(ipv4)) + } + StdIpAddr::V6(ipv6) => { + // Check if IPv6 address is within Unique Local Addresses range, except for meshnet IP + // fd74:656c:696f:0000/64 + match ipv6.segments() { + // If the first segment matches 0xfc00 and the next few match specific values + [0xfd74, 0x656c, 0x696f, 0, ..] => false, + [first_segment, ..] if (first_segment & 0xfc00) == 0xfc00 => true, + _ => false, + } + } + } + } + + fn decide_packet_handling( + &self, + pubkey: PublicKey, + local_addr: StdIpAddr, + whitelist: RwLockReadGuard<'_, Whitelist>, + ) -> PacketAction { + let local_ip = unwrap_lock_or_return!(self.ip_addresses.read(), PacketAction::Drop); + if self.is_private_address_except_local_interface(&local_addr) { + #[allow(index_access_check)] + if whitelist.peer_whitelists[Permissions::LocalAreaConnections].contains(&pubkey) { + return PacketAction::PassThrough; + } else { + telio_log_trace!("Local connection policy failed"); + return PacketAction::Drop; + } + } + + if local_ip.contains(&local_addr) { + return PacketAction::HandleLocally; + } + + #[allow(index_access_check)] + if whitelist.peer_whitelists[Permissions::RoutingConnections].contains(&pubkey) { + telio_log_trace!("Forwarding due to Routing policy"); + return PacketAction::PassThrough; + } + + PacketAction::Drop + } + + fn decide_connection_handling(&self, pubkey: PublicKey, local_port: u16) -> PacketAction { + let whitelist = unwrap_lock_or_return!(self.whitelist.read(), PacketAction::Drop); + #[allow(index_access_check)] + if whitelist.peer_whitelists[Permissions::IncomingConnections].contains(&pubkey) { + if self.record_whitelisted { + return PacketAction::HandleLocally; + } else { + return PacketAction::PassThrough; + } + } + + if whitelist.is_port_whitelisted(&pubkey, local_port) { + return PacketAction::HandleLocally; + } + + PacketAction::Drop + } } impl Firewall for StatefullFirewall { @@ -1235,33 +1423,43 @@ impl Firewall for StatefullFirewall { .clone() } - fn clear_peer_whitelist(&self) { - telio_log_debug!("Clearing firewall peer whitelist"); + fn clear_peer_whitelists(&self) { + telio_log_debug!("Clearing all firewall whitelist"); unwrap_lock_or_return!(self.whitelist.write()) - .peer_whitelist - .clear(); + .peer_whitelists + .iter_mut() + .for_each(|(_, whitelist)| whitelist.clear()); } - fn add_to_peer_whitelist(&self, peer: PublicKey) { - telio_log_debug!("Adding {:?} peer to firewall whitelist", peer); - unwrap_lock_or_return!(self.whitelist.write()) - .peer_whitelist + #[allow(index_access_check)] + fn add_to_peer_whitelist(&self, peer: PublicKey, permissions: Permissions) { + unwrap_lock_or_return!(self.whitelist.write(), Default::default()).peer_whitelists + [permissions] .insert(peer); } - fn remove_from_peer_whitelist(&self, peer: PublicKey) { - telio_log_debug!("Removing {:?} peer from firewall whitelist", peer); - unwrap_lock_or_return!(self.whitelist.write()) - .peer_whitelist + #[allow(index_access_check)] + fn remove_from_peer_whitelist(&self, peer: PublicKey, permissions: Permissions) { + unwrap_lock_or_return!(self.whitelist.write(), Default::default()).peer_whitelists + [permissions] .remove(&peer); } - fn get_peer_whitelist(&self) -> HashSet { - unwrap_lock_or_return!(self.whitelist.write(), Default::default()) - .peer_whitelist + #[allow(index_access_check)] + fn get_peer_whitelist(&self, permissions: Permissions) -> HashSet { + unwrap_lock_or_return!(self.whitelist.write(), Default::default()).peer_whitelists + [permissions] .clone() } + fn add_vpn_peer(&self, vpn_peer: PublicKey) { + unwrap_lock_or_return!(self.whitelist.write()).vpn_peer = Some(vpn_peer); + } + + fn remove_vpn_peer(&self) { + unwrap_lock_or_return!(self.whitelist.write()).vpn_peer = None; + } + fn process_outbound_packet(&self, public_key: &[u8; 32], buffer: &[u8]) -> bool { match unwrap_option_or_return!(buffer.first(), false) >> 4 { 4 => self.process_outbound_ip_packet::(public_key, buffer), @@ -1306,12 +1504,26 @@ impl Firewall for StatefullFirewall { Ok(()) } + + fn set_ip_addresses(&self, ip_addrs: Vec) { + let mut node_ip_address = unwrap_lock_or_return!(self.ip_addresses.write()); + for ip in ip_addrs { + node_ip_address.push(ip); + } + } } /// The default initialization of Firewall object impl Default for StatefullFirewall { fn default() -> Self { - Self::new(true, false) + Self::new( + true, + FeatureFirewall { + boringtun_reset_conns: false, + neptun_reset_conns: false, + custom_private_ip_range: None, + }, + ) } } @@ -1755,8 +1967,8 @@ pub mod tests { }, ]; for TestInput { src1, src2, src3, src4, src5, dst1, dst2, make_udp } in test_inputs { - let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, true, false); - + let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, true, FeatureFirewall::default()); + fw.set_ip_addresses(vec![(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))]); // Should FAIL (no matching outgoing connections yet) assert_eq!(fw.process_inbound_packet(&make_peer(), &make_udp(dst1, src1)), false); assert_eq!(fw.process_inbound_packet(&make_peer(), &make_udp(dst1, src2)), false); @@ -1823,7 +2035,8 @@ pub mod tests { }, ]; for TestInput { src1, src2, src3, src4, src5, dst1, dst2, make_tcp } in test_inputs { - let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, true, false); + let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, true, FeatureFirewall::default(),); + fw.set_ip_addresses(vec![StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1)),StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),]); // Should FAIL (no matching outgoing connections yet) assert_eq!(fw.process_inbound_packet(&make_peer(), &make_tcp(dst1, src1, TcpFlags::SYN)), false); @@ -1894,8 +2107,8 @@ pub mod tests { }, ]; for TestInput { src1, src2, src3, src4, src5, dst1, dst2, make_udp , is_ipv4} in test_inputs { - let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, false, false); - + let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, false, FeatureFirewall::default(),); + fw.set_ip_addresses(vec![(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))]); // Should FAIL (no matching outgoing connections yet) assert_eq!(fw.process_inbound_packet(&make_peer(), &make_udp(dst1, src1)), false); assert_eq!(fw.process_inbound_packet(&make_peer(), &make_udp(dst1, src2)), false); @@ -1951,7 +2164,8 @@ pub mod tests { TestInput{ us: "[::1]:1111", them: "[2001:4860:4860::8888]:8888", make_tcp: &make_tcp6 }, ]; for test_input @ TestInput { us, them, make_tcp } in test_inputs { - let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, true, false); + let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, true, FeatureFirewall::default(),); + fw.set_ip_addresses(vec![(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))]); let peer = make_peer(); assert_eq!(fw.process_inbound_packet(&peer, &make_tcp(them, us, TcpFlags::SYN)), false); @@ -1968,7 +2182,7 @@ pub mod tests { local_addr: test_input.us_ip(), local_port: test_input.us_port(), }; - let tcp_key = TcpConn { link , pubkey: PublicKey(peer) }; + let tcp_key = Connection { link , pubkey: PublicKey(peer) }; assert_eq!(fw.tcp.lock().unwrap().get(&tcp_key), Some(&TcpConnectionInfo{ tx_alive: true, rx_alive: true, conn_remote_initiated: false, next_seq: Some(1) @@ -2016,7 +2230,8 @@ pub mod tests { TestInput{ us: "[::1]:1111", them: "[2001:4860:4860::8888]:8888", make_tcp: &make_tcp6 }, ]; for test_input @ TestInput { us, them, make_tcp } in test_inputs { - let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, true, false); + let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, true, FeatureFirewall::default(),); + fw.set_ip_addresses(vec![StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1)),StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),]); let peer = make_peer(); let outgoing_init_packet = make_tcp(us, them, TcpFlags::SYN); @@ -2028,7 +2243,7 @@ pub mod tests { local_addr: test_input.us_ip(), local_port: test_input.us_port(), }; - let conn_key = TcpConn { link , pubkey: PublicKey(peer) }; + let conn_key = Connection { link , pubkey: PublicKey(peer) }; assert_eq!(fw.tcp.lock().unwrap().get(&conn_key), Some(&TcpConnectionInfo{ tx_alive: true, rx_alive: true, conn_remote_initiated: false, next_seq: None @@ -2075,7 +2290,8 @@ pub mod tests { ]; for test_input @ TestInput { us, them, make_tcp } in test_inputs { let ttl = 20; - let fw = StatefullFirewall::new_custom(3, ttl, true, false); + let fw = StatefullFirewall::new_custom(3, ttl, true, FeatureFirewall::default(),); + fw.set_ip_addresses(vec![StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1)),StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),]); let peer = make_peer(); let outgoing_init_packet = make_tcp(us, them, TcpFlags::SYN); @@ -2087,7 +2303,7 @@ pub mod tests { local_addr: test_input.us_ip(), local_port: test_input.us_port(), }; - let conn_key = TcpConn { link , pubkey: PublicKey(peer) }; + let conn_key = Connection { link , pubkey: PublicKey(peer) }; assert_eq!(fw.tcp.lock().unwrap().get(&conn_key), Some(&TcpConnectionInfo{ tx_alive: true, rx_alive: true, conn_remote_initiated: false, next_seq: None @@ -2136,7 +2352,8 @@ pub mod tests { TestInput { src1: "2001:4860:4860::8888", src2: "2001:4860:4860::8844", src3: "2001:4860:4860::4444", dst: "::1", make_icmp: &make_icmp6_with_body }, ]; for TestInput{ src1, src2, src3, dst, make_icmp } in test_inputs { - let fw = StatefullFirewall::new_custom(2, LRU_TIMEOUT, true, false); + let fw = StatefullFirewall::new_custom(2, LRU_TIMEOUT, true, FeatureFirewall::default(),); + fw.set_ip_addresses(vec![StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1)),StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),]); let request1 = make_icmp(dst, src1, IcmpTypes::EchoRequest.into(), &[1, 0, 1, 0]); let request2 = make_icmp(dst, src2, IcmpTypes::EchoRequest.into(), &[1, 0, 1, 0]); @@ -2176,7 +2393,8 @@ pub mod tests { TestInput { src: "2001:4860:4860::8888", dst: "::1", make_icmp: &make_icmp6_with_body }, ]; for TestInput{ src, dst, make_icmp } in test_inputs { - let fw = StatefullFirewall::new(true, false); + let fw = StatefullFirewall::new_custom(LRU_CAPACITY, LRU_TIMEOUT, true, FeatureFirewall::default()); + fw.set_ip_addresses(vec![StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1)),StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),]); let request = make_icmp(dst, src, IcmpTypes::EchoRequest.into(), &[1, 0, 1, 0]); let reply1 = make_icmp(src, dst, IcmpTypes::EchoReply.into(), &[1, 0, 1, 0]); @@ -2200,11 +2418,11 @@ pub mod tests { MakeIcmpWithBody, ); - const V4_SRC_IP: &str = "8.8.8.8"; - const V4_DST_IP: &str = "127.0.0.1"; + const V4_DST_IP: &str = "8.8.8.8"; + const V4_SRC_IP: &str = "127.0.0.1"; - const V6_SRC_IP: &str = "2001:4860:4860::8888"; - const V6_DST_IP: &str = "::1"; + const V6_DST_IP: &str = "2001:4860:4860::8888"; + const V6_SRC_IP: &str = "::1"; let test_input_v4 = vec![ (IcmpTypes::EchoRequest, Some(IcmpTypes::EchoReply)), @@ -2258,7 +2476,16 @@ pub mod tests { if request_type == reply_type { continue; } - let fw = StatefullFirewall::new(true, false); + let fw = StatefullFirewall::new_custom( + LRU_CAPACITY, + LRU_TIMEOUT, + true, + FeatureFirewall::default(), + ); + fw.set_ip_addresses(vec![ + StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1)), + StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); let outbound = make_request_packet(src_ip, dst_ip, *request_type, &[1, 0, 1, 0]); let inbound = make_reply_packet(dst_ip, src_ip, *reply_type, &[1, 0, 1, 0]); @@ -2281,7 +2508,7 @@ pub mod tests { GenericIcmpType, MakeIcmpWithBody, ); - for TestInput(src, dst, request_type, error_type, make_icmp) in [ + for TestInput(dst, src, request_type, error_type, make_icmp) in [ TestInput( "8.8.8.8", "127.0.0.1", @@ -2297,7 +2524,16 @@ pub mod tests { &make_icmp6_with_body, ), ] { - let fw = StatefullFirewall::new(true, false); + let fw = StatefullFirewall::new_custom( + LRU_CAPACITY, + LRU_TIMEOUT, + true, + FeatureFirewall::default(), + ); + fw.set_ip_addresses(vec![ + StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1)), + StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); let actual_request = make_icmp(src, dst, request_type, &[1, 0, 1, 0]); let other_request = make_icmp(src, dst, request_type, &[2, 0, 3, 0]); @@ -2339,12 +2575,12 @@ pub mod tests { MakeIcmpWithBody, ); for TestInput( - src1, - src1_with_port, - src2, - src2_with_port, dst, dst_with_port, + src2, + src2_with_port, + src1, + src1_with_port, error_type, make_tcp, make_icmp, @@ -2352,8 +2588,8 @@ pub mod tests { TestInput( "8.8.8.8", "8.8.8.8:8888", - "8.8.4.4", - "8.8.4.4:4444", + "127.0.0.1", + "127.0.0.1:4444", "127.0.0.1", "127.0.0.1:1111", IcmpTypes::DestinationUnreachable.into(), @@ -2363,8 +2599,8 @@ pub mod tests { TestInput( "2001:4860:4860::8888", "[2001:4860:4860::8888]:8888", - "2001:4860:4860::4444", - "[2001:4860:4860::4444]:4444", + "2001:4860:4860::8844", + "[2001:4860:4860::8844]:4444", "::1", "[::1]:1111", Icmpv6Types::DestinationUnreachable.into(), @@ -2372,7 +2608,16 @@ pub mod tests { &make_icmp6_with_body, ), ] { - let fw = StatefullFirewall::new(true, false); + let fw = StatefullFirewall::new_custom( + LRU_CAPACITY, + LRU_TIMEOUT, + true, + FeatureFirewall::default(), + ); + fw.set_ip_addresses(vec![ + StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1)), + StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); let actual_request = make_tcp(src1_with_port, dst_with_port, TcpFlags::SYN); let other_request = make_tcp(src2_with_port, dst_with_port, TcpFlags::SYN); @@ -2414,12 +2659,12 @@ pub mod tests { MakeIcmpWithBody, ); for TestInput( - src1, - src1_with_port, - src2, - src2_with_port, dst, dst_with_port, + src2, + src2_with_port, + src1, + src1_with_port, error_type, make_udp, make_icmp, @@ -2427,8 +2672,8 @@ pub mod tests { TestInput( "8.8.8.8", "8.8.8.8:8888", - "8.8.4.4", - "8.8.4.4:4444", + "127.0.0.1", + "127.0.0.1:4444", "127.0.0.1", "127.0.0.1:1111", IcmpTypes::DestinationUnreachable.into(), @@ -2438,8 +2683,8 @@ pub mod tests { TestInput( "2001:4860:4860::8888", "[2001:4860:4860::8888]:8888", - "2001:4860:4860::4444", - "[2001:4860:4860::4444]:4444", + "2001:4860:4860::8844", + "[2001:4860:4860::8844]:4444", "::1", "[::1]:1111", Icmpv6Types::DestinationUnreachable.into(), @@ -2447,7 +2692,16 @@ pub mod tests { &make_icmp6_with_body, ), ] { - let fw = StatefullFirewall::new(true, false); + let fw = StatefullFirewall::new_custom( + LRU_CAPACITY, + LRU_TIMEOUT, + true, + FeatureFirewall::default(), + ); + fw.set_ip_addresses(vec![ + StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1)), + StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); let actual_request = make_udp(src1_with_port, dst_with_port); let other_request = make_udp(src2_with_port, dst_with_port); @@ -2491,7 +2745,7 @@ pub mod tests { TestInput { src1: "2001:4860:4860::8888",src2: "2001:4860:4860::8844", dst: "::1", make_icmp: &make_icmp6, is_v4: false}, ]; for TestInput { src1, src2, dst, make_icmp, is_v4 } in test_inputs { - let fw = StatefullFirewall::new_custom(0, LRU_TIMEOUT, true, false); + let fw = StatefullFirewall::new_custom(0, LRU_TIMEOUT, true, FeatureFirewall::default(),); // Firewall only allow inbound ICMP packets that are either whitelisted or that exist in the ICMP cache // The ICMP cache only accepts a small number of ICMP types, but unrelated to that, this test ignores the cache completely @@ -2548,7 +2802,11 @@ pub mod tests { ]; for TestInput { src, dst, make_udp } in test_inputs { - let fw = StatefullFirewall::new_custom(3, 100, true, false); + let fw = StatefullFirewall::new_custom(3, 100, true, FeatureFirewall::default(),); + fw.set_ip_addresses(vec![ + (StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), + StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); assert_eq!(fw.process_outbound_packet(&make_peer(), &make_udp(src, dst)), true); assert_eq!(fw.process_inbound_packet(&make_peer(), &make_udp(dst, src)), true); @@ -2571,7 +2829,7 @@ pub mod tests { ]; for TestInput { src, dst, make_udp } in test_inputs { - let fw = StatefullFirewall::new_custom(capacity, ttl, true, false); + let fw = StatefullFirewall::new_custom(capacity, ttl, true, FeatureFirewall::default(),); // Should PASS (adds 1111) assert_eq!(fw.process_outbound_packet(&make_peer(), &make_udp(src, dst)), true); @@ -2594,19 +2852,19 @@ pub mod tests { #[rustfmt::skip] #[test] fn firewall_whitelist_crud() { - let fw = StatefullFirewall::new(true, false); - assert!(fw.get_peer_whitelist().is_empty()); + let fw = StatefullFirewall::new_custom(LRU_CAPACITY,LRU_TIMEOUT,true,FeatureFirewall::default(),); + assert!(fw.get_peer_whitelist(Permissions::IncomingConnections).is_empty()); let peer = make_random_peer(); - fw.add_to_peer_whitelist(peer); - fw.add_to_peer_whitelist(make_random_peer()); - assert_eq!(fw.get_peer_whitelist().len(), 2); + fw.add_to_peer_whitelist(peer, Permissions::IncomingConnections); + fw.add_to_peer_whitelist(make_random_peer(), Permissions::IncomingConnections); + assert_eq!(fw.get_peer_whitelist(Permissions::IncomingConnections).len(), 2); - fw.remove_from_peer_whitelist(peer); - assert_eq!(fw.get_peer_whitelist().len(), 1); + fw.remove_from_peer_whitelist(peer, Permissions::IncomingConnections); + assert_eq!(fw.get_peer_whitelist(Permissions::IncomingConnections).len(), 1); - fw.clear_peer_whitelist(); - assert!(fw.get_peer_whitelist().is_empty()); + fw.clear_peer_whitelists(); + assert!(fw.get_peer_whitelist(Permissions::IncomingConnections).is_empty()); } #[rustfmt::skip] @@ -2655,11 +2913,12 @@ pub mod tests { } ]; for TestInput { src1, src2, src3, src4, src5, dst1, dst2, make_udp, make_tcp, make_icmp } in test_inputs { - let fw = StatefullFirewall::new(true, false); + let fw = StatefullFirewall::new_custom(LRU_CAPACITY, LRU_TIMEOUT, true, FeatureFirewall::default(),); + fw.set_ip_addresses(vec![(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))]); let peer1 = make_random_peer(); let peer2 = make_random_peer(); - assert!(fw.get_peer_whitelist().is_empty()); + assert!(fw.get_peer_whitelist(Permissions::IncomingConnections).is_empty()); assert_eq!(fw.process_inbound_packet(&peer1.0, &make_udp(src1, dst1,)), false); assert_eq!(fw.process_inbound_packet(&peer2.0, &make_udp(src2, dst1,)), false); @@ -2679,7 +2938,7 @@ pub mod tests { assert_eq!(fw.process_inbound_packet(&peer2.0, &make_tcp(src5, dst1, TcpFlags::SYN)), true); assert_eq!(fw.tcp.lock().unwrap().len(), 1); - fw.add_to_peer_whitelist(peer2); + fw.add_to_peer_whitelist(peer2, Permissions::IncomingConnections); let src = src1.parse::().unwrap().ip().to_string(); let dst = dst1.parse::().unwrap().ip().to_string(); assert_eq!(fw.process_inbound_packet(&peer1.0, &make_icmp(&src, &dst, IcmpTypes::EchoRequest.into())), false); @@ -2703,7 +2962,8 @@ pub mod tests { ]; for TestInput { us, them, make_icmp, is_v4 } in test_inputs { // Set number of conntrack entries to 0 to test only the whitelist - let fw = StatefullFirewall::new_custom(0, 20, true, false); + let fw = StatefullFirewall::new_custom(0, 20, true, FeatureFirewall::default(),); + fw.set_ip_addresses(vec![(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))]); let peer = make_random_peer(); assert_eq!(fw.process_outbound_packet(&peer.0, &make_icmp(us, them, IcmpTypes::EchoRequest.into())), true); @@ -2716,7 +2976,7 @@ pub mod tests { assert_eq!(fw.process_inbound_packet(&peer.0, &make_icmp4(them, us, IcmpTypes::AddressMaskRequest.into())), false); } - fw.add_to_peer_whitelist(peer); + fw.add_to_peer_whitelist(peer, Permissions::IncomingConnections); assert_eq!(fw.process_inbound_packet(&peer.0, &make_icmp(them, us, IcmpTypes::EchoRequest.into())), true); assert_eq!(fw.process_inbound_packet(&peer.0, &make_icmp(them, us, IcmpTypes::EchoReply.into())), true); if is_v4 { @@ -2737,7 +2997,8 @@ pub mod tests { ]; for TestInput { us, them, make_udp } in test_inputs { - let fw = StatefullFirewall::new(true, false); + let fw = StatefullFirewall::new_custom(LRU_CAPACITY, LRU_TIMEOUT, true, FeatureFirewall::default(),); + fw.set_ip_addresses(vec![(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))]); let them_peer = make_random_peer(); @@ -2765,8 +3026,8 @@ pub mod tests { ]; for TestInput { us, them, make_udp } in test_inputs { - let fw = StatefullFirewall::new(true, false); - + let fw = StatefullFirewall::new_custom(LRU_CAPACITY, LRU_TIMEOUT, true, FeatureFirewall::default(),); + fw.set_ip_addresses(vec![(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))]); let them_peer = make_random_peer(); fw.add_to_port_whitelist(them_peer,1111); @@ -2791,7 +3052,8 @@ pub mod tests { TestInput{ us: "[::1]:1111", them: "[2001:4860:4860::8888]:8888", make_tcp: &make_tcp6, }, ]; for TestInput { us, them, make_tcp } in test_inputs { - let fw = StatefullFirewall::new(true, false); + let fw = StatefullFirewall::new_custom(LRU_CAPACITY, LRU_TIMEOUT, true, FeatureFirewall::default(),); + fw.set_ip_addresses(vec![(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))]); let them_peer = make_random_peer(); @@ -2821,7 +3083,8 @@ pub mod tests { TestInput{ us: "[::1]:1111", them: "[2001:4860:4860::8888]:8888", make_tcp: &make_tcp6, }, ]; for TestInput { us, them, make_tcp } in test_inputs { - let fw = StatefullFirewall::new(true, false); + let fw = StatefullFirewall::new_custom(LRU_CAPACITY,LRU_TIMEOUT,true,FeatureFirewall::default(),); + fw.set_ip_addresses(vec![(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))]); let them_peer = make_random_peer(); @@ -2872,8 +3135,9 @@ pub mod tests { } ]; for TestInput { src1, src2, dst, make_udp, make_tcp, make_icmp } in test_inputs { - let fw = StatefullFirewall::new(true, false); - assert!(fw.get_peer_whitelist().is_empty()); + let fw = StatefullFirewall::new_custom(LRU_CAPACITY, LRU_TIMEOUT, true, FeatureFirewall::default(),); + fw.set_ip_addresses(vec![(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))]); + assert!(fw.get_peer_whitelist(Permissions::IncomingConnections).is_empty()); let src2_ip = src2.parse::().unwrap().ip().to_string(); let dst_ip = dst.parse::().unwrap().ip().to_string(); @@ -2882,15 +3146,15 @@ pub mod tests { assert_eq!(fw.process_inbound_packet(&make_peer(), &make_icmp(&src2_ip, &dst_ip, IcmpTypes::EchoRequest.into())), false); assert_eq!(fw.process_inbound_packet(&make_peer(), &make_tcp(src2, dst, TcpFlags::PSH)), false); - fw.add_to_peer_whitelist((&make_peer()).into()); - assert_eq!(fw.get_peer_whitelist().len(), 1); + fw.add_to_peer_whitelist((&make_peer()).into(), Permissions::IncomingConnections); + assert_eq!(fw.get_peer_whitelist(Permissions::IncomingConnections).len(), 1); assert_eq!(fw.process_inbound_packet(&make_peer(), &make_udp(src1, dst,)), true); assert_eq!(fw.process_inbound_packet(&make_peer(), &make_icmp(&src2_ip, &dst_ip, IcmpTypes::EchoRequest.into())), true); assert_eq!(fw.process_inbound_packet(&make_peer(), &make_tcp(src2, dst, TcpFlags::PSH)), true); - fw.remove_from_peer_whitelist((&make_peer()).into()); - assert_eq!(fw.get_peer_whitelist().len(), 0); + fw.remove_from_peer_whitelist((&make_peer()).into(), Permissions::IncomingConnections); + assert_eq!(fw.get_peer_whitelist(Permissions::IncomingConnections).len(), 0); assert_eq!(fw.process_inbound_packet(&make_peer(), &make_udp(src1, dst,)), false); assert_eq!(fw.process_inbound_packet(&make_peer(), &make_icmp(&src2_ip, &dst_ip, IcmpTypes::EchoRequest.into())), false); @@ -2898,6 +3162,266 @@ pub mod tests { } } + #[test] + fn test_local_network_range() { + use if_addrs::{IfAddr, Ifv4Addr, Interface}; + + *LOCAL_ADDRS_CACHE.lock() = vec![Interface { + name: "eth0".to_string(), + addr: IfAddr::V4(Ifv4Addr { + ip: Ipv4Addr::new(192, 168, 1, 10), + netmask: Ipv4Addr::new(192, 168, 1, 0), + broadcast: None, + }), + index: Some(12), + #[cfg(windows)] + adapter_name: "adapter".to_string(), + }]; + + let fw = StatefullFirewall::new_custom( + LRU_CAPACITY, + LRU_TIMEOUT, + true, + FeatureFirewall::default(), + ); + fw.set_ip_addresses(vec![ + (StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), + StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); + assert!(!fw.is_private_address_except_local_interface( + &(StdIpAddr::V4(StdIpv4Addr::new(192, 168, 1, 10))) + )); + assert!(fw.is_private_address_except_local_interface( + &(StdIpAddr::V4(StdIpv4Addr::new(10, 10, 10, 10))) + )); + assert!(fw.is_private_address_except_local_interface( + &(StdIpAddr::V4(StdIpv4Addr::new(172, 20, 10, 8))) + )); + assert!(!fw.is_private_address_except_local_interface( + &(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))) + )); + assert!(fw.is_private_address_except_local_interface( + &(StdIpAddr::V6(StdIpv6Addr::new(0xfc53, 0, 0, 1, 0, 0, 0, 1))) + )); + assert!(fw.is_private_address_except_local_interface( + &(StdIpAddr::V6(StdIpv6Addr::new(0xfd47, 0xde82, 0, 1, 0, 0, 0, 1))) + )); + assert!(fw.is_private_address_except_local_interface( + &(StdIpAddr::V6(StdIpv6Addr::new(0xfc47, 0x656c, 0x696f, 0, 0, 0, 0, 1))) + )); + assert!(!fw.is_private_address_except_local_interface( + &(StdIpAddr::V6(StdIpv6Addr::new(0xfd74, 0x656c, 0x696f, 0, 0, 0, 0, 1))) + )); + assert!(!fw.is_private_address_except_local_interface( + &(StdIpAddr::V6(StdIpv6Addr::new(0xefab, 0, 0, 1, 0, 0, 0, 1))) + )); + } + + #[test] + fn test_local_network_range_with_custom_range() { + let fw = StatefullFirewall::new_custom( + LRU_CAPACITY, + LRU_TIMEOUT, + true, + FeatureFirewall { + custom_private_ip_range: Some( + Ipv4Net::new(StdIpv4Addr::new(10, 0, 0, 0), 8).unwrap(), + ), + boringtun_reset_conns: false, + neptun_reset_conns: false, + }, + ); + fw.set_ip_addresses(vec![ + (StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), + StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); + assert!(!fw.is_private_address_except_local_interface( + &(StdIpAddr::V4(StdIpv4Addr::new(10, 10, 10, 10))) + )); + } + + #[test] + fn firewall_test_permissions() { + struct TestInput { + src1: &'static str, + src2: &'static str, + local_dst: &'static str, + area_dst: &'static str, + external_dst: &'static str, + make_udp: MakeUdp, + make_tcp: MakeTcp, + } + let test_inputs = vec![ + TestInput { + src1: "100.100.100.100:1234", + src2: "100.100.100.101:1234", + local_dst: "127.0.0.1:1111", + area_dst: "192.168.0.1:1111", + external_dst: "13.32.4.21:1111", + make_udp: &make_udp, + make_tcp: &make_tcp, + }, + TestInput { + src1: "[2001:4860:4860::8888]:1234", + src2: "[2001:4860:4860::8844]:1234", + local_dst: "[::1]:1111", + area_dst: "[fe80:4860:4860::8844]:2222", + external_dst: "[2001:4860:4860::8888]:8888", + make_udp: &make_udp6, + make_tcp: &make_tcp6, + }, + ]; + for TestInput { + src1, + src2, + local_dst, + area_dst, + external_dst, + make_udp, + make_tcp, + } in test_inputs + { + let fw = StatefullFirewall::new_custom( + LRU_CAPACITY, + LRU_TIMEOUT, + true, + FeatureFirewall::default(), + ); + fw.set_ip_addresses(vec![ + StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1)), + StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); + assert!(fw + .get_peer_whitelist(Permissions::IncomingConnections) + .is_empty()); + assert!(fw + .get_peer_whitelist(Permissions::RoutingConnections) + .is_empty()); + assert!(fw + .get_peer_whitelist(Permissions::LocalAreaConnections) + .is_empty()); + + // No permissions. Incoming packet should FAIL + assert!(!fw.process_inbound_packet(&make_peer(), &make_udp(src1, local_dst,))); + assert!( + !fw.process_inbound_packet(&make_peer(), &make_tcp(src1, local_dst, TcpFlags::PSH)) + ); + + // Allow incoming connection + fw.add_to_peer_whitelist((&make_peer()).into(), Permissions::IncomingConnections); + assert_eq!( + fw.get_peer_whitelist(Permissions::IncomingConnections) + .len(), + 1 + ); + + // Packet destined for local node + assert!(fw.process_inbound_packet(&make_peer(), &make_udp(src1, local_dst,))); + assert!( + fw.process_inbound_packet(&make_peer(), &make_tcp(src1, local_dst, TcpFlags::PSH)) + ); + // Packet destined for foreign node but within local area. Should FAIL + assert!(!fw.process_inbound_packet(&make_peer(), &make_udp(src1, area_dst,))); + assert!( + !fw.process_inbound_packet(&make_peer(), &make_tcp(src1, area_dst, TcpFlags::PSH)) + ); + // Packet destined for totally external node. Should FAIL + assert!(!fw.process_inbound_packet(&make_peer(), &make_udp(src1, external_dst,))); + assert!(!fw.process_inbound_packet( + &make_peer(), + &make_tcp(src1, external_dst, TcpFlags::PSH) + )); + + // Allow local area connections + fw.add_to_peer_whitelist((&make_peer()).into(), Permissions::LocalAreaConnections); + assert_eq!( + fw.get_peer_whitelist(Permissions::LocalAreaConnections) + .len(), + 1 + ); + // Packet destined for foreign node but within local area + assert!(fw.process_inbound_packet(&make_peer(), &make_udp(src1, area_dst,))); + assert!( + fw.process_inbound_packet(&make_peer(), &make_tcp(src1, area_dst, TcpFlags::PSH)) + ); + // Packet destined for totally external node. Should FAIL + assert!(!fw.process_inbound_packet(&make_peer(), &make_udp(src1, external_dst,))); + assert!(!fw.process_inbound_packet( + &make_peer(), + &make_tcp(src1, external_dst, TcpFlags::PSH) + )); + + // Allow routing permissiongs but no local area connection + fw.remove_from_peer_whitelist((&make_peer()).into(), Permissions::LocalAreaConnections); + assert_eq!( + fw.get_peer_whitelist(Permissions::LocalAreaConnections) + .len(), + 0 + ); + + fw.add_to_peer_whitelist((&make_peer()).into(), Permissions::RoutingConnections); + assert_eq!( + fw.get_peer_whitelist(Permissions::RoutingConnections).len(), + 1 + ); + // Packet destined for foreign node but within local area. Should FAIL + assert!(!fw.process_inbound_packet(&make_peer(), &make_udp(src1, area_dst,))); + assert!( + !fw.process_inbound_packet(&make_peer(), &make_tcp(src1, area_dst, TcpFlags::PSH)) + ); + // Packet destined for totally external node + assert!(fw.process_inbound_packet(&make_peer(), &make_udp(src1, external_dst,))); + assert!(fw.process_inbound_packet( + &make_peer(), + &make_tcp(src1, external_dst, TcpFlags::PSH) + )); + + // Allow only routing + fw.remove_from_peer_whitelist((&make_peer()).into(), Permissions::IncomingConnections); + assert_eq!( + fw.get_peer_whitelist(Permissions::IncomingConnections) + .len(), + 0 + ); + + // Packet destined for local node. Should FAIL + assert!(!fw.process_inbound_packet(&make_peer(), &make_udp(src1, local_dst,))); + assert!( + !fw.process_inbound_packet(&make_peer(), &make_tcp(src1, local_dst, TcpFlags::PSH)) + ); + // Packet destined for totally external node + assert!(fw.process_inbound_packet(&make_peer(), &make_udp(src1, external_dst,))); + assert!(fw.process_inbound_packet( + &make_peer(), + &make_tcp(src1, external_dst, TcpFlags::PSH) + )); + + fw.remove_from_peer_whitelist((&make_peer()).into(), Permissions::RoutingConnections); + assert_eq!( + fw.get_peer_whitelist(Permissions::RoutingConnections).len(), + 0 + ); + + // All packets should FAIL + // Packet destined for local node + assert!(!fw.process_inbound_packet(&make_peer(), &make_udp(src1, local_dst,))); + assert!( + !fw.process_inbound_packet(&make_peer(), &make_tcp(src1, local_dst, TcpFlags::PSH)) + ); + // Packet destined for foreign node but within local area + assert!(!fw.process_inbound_packet(&make_peer(), &make_udp(src1, area_dst,))); + assert!( + !fw.process_inbound_packet(&make_peer(), &make_tcp(src1, area_dst, TcpFlags::PSH)) + ); + // Packet destined for totally external node + assert!(!fw.process_inbound_packet(&make_peer(), &make_udp(src1, external_dst,))); + assert!(!fw.process_inbound_packet( + &make_peer(), + &make_tcp(src1, external_dst, TcpFlags::PSH) + )); + } + } + #[rustfmt::skip] #[test] fn firewall_whitelist_port() { @@ -2927,8 +3451,9 @@ pub mod tests { } ]; for test_input @ TestInput { src, dst, make_udp, make_tcp, make_icmp } in test_inputs { - let fw = StatefullFirewall::new(true, false); - assert!(fw.get_peer_whitelist().is_empty()); + let fw = StatefullFirewall::new_custom(LRU_CAPACITY,LRU_TIMEOUT,true,FeatureFirewall::default(),); + fw.set_ip_addresses(vec![(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))]); + assert!(fw.get_peer_whitelist(Permissions::IncomingConnections).is_empty()); assert!(fw.get_port_whitelist().is_empty()); assert_eq!(fw.process_inbound_packet(&make_peer(), &make_udp(&test_input.src_socket(11111), &test_input.dst_socket(FILE_SEND_PORT))), false); @@ -2957,25 +3482,31 @@ pub mod tests { #[test] fn firewall_tcp_conns_reset() { - let fw = StatefullFirewall::new(false, false); + let fw = StatefullFirewall::new_custom( + LRU_CAPACITY, + LRU_TIMEOUT, + false, + FeatureFirewall::default(), + ); + fw.set_ip_addresses(vec![(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1)))]); let peer = make_peer(); fw.add_to_port_whitelist(PublicKey(peer), FILE_SEND_PORT); // Outbound half opened connection assert!(fw.process_outbound_packet( &peer, - &make_tcp("127.0.0.2:12345", "101.101.101.101:54321", TcpFlags::SYN), + &make_tcp("127.0.0.1:12345", "104.104.104.104:54321", TcpFlags::SYN), )); // Outbound opened connection assert!(fw.process_outbound_packet( &peer, - &make_tcp("127.0.0.4:12345", "103.103.103.103:54321", TcpFlags::SYN), + &make_tcp("127.0.0.1:12345", "103.103.103.103:54321", TcpFlags::SYN), )); assert!(fw.process_inbound_packet( &peer, &make_tcp( "103.103.103.103:54321", - &"127.0.0.4:12345", + "127.0.0.1:12345", TcpFlags::SYN | TcpFlags::ACK ), )); @@ -2993,7 +3524,7 @@ pub mod tests { &peer, &make_tcp( "102.102.102.102:12345", - &format!("127.0.0.3:{FILE_SEND_PORT}"), + &format!("127.0.0.1:{FILE_SEND_PORT}"), TcpFlags::SYN ), )); @@ -3001,7 +3532,7 @@ pub mod tests { &peer, &make_tcp( "102.102.102.102:12345", - &format!("127.0.0.3:{FILE_SEND_PORT}"), + &format!("127.0.0.1:{FILE_SEND_PORT}"), TcpFlags::PSH ), )); @@ -3039,7 +3570,7 @@ pub mod tests { .collect(); // Make it sorted by dest IP - ippkgs.sort_unstable_by_key(|ippkg| ippkg.get_destination()); + ippkgs.sort_unstable_by_key(|ippkg| ippkg.get_source()); let tcppkgs: Vec<_> = ippkgs .iter() @@ -3067,14 +3598,14 @@ pub mod tests { assert_eq!(tcppkgs[0].get_sequence(), 1); assert_eq!(ippkgs[1].get_source(), Ipv4Addr::new(102, 102, 102, 102)); - assert_eq!(ippkgs[1].get_destination(), Ipv4Addr::new(127, 0, 0, 3)); + assert_eq!(ippkgs[1].get_destination(), Ipv4Addr::new(127, 0, 0, 1)); assert_eq!(tcppkgs[1].get_source(), 12345); assert_eq!(tcppkgs[1].get_destination(), FILE_SEND_PORT); assert_eq!(tcppkgs[1].get_sequence(), 12); assert_eq!(ippkgs[2].get_source(), Ipv4Addr::new(103, 103, 103, 103)); - assert_eq!(ippkgs[2].get_destination(), Ipv4Addr::new(127, 0, 0, 4)); + assert_eq!(ippkgs[2].get_destination(), Ipv4Addr::new(127, 0, 0, 1)); assert_eq!(tcppkgs[2].get_source(), 54321); assert_eq!(tcppkgs[2].get_destination(), 12345); @@ -3083,11 +3614,16 @@ pub mod tests { #[test] fn firewall_udp_conns_reset() { - let fw = StatefullFirewall::new(false, false); + let fw = StatefullFirewall::new_custom( + LRU_CAPACITY, + LRU_TIMEOUT, + false, + FeatureFirewall::default(), + ); let peer = make_peer(); fw.add_to_port_whitelist(PublicKey(peer), FILE_SEND_PORT); - let test_udppkg = make_udp("127.0.0.2:12345", "101.101.101.101:54321"); + let test_udppkg = make_udp("127.0.0.2:12345", "127.0.0.1:54321"); // Outbound connection assert!(fw.process_outbound_packet(&peer, &test_udppkg,)); @@ -3143,13 +3679,13 @@ pub mod tests { #[test] fn firewall_icmp_ddos_vulnerability() { - let src1 = "8.8.8.8:8888"; - let src2 = "8.8.8.8"; - let dst1 = "127.0.0.1:2000"; - let dst2 = "127.0.0.1"; - - let fw = StatefullFirewall::new_custom(2, LRU_TIMEOUT, false, false); + let dst1 = "8.8.8.8:8888"; + let dst2 = "8.8.8.8"; + let src1 = "127.0.0.1:2000"; + let src2 = "127.0.0.1"; + let fw = StatefullFirewall::new_custom(2, LRU_TIMEOUT, false, FeatureFirewall::default()); + fw.set_ip_addresses(vec![(StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1)))]); let good_peer = make_random_peer(); let bad_peer = make_random_peer(); @@ -3180,8 +3716,11 @@ pub mod tests { let them = "8.8.8.8:8888"; let random = "192.168.0.1:7777"; - let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, false, false); - + let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, false, Default::default()); + fw.set_ip_addresses(vec![ + (StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), + StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); let outgoing_init_packet = make_tcp(us, them, TcpFlags::SYN); let incoming_rst_packet = make_tcp(them, us, TcpFlags::RST); @@ -3219,15 +3758,18 @@ pub mod tests { ]; for TestInput { src, dst, make_udp } in test_inputs { - let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, true, false); - + let fw = StatefullFirewall::new_custom(3, LRU_TIMEOUT, true, Default::default()); + fw.set_ip_addresses(vec![ + (StdIpAddr::V4(StdIpv4Addr::new(127, 0, 0, 1))), + StdIpAddr::V6(StdIpv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]); let outgoing_packet = make_udp(src, dst); let incoming_packet = make_udp(dst, src); let peer_bad = make_random_peer(); let peer_good = make_random_peer(); - fw.add_to_peer_whitelist(peer_good); + fw.add_to_peer_whitelist(peer_good, Permissions::IncomingConnections); fw.add_to_port_whitelist(peer_good, 1111); // Should Pass as it is inbound for trusted peer diff --git a/crates/telio-model/src/config.rs b/crates/telio-model/src/config.rs index 71d805b2a..2363c5fa1 100644 --- a/crates/telio-model/src/config.rs +++ b/crates/telio-model/src/config.rs @@ -42,6 +42,10 @@ pub struct Peer { pub is_local: bool, /// Flag to control whether the peer allows incoming connections pub allow_incoming_connections: bool, + /// Flag to control whether the Node allows routing through + pub allow_peer_traffic_routing: bool, + /// Flag to control whether the Node allows incoming local area access + pub allow_peer_local_network_access: bool, #[serde(default)] /// Flag to control whether the peer allows incoming files pub allow_peer_send_files: bool, @@ -345,6 +349,7 @@ mod tests { "is_local": true, "user_email": "alice@example.com", "allow_incoming_connections": true, + "allow_peer_local_network_access": true, "allow_peer_send_files": true, "peer_allows_traffic_routing": false, "allow_peer_traffic_routing": true, @@ -364,6 +369,7 @@ mod tests { "is_local": false, "user_email": "bob@example.com", "allow_incoming_connections": false, + "allow_peer_local_network_access": false, "allow_peer_send_files": false, "peer_allows_traffic_routing": true, "allow_peer_traffic_routing": false, @@ -455,6 +461,8 @@ mod tests { }, is_local: true, allow_incoming_connections: true, + allow_peer_traffic_routing: true, + allow_peer_local_network_access: true, allow_peer_send_files: true, allow_multicast: true, peer_allows_multicast: true, @@ -471,6 +479,8 @@ mod tests { }, is_local: false, allow_incoming_connections: false, + allow_peer_traffic_routing: false, + allow_peer_local_network_access: false, allow_peer_send_files: false, allow_multicast: true, peer_allows_multicast: false, diff --git a/crates/telio-model/src/event.rs b/crates/telio-model/src/event.rs index 76e713212..9753ab1ae 100644 --- a/crates/telio-model/src/event.rs +++ b/crates/telio-model/src/event.rs @@ -250,6 +250,8 @@ mod tests { endpoint: Some(SocketAddr::new("127.0.0.1".parse().unwrap(), 8080)), hostname: Some(String::from("example.com")), allow_incoming_connections: false, + allow_peer_traffic_routing: false, + allow_peer_local_network_access: false, allow_peer_send_files: false, path: crate::features::PathType::Relay, allow_multicast: false, @@ -300,6 +302,8 @@ mod tests { r#""is_exit":true,"is_vpn":true,"ip_addresses":["127.0.0.1"],"allowed_ips":["127.0.0.1/32"],"#, r#""endpoint":"127.0.0.1:8080","hostname":"example.com","#, r#""allow_incoming_connections":false,"#, + r#""allow_peer_traffic_routing":false,"#, + r#""allow_peer_local_network_access":false,"#, r#""allow_peer_send_files":false,"#, r#""path":"relay","#, r#""allow_multicast":false,"#, diff --git a/crates/telio-model/src/features.rs b/crates/telio-model/src/features.rs index 23c023810..9c6d87f66 100644 --- a/crates/telio-model/src/features.rs +++ b/crates/telio-model/src/features.rs @@ -3,6 +3,7 @@ use std::{collections::HashSet, fmt}; +use ipnet::Ipv4Net; use num_enum::{IntoPrimitive, TryFromPrimitive}; use serde::{de::IntoDeserializer, Deserialize, Deserializer, Serialize}; use smart_default::SmartDefault; @@ -353,6 +354,9 @@ pub struct FeatureFirewall { /// Turns on connection resets upon VPN server change (Deprecated alias for neptun_reset_conns) #[serde(default)] pub boringtun_reset_conns: bool, + /// Customizable private IP range to treat certain private IP ranges + /// as public IPs for testing purposes. + pub custom_private_ip_range: Option, } /// Turns on post quantum VPN tunnel @@ -540,7 +544,8 @@ mod tests { "nicknames": true, "firewall": { "neptun_reset_conns": true, - "boringtun_reset_conns": true + "boringtun_reset_conns": true, + "custom_private_ip_range": null }, "flush_events_on_stop_timeout_seconds": 15, "post_quantum_vpn": { @@ -630,6 +635,7 @@ mod tests { firewall: FeatureFirewall { neptun_reset_conns: true, boringtun_reset_conns: true, + custom_private_ip_range: None, }, flush_events_on_stop_timeout_seconds: Some(15), post_quantum_vpn: FeaturePostQuantumVPN { diff --git a/crates/telio-model/src/mesh.rs b/crates/telio-model/src/mesh.rs index b31ee84c7..bb8f6919d 100644 --- a/crates/telio-model/src/mesh.rs +++ b/crates/telio-model/src/mesh.rs @@ -40,6 +40,10 @@ pub struct Node { pub hostname: Option, /// Flag to control whether the Node allows incoming connections pub allow_incoming_connections: bool, + /// Flag to control whether the Node allows routing through + pub allow_peer_traffic_routing: bool, + /// Flag to control whether the Node allows incoming local area access + pub allow_peer_local_network_access: bool, /// Flag to control whether the Node allows incoming files pub allow_peer_send_files: bool, /// Connection type in the network mesh (through Relay or hole punched directly) @@ -134,6 +138,8 @@ impl From<&Peer> for Node { .unwrap_or_default(), hostname: Some(peer.hostname.0.to_owned().to_string()), allow_incoming_connections: peer.allow_incoming_connections, + allow_peer_traffic_routing: peer.allow_peer_traffic_routing, + allow_peer_local_network_access: peer.allow_peer_local_network_access, allow_peer_send_files: peer.allow_peer_send_files, ..Default::default() } diff --git a/crates/telio-utils/src/lru_cache.rs b/crates/telio-utils/src/lru_cache.rs index 07d525bea..41706c04c 100644 --- a/crates/telio-utils/src/lru_cache.rs +++ b/crates/telio-utils/src/lru_cache.rs @@ -184,7 +184,7 @@ impl LruCache { /// Gets the given key’s corresponding entry in the map for in-place manipulation. #[inline(always)] - pub fn entry(&mut self, key: Key) -> Entry<'_, Key, Value> { + pub fn entry(&mut self, key: Key, update_last_access_time: bool) -> Entry<'_, Key, Value> { let hash = self.map.hasher().hash_one(&key); match self.map.raw_entry_mut().from_key_hashed_nocheck(hash, &key) { RawEntryMut::Occupied(mut e) => { @@ -198,7 +198,9 @@ impl LruCache { max_map_size: self.capacity, }); } - Self::update_last_time(&mut e, now); + if update_last_access_time { + Self::update_last_time(&mut e, now) + } Entry::Occupied(OccupiedEntry { key, hash, @@ -492,7 +494,7 @@ mod tests { assert_eq!(lru_cache.len(), 1); advance_time_by_ms(300); - lru_cache.entry(0); + lru_cache.entry(0, true); advance_time_by_ms(300); assert_eq!(Some(&0), lru_cache.peek(&0)); advance_time_by_ms(300); @@ -631,7 +633,7 @@ mod tests { old.remove(&k); }, Op::GetUsingEntry(k) => { - let new_val = match new.entry(k) { + let new_val = match new.entry(k, true) { Entry::Occupied(mut e) => { Some(*e.get_mut()) }, @@ -642,7 +644,7 @@ mod tests { assert_eq!(new_val, old.get(&k).copied()); }, Op::GetMutUsingEntry(k) => { - if let Entry::Occupied(mut e) = new.entry(k) { + if let Entry::Occupied(mut e) = new.entry(k, true) { *e.get_mut() += 1; } if let Some(v) = old.get_mut(&k) { diff --git a/crates/telio-wg/src/wg.rs b/crates/telio-wg/src/wg.rs index 393010360..a6b562f3e 100644 --- a/crates/telio-wg/src/wg.rs +++ b/crates/telio-wg/src/wg.rs @@ -278,6 +278,7 @@ impl DynamicWg { /// ``` /// use std::{sync::Arc, io}; /// use telio_firewall::firewall::{StatefullFirewall, Firewall}; + /// use telio_model::features::FeatureFirewall; /// use telio_sockets::{native::NativeSocket, Protector, SocketPool, Protect}; /// use telio_task::io::Chan; /// pub use telio_wg::{AdapterType, DynamicWg, Tun, Io, Config}; @@ -297,7 +298,7 @@ impl DynamicWg { /// fn set_tunnel_interface(&self, interface: u64); /// } /// } - /// let firewall = Arc::new(StatefullFirewall::new(true, false)); + /// let firewall = Arc::new(StatefullFirewall::new(true, FeatureFirewall::default(),)); /// let firewall_filter_inbound_packets = { /// let fw = firewall.clone(); /// move |peer: &[u8; 32], packet: &[u8]| fw.process_inbound_packet(peer, packet) diff --git a/nat-lab/tests/mesh_api.py b/nat-lab/tests/mesh_api.py index 2e0be517c..603a186d6 100644 --- a/nat-lab/tests/mesh_api.py +++ b/nat-lab/tests/mesh_api.py @@ -86,15 +86,21 @@ class NicknameCollisionError(NodeError): class FirewallRule: allow_incoming_connections: bool + allow_peer_traffic_routing: bool + allow_peer_local_network_access: bool allow_peer_send_files: bool def __init__( self, allow_incoming_connections: bool = False, + allow_peer_traffic_routing: bool = False, + allow_peer_local_network_access: bool = False, allow_peer_send_files: bool = False, ): self.allow_incoming_connections = allow_incoming_connections self.allow_peer_send_files = allow_peer_send_files + self.allow_peer_local_network_access = allow_peer_local_network_access + self.allow_peer_traffic_routing = allow_peer_traffic_routing def __str__(self): return pprint.pformat(vars(self)) @@ -168,6 +174,8 @@ def to_peer_config_for_node(self, node) -> Peer: ), is_local=node.is_local and self.is_local, allow_incoming_connections=firewall_config.allow_incoming_connections, + allow_peer_traffic_routing=firewall_config.allow_peer_traffic_routing, + allow_peer_local_network_access=firewall_config.allow_peer_local_network_access, allow_peer_send_files=firewall_config.allow_peer_send_files, allow_multicast=True, peer_allows_multicast=True, @@ -178,9 +186,12 @@ def set_peer_firewall_settings( node_id: str, allow_incoming_connections: bool = False, allow_peer_send_files: bool = False, + allow_peer_traffic_routing: bool = False, ) -> None: self.firewall_rules[node_id] = FirewallRule( - allow_incoming_connections, allow_peer_send_files + allow_incoming_connections, + allow_peer_send_files=allow_peer_send_files, + allow_peer_traffic_routing=allow_peer_traffic_routing, ) def get_firewall_config(self, node_id: str) -> FirewallRule: @@ -317,12 +328,15 @@ def default_config_two_nodes( beta_is_local: bool = False, alpha_ip_stack: IPStack = IPStack.IPv4, beta_ip_stack: IPStack = IPStack.IPv4, + allow_peer_traffic_routing: bool = False, ) -> Tuple[Node, Node]: alpha, beta, *_ = self.config_dynamic_nodes( [(alpha_is_local, alpha_ip_stack), (beta_is_local, beta_ip_stack)] ) alpha.set_peer_firewall_settings(beta.id, True) - beta.set_peer_firewall_settings(alpha.id, True) + beta.set_peer_firewall_settings( + alpha.id, True, allow_peer_traffic_routing=allow_peer_traffic_routing + ) return alpha, beta def default_config_three_nodes( diff --git a/nat-lab/tests/test_dns_through_exit.py b/nat-lab/tests/test_dns_through_exit.py index 25713c2a1..abd2b9a01 100644 --- a/nat-lab/tests/test_dns_through_exit.py +++ b/nat-lab/tests/test_dns_through_exit.py @@ -2,9 +2,9 @@ import config import pytest from contextlib import AsyncExitStack -from helpers import setup_mesh_nodes, SetupParameters +from helpers import setup_api, setup_mesh_nodes, SetupParameters from typing import List, Tuple -from utils.bindings import TelioAdapterType +from utils.bindings import default_features, TelioAdapterType from utils.connection_util import generate_connection_tracker_config, ConnectionTag from utils.dns import query_dns from utils.router import IPStack @@ -96,6 +96,7 @@ ConnectionTag.DOCKER_CONE_CLIENT_2, derp_1_limits=(1, 1), ), + features=default_features(enable_firewall=("10.0.0.0/8", False)), ) ) ], @@ -124,8 +125,17 @@ async def test_dns_through_exit( else config.LIBTELIO_DNS_IPV6 ) + api, (alpha, beta) = setup_api( + [(False, alpha_setup_params.ip_stack), (False, beta_setup_params.ip_stack)] + ) + beta.set_peer_firewall_settings( + alpha.id, allow_incoming_connections=True, allow_peer_traffic_routing=True + ) + env = await setup_mesh_nodes( - exit_stack, [alpha_setup_params, beta_setup_params] + exit_stack, + [alpha_setup_params, beta_setup_params], + provided_api=api, ) _, exit_node = env.nodes diff --git a/nat-lab/tests/test_events.py b/nat-lab/tests/test_events.py index 0b382382e..c3cbdc4ee 100644 --- a/nat-lab/tests/test_events.py +++ b/nat-lab/tests/test_events.py @@ -9,6 +9,7 @@ from utils import stun from utils.bindings import ( features_with_endpoint_providers, + default_features, EndpointProvider, PathType, TelioNode, @@ -435,6 +436,7 @@ async def test_event_content_vpn_connection( derp_1_limits=(1, 1), stun_limits=(1, 2), ), + features=default_features(enable_firewall=("10.0.0.0/8", False)), ) ) ], @@ -449,6 +451,9 @@ async def test_event_content_exit_through_peer( alpha.nickname = "alpha" beta.nickname = "BETA" alpha.set_peer_firewall_settings(beta.id) + beta.set_peer_firewall_settings( + alpha.id, allow_incoming_connections=True, allow_peer_traffic_routing=True + ) env = await setup_mesh_nodes( exit_stack, [alpha_setup_params, beta_setup_params], provided_api=api ) diff --git a/nat-lab/tests/test_features_builder.py b/nat-lab/tests/test_features_builder.py index 71cc1fffd..ed6f7c7b1 100644 --- a/nat-lab/tests/test_features_builder.py +++ b/nat-lab/tests/test_features_builder.py @@ -22,6 +22,31 @@ def test_telio_features_builder_empty(): assert expect == built +def test_telio_features_builder_firewall(): + built = FeaturesDefaultsBuilder().enable_firewall("1.2.3.4/10", False).build() + json = """ + { + "lana": null, + "nurse": null, + "firewall": { + "custom_private_ip_range": "1.2.3.4/10", + "neptun_reset_conns": false + }, + "direct": null, + "derp": null, + "link_detection": null, + "pmtu_discovery": null, + "flush_events_on_stop_timeout_seconds": null, + "multicast": false, + "ipv6": false, + "nicknames": false + } + """ + expect = deserialize_feature_config(json) + + assert expect == built + + def test_telio_features_builder_lana(): built = FeaturesDefaultsBuilder().enable_lana("some/path", False).build() json = """ @@ -51,7 +76,6 @@ def test_telio_features_builder_all_defaults(): FeaturesDefaultsBuilder() .enable_nurse() .enable_direct() - .enable_firewall_connection_reset() .enable_battery_saving_defaults() .enable_link_detection() .enable_pmtu_discovery() @@ -79,9 +103,6 @@ def test_telio_features_builder_all_defaults(): "derp_keepalive": 125 }, "nurse": {}, - "firewall": { - "neptun_reset_conns": true - }, "direct": {}, "link_detection": {}, "pmtu_discovery": {}, diff --git a/nat-lab/tests/test_lana.py b/nat-lab/tests/test_lana.py index 4955216d9..c9f828804 100644 --- a/nat-lab/tests/test_lana.py +++ b/nat-lab/tests/test_lana.py @@ -126,12 +126,15 @@ def build_telio_features( - initial_heartbeat_interval: int = 300, rtt_interval: int = RTT_INTERVAL + initial_heartbeat_interval: int = 300, + rtt_interval: int = RTT_INTERVAL, + custom_ip_range: str = "", ) -> Features: features = default_features( enable_lana=(CONTAINER_EVENT_PATH, False), enable_direct=True, enable_nurse=True, + enable_firewall=(custom_ip_range, False), ) assert features.direct features.direct.providers = [EndpointProvider.STUN] @@ -1280,7 +1283,11 @@ async def test_lana_with_meshnet_exit_node( api = API() (alpha, beta) = api.default_config_two_nodes( - True, True, alpha_ip_stack=alpha_ip_stack, beta_ip_stack=beta_ip_stack + True, + True, + alpha_ip_stack=alpha_ip_stack, + beta_ip_stack=beta_ip_stack, + allow_peer_traffic_routing=True, ) (connection_alpha, alpha_conn_tracker) = await exit_stack.enter_async_context( new_connection_with_conn_tracker( @@ -1291,6 +1298,7 @@ async def test_lana_with_meshnet_exit_node( ), ) ) + (connection_beta, beta_conn_tracker) = await exit_stack.enter_async_context( new_connection_with_conn_tracker( ConnectionTag.DOCKER_OPEN_INTERNET_CLIENT_DUAL_STACK, @@ -1319,8 +1327,8 @@ async def test_lana_with_meshnet_exit_node( beta, connection_alpha, connection_beta, - build_telio_features(), - build_telio_features(), + build_telio_features(custom_ip_range="10.0.0.0/8"), + build_telio_features(custom_ip_range="10.0.0.0/8"), ) await asyncio.gather( diff --git a/nat-lab/tests/test_mesh_api.py b/nat-lab/tests/test_mesh_api.py index f733b76d3..d39f7a998 100644 --- a/nat-lab/tests/test_mesh_api.py +++ b/nat-lab/tests/test_mesh_api.py @@ -42,6 +42,8 @@ def test_to_peer_config(self) -> None: ), is_local=False, allow_incoming_connections=False, + allow_peer_traffic_routing=False, + allow_peer_local_network_access=False, allow_peer_send_files=False, allow_multicast=True, peer_allows_multicast=True, diff --git a/nat-lab/tests/test_mesh_exit_through_peer.py b/nat-lab/tests/test_mesh_exit_through_peer.py index 3fcb65146..eb4871b7a 100644 --- a/nat-lab/tests/test_mesh_exit_through_peer.py +++ b/nat-lab/tests/test_mesh_exit_through_peer.py @@ -2,11 +2,17 @@ import config import pytest from contextlib import AsyncExitStack -from helpers import setup_mesh_nodes, SetupParameters +from helpers import setup_api, setup_mesh_nodes, SetupParameters from mesh_api import API from telio import Client from utils import testing, stun -from utils.bindings import PathType, NodeState, RelayState, TelioAdapterType +from utils.bindings import ( + default_features, + PathType, + NodeState, + RelayState, + TelioAdapterType, +) from utils.connection_util import ( generate_connection_tracker_config, ConnectionTag, @@ -106,6 +112,7 @@ derp_1_limits=(1, 1), stun_limits=(1, 2), ), + features=default_features(enable_firewall=("10.0.0.0/8", False)), ) ) ], @@ -117,8 +124,18 @@ async def test_mesh_exit_through_peer( ) -> None: async with AsyncExitStack() as exit_stack: beta_setup_params.ip_stack = exit_ip_stack + + api, (alpha, beta) = setup_api( + [(False, alpha_setup_params.ip_stack), (False, beta_setup_params.ip_stack)] + ) + beta.set_peer_firewall_settings( + alpha.id, allow_incoming_connections=True, allow_peer_traffic_routing=True + ) + env = await setup_mesh_nodes( - exit_stack, [alpha_setup_params, beta_setup_params] + exit_stack, + [alpha_setup_params, beta_setup_params], + provided_api=api, ) _, beta = env.nodes @@ -234,7 +251,9 @@ async def test_ipv6_exit_node( api = API() (alpha, beta) = api.default_config_two_nodes( - alpha_ip_stack=IPStack.IPv4v6, beta_ip_stack=exit_ip_stack + alpha_ip_stack=IPStack.IPv4v6, + beta_ip_stack=exit_ip_stack, + allow_peer_traffic_routing=True, ) (connection_alpha, alpha_conn_tracker) = await exit_stack.enter_async_context( new_connection_with_conn_tracker( diff --git a/nat-lab/tests/test_mesh_firewall.py b/nat-lab/tests/test_mesh_firewall.py index 8f5bfc0bc..96e9868cb 100644 --- a/nat-lab/tests/test_mesh_firewall.py +++ b/nat-lab/tests/test_mesh_firewall.py @@ -8,7 +8,7 @@ from mesh_api import Node from typing import Tuple, Optional from utils import testing, stun -from utils.bindings import TelioAdapterType +from utils.bindings import default_features, Features, TelioAdapterType from utils.connection_tracker import ( FiveTuple, TCPStateSequence, @@ -40,6 +40,9 @@ def _setup_params( connection_tag: ConnectionTag, adapter_type_override: Optional[TelioAdapterType] = None, stun_limits=(0, 0), + features: Features = default_features( + enable_firewall=("10.0.0.0/8", False), + ), ) -> SetupParameters: return SetupParameters( connection_tag=connection_tag, @@ -49,6 +52,7 @@ def _setup_params( derp_1_limits=(1, 1), stun_limits=stun_limits, ), + features=features, ) @@ -259,10 +263,12 @@ async def test_blocking_incoming_connections_from_exit_node() -> None: ConnectionTag.DOCKER_CONE_CLIENT_1, TelioAdapterType.NEP_TUN, stun_limits=(1, 1), + features=default_features(enable_firewall=("10.0.0.0/8", False)), ), _setup_params( ConnectionTag.DOCKER_CONE_CLIENT_2, stun_limits=(1, 5), + features=default_features(enable_firewall=("10.0.0.0/8", False)), ), ], ) @@ -305,7 +311,9 @@ async def get_external_ips(): alpha.set_peer_firewall_settings(exit_node.id, allow_incoming_connections=True) await client_alpha.set_meshnet_config(api.get_meshnet_config(alpha.id)) - exit_node.set_peer_firewall_settings(alpha.id, allow_incoming_connections=True) + exit_node.set_peer_firewall_settings( + alpha.id, allow_incoming_connections=True, allow_peer_traffic_routing=True + ) await client_exit_node.set_meshnet_config(api.get_meshnet_config(exit_node.id)) # Ping should again work both ways diff --git a/nat-lab/tests/test_vpn.py b/nat-lab/tests/test_vpn.py index bd7b99876..744d99bc3 100644 --- a/nat-lab/tests/test_vpn.py +++ b/nat-lab/tests/test_vpn.py @@ -335,7 +335,7 @@ async def test_vpn_reconnect( connection_tag=ConnectionTag.DOCKER_CONE_CLIENT_1, adapter_type_override=TelioAdapterType.NEP_TUN, ip_stack=IPStack.IPv4, - features=default_features(enable_firewall_connection_reset=True), + features=default_features(enable_firewall=("10.0.0.0/8", True)), ) ), # TODO(msz): IPv6 public server, it doesn't work with the current VPN implementation @@ -447,7 +447,7 @@ async def connect( connection_tag=ConnectionTag.DOCKER_CONE_CLIENT_1, adapter_type_override=TelioAdapterType.NEP_TUN, ip_stack=IPStack.IPv4, - features=default_features(enable_firewall_connection_reset=True), + features=default_features(enable_firewall=("10.0.0.0/8", True)), ) ), pytest.param( @@ -455,7 +455,7 @@ async def connect( connection_tag=ConnectionTag.MAC_VM, adapter_type_override=TelioAdapterType.NEP_TUN, ip_stack=IPStack.IPv4, - features=default_features(enable_firewall_connection_reset=True), + features=default_features(enable_firewall=("10.0.0.0/8", True)), ), marks=pytest.mark.mac, ), diff --git a/nat-lab/tests/utils/bindings.py b/nat-lab/tests/utils/bindings.py index d7520c019..d81341c09 100644 --- a/nat-lab/tests/utils/bindings.py +++ b/nat-lab/tests/utils/bindings.py @@ -15,7 +15,7 @@ def features_with_endpoint_providers( def default_features( enable_lana: Optional[Tuple[str, bool]] = None, enable_nurse: bool = False, - enable_firewall_connection_reset: bool = False, + enable_firewall: Optional[Tuple[str, bool]] = None, enable_direct: bool = False, enable_ipv6: bool = False, enable_nicknames: bool = False, @@ -30,8 +30,9 @@ def default_features( if enable_nurse: features_builder = features_builder.enable_nurse() - if enable_firewall_connection_reset: - features_builder = features_builder.enable_firewall_connection_reset() + if enable_firewall is not None: + ips, reset_conns = enable_firewall + features_builder = features_builder.enable_firewall(ips, reset_conns) if enable_direct: features_builder = features_builder.enable_direct() if enable_ipv6: @@ -66,6 +67,8 @@ def telio_node( # pylint: disable=dangerous-default-value path: PathType = PathType.RELAY, allow_incoming_connections: bool = False, allow_peer_send_files: bool = False, + allow_peer_traffic_routing: bool = False, + allow_peer_local_network_access: bool = False, link_state: Optional[LinkState] = None, allow_multicast: bool = False, peer_allows_multicast: bool = False, @@ -84,6 +87,8 @@ def telio_node( # pylint: disable=dangerous-default-value path=path, allow_incoming_connections=allow_incoming_connections, allow_peer_send_files=allow_peer_send_files, + allow_peer_traffic_routing=allow_peer_traffic_routing, + allow_peer_local_network_access=allow_peer_local_network_access, link_state=link_state, allow_multicast=allow_multicast, peer_allows_multicast=peer_allows_multicast, diff --git a/src/device.rs b/src/device.rs index aced51305..240c05077 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1043,10 +1043,7 @@ impl Runtime { features: Features, protect: Option>, ) -> Result { - let neptun_reset_conns = - features.firewall.neptun_reset_conns || features.firewall.boringtun_reset_conns; - - let firewall = Arc::new(StatefullFirewall::new(features.ipv6, neptun_reset_conns)); + let firewall = Arc::new(StatefullFirewall::new(features.ipv6, features.firewall)); let firewall_filter_inbound_packets = { let fw = firewall.clone(); @@ -1056,7 +1053,7 @@ impl Runtime { let fw = firewall.clone(); move |peer: &[u8; 32], packet: &[u8]| fw.process_outbound_packet(peer, packet) }; - let firewall_reset_connections = if neptun_reset_conns { + let firewall_reset_connections = if features.firewall.neptun_reset_conns { let fw = firewall.clone(); let cb = move |exit_pubkey: &PublicKey, exit_ipv4: Ipv4Addr, @@ -2103,9 +2100,7 @@ impl Runtime { async fn disconnect_exit_node(&mut self, node_key: &PublicKey) -> Result { match self.requested_state.exit_node.as_ref() { Some(exit_node) if &exit_node.public_key == node_key => { - self.entities - .firewall - .remove_from_peer_whitelist(exit_node.public_key); + self.entities.firewall.remove_vpn_peer(); self.disconnect_exit_nodes().boxed().await } _ => Err(Error::InvalidNode), @@ -2231,6 +2226,8 @@ impl Runtime { endpoint, hostname: Some(meshnet_peer.base.hostname.0.clone().to_string()), allow_incoming_connections: meshnet_peer.allow_incoming_connections, + allow_peer_traffic_routing: meshnet_peer.allow_peer_traffic_routing, + allow_peer_local_network_access: meshnet_peer.allow_peer_local_network_access, allow_peer_send_files: meshnet_peer.allow_peer_send_files, path: path_type, allow_multicast: meshnet_peer.allow_multicast, @@ -2597,6 +2594,8 @@ fn node_from_exit_node(exit_node: &ExitNode) -> Node { endpoint: exit_node.endpoint, hostname: None, allow_incoming_connections: false, + allow_peer_traffic_routing: false, + allow_peer_local_network_access: false, allow_peer_send_files: false, path: PathType::Direct, allow_multicast: false, diff --git a/src/device/wg_controller.rs b/src/device/wg_controller.rs index 3311fe09e..e7114c234 100644 --- a/src/device/wg_controller.rs +++ b/src/device/wg_controller.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use std::time::Duration; use telio_crypto::PublicKey; use telio_dns::DnsResolver; -use telio_firewall::firewall::{Firewall, FILE_SEND_PORT}; +use telio_firewall::firewall::{Firewall, Permissions, FILE_SEND_PORT}; use telio_model::constants::{VPN_EXTERNAL_IPV4, VPN_INTERNAL_IPV4, VPN_INTERNAL_IPV6}; use telio_model::features::Features; use telio_model::mesh::{LinkState, NodeState}; @@ -37,6 +37,8 @@ pub enum Error { BadAllowedIps, #[error("Peer not found error")] PeerNotFound, + #[error("Meshnet IP address not set")] + IpNotSet, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -125,7 +127,23 @@ pub async fn consolidate_wg_state( } else { None }; - consolidate_firewall(requested_state, &*entities.firewall, starcast_pub_key).await?; + + let dns_pubkey = entities + .dns + .as_ref() + .lock() + .await + .resolver + .as_ref() + .map(|resolver| resolver.public_key()); + + consolidate_firewall( + requested_state, + &*entities.firewall, + starcast_pub_key, + dns_pubkey, + ) + .await?; Ok(()) } @@ -580,30 +598,54 @@ fn iter_peers( .flatten() } +fn upsert_peer_whitelist( + requested_state: &RequestedState, + firewall: &F, + permission: Permissions, +) { + let from_keys_peer_whitelist: HashSet = firewall + .get_peer_whitelist(permission) + .iter() + .copied() + .collect(); + + // Build a list of peers expected to be peer-whitelisted + let to_keys_peer_whitelist: HashSet = iter_peers(requested_state) + .filter(|p| match permission { + Permissions::IncomingConnections => p.allow_incoming_connections, + Permissions::LocalAreaConnections => p.allow_peer_local_network_access, + Permissions::RoutingConnections => p.allow_peer_traffic_routing, + }) + .map(|p| p.public_key) + .collect(); + + // Consolidate peer-whitelist + let delete_keys = &from_keys_peer_whitelist - &to_keys_peer_whitelist; + let add_keys = &to_keys_peer_whitelist - &from_keys_peer_whitelist; + for key in delete_keys { + firewall.remove_from_peer_whitelist(key, permission); + } + for key in add_keys { + firewall.add_to_peer_whitelist(key, permission); + } +} + async fn consolidate_firewall( requested_state: &RequestedState, firewall: &F, starcast_vpeer_pubkey: Option, + dns_pubkey: Option, ) -> Result { - let from_keys_peer_whitelist: HashSet = - firewall.get_peer_whitelist().iter().copied().collect(); let from_keys_ports_whitelist: HashSet = firewall.get_port_whitelist().keys().copied().collect(); - // Build a list of peers expected to be peer-whitelisted according - // to allow_incoming_connections permission - let mut to_keys_peer_whitelist: HashSet = iter_peers(requested_state) - .filter(|p| p.allow_incoming_connections) - .map(|p| p.public_key) - .collect(); - // VPN peer must always be peer-whitelisted if let Some(exit_node) = &requested_state.exit_node { let is_vpn_exit_node = !iter_peers(requested_state).any(|p| p.public_key == exit_node.public_key); if is_vpn_exit_node { - to_keys_peer_whitelist.insert(exit_node.public_key); + firewall.add_vpn_peer(exit_node.public_key); } } @@ -614,17 +656,18 @@ async fn consolidate_firewall( .map(|p| p.public_key) .collect(); - // Consolidate peer-whitelist - let delete_keys = &from_keys_peer_whitelist - &to_keys_peer_whitelist; - let mut add_keys = &to_keys_peer_whitelist - &from_keys_peer_whitelist; - if let Some(pub_key) = starcast_vpeer_pubkey { - add_keys.insert(pub_key); + // Upsert peer-whitelists + for permission in Permissions::VALUES { + upsert_peer_whitelist(requested_state, firewall, permission); } - for key in delete_keys { - firewall.remove_from_peer_whitelist(key); + + if let Some(key) = starcast_vpeer_pubkey { + firewall.add_to_peer_whitelist(key, Permissions::RoutingConnections); + firewall.add_to_peer_whitelist(key, Permissions::IncomingConnections); } - for key in add_keys { - firewall.add_to_peer_whitelist(key); + + if let Some(key) = dns_pubkey { + firewall.add_to_peer_whitelist(key, Permissions::RoutingConnections); } // Consolidate port-whitelist @@ -637,6 +680,12 @@ async fn consolidate_firewall( firewall.add_to_port_whitelist(key, FILE_SEND_PORT); } + // Meshnet config can be None in the beginning when this method + // is called. + if let Some(config) = &requested_state.meshnet_config { + // Save local node ip addresses + firewall.set_ip_addresses(config.this.ip_addresses.clone().ok_or(Error::IpNotSet)?); + } Ok(()) } @@ -1282,6 +1331,8 @@ mod tests { use tokio::time::Instant; type AllowIncomingConnections = bool; + type AllowLocalAreaAccess = bool; + type AllowRouting = bool; type AllowPeerSendFiles = bool; type AllowedIps = Vec; @@ -1419,6 +1470,8 @@ mod tests { PublicKey, AllowedIps, AllowIncomingConnections, + AllowLocalAreaAccess, + AllowRouting, AllowPeerSendFiles, )>, ) -> RequestedState { @@ -1426,6 +1479,13 @@ mod tests { let mut meshnet_config = Config { peers: Some(vec![]), + this: PeerBase { + ip_addresses: Some(vec![ + (IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + ]), + ..Default::default() + }, ..Default::default() }; @@ -1438,7 +1498,9 @@ mod tests { ..Default::default() }, allow_incoming_connections: i.2, - allow_peer_send_files: i.3, + allow_peer_local_network_access: i.3, + allow_peer_traffic_routing: i.4, + allow_peer_send_files: i.5, ..Default::default() }; peers.push(peer); @@ -1453,19 +1515,33 @@ mod tests { fn expect_add_to_peer_whitelist(firewall: &mut MockFirewall, pub_key: PublicKey) { firewall .expect_add_to_peer_whitelist() - .with(eq(pub_key)) + .with(eq(pub_key), eq(Permissions::IncomingConnections)) .once() .return_const(()); } - fn expect_remove_from_peer_whitelist(firewall: &mut MockFirewall, pub_key: PublicKey) { + fn expect_add_to_vpeer_whitelist(firewall: &mut MockFirewall, pub_key: PublicKey) { firewall - .expect_remove_from_peer_whitelist() - .with(eq(pub_key)) + .expect_add_to_peer_whitelist() + .with(eq(pub_key), eq(Permissions::IncomingConnections)) + .once() + .return_const(()); + firewall + .expect_add_to_peer_whitelist() + .with(eq(pub_key), eq(Permissions::RoutingConnections)) .once() .return_const(()); } + fn expect_remove_from_peer_whitelist(firewall: &mut MockFirewall, pub_key: PublicKey) { + for permission in Permissions::VALUES { + firewall + .expect_remove_from_peer_whitelist() + .with(eq(pub_key), eq(permission)) + .return_const(()); + } + } + fn expect_add_to_port_whitelist(firewall: &mut MockFirewall, pub_key: PublicKey) { firewall .expect_add_to_port_whitelist() @@ -1485,7 +1561,8 @@ mod tests { fn expect_get_peer_whitelist(firewall: &mut MockFirewall, pub_keys: Vec) { firewall .expect_get_peer_whitelist() - .return_once(move || pub_keys.into_iter().collect()); + .times(3) + .returning(move |_| pub_keys.clone().into_iter().collect()); } fn expect_get_port_whitelist(firewall: &mut MockFirewall, pub_keys: Vec) { @@ -1505,21 +1582,23 @@ mod tests { let pub_key_4 = SecretKey::gen().public(); let requested_state = create_requested_state(vec![ - (pub_key_1, vec![], true, true), - (pub_key_2, vec![], true, false), - (pub_key_3, vec![], false, true), - (pub_key_4, vec![], false, false), + (pub_key_1, vec![], true, false, false, true), + (pub_key_2, vec![], true, false, false, false), + (pub_key_3, vec![], false, false, false, true), + (pub_key_4, vec![], false, false, false, false), ]); firewall .expect_get_peer_whitelist() - .return_once(Default::default); + .times(3) + .returning(|_| Default::default()); firewall .expect_get_port_whitelist() .return_once(Default::default); - expect_add_to_peer_whitelist(&mut firewall, pub_key_starcast_vpeer); + expect_add_to_vpeer_whitelist(&mut firewall, pub_key_starcast_vpeer); + firewall.expect_set_ip_addresses().once().return_const(()); expect_add_to_peer_whitelist(&mut firewall, pub_key_1); expect_add_to_port_whitelist(&mut firewall, pub_key_1); @@ -1528,9 +1607,14 @@ mod tests { expect_add_to_port_whitelist(&mut firewall, pub_key_3); - consolidate_firewall(&requested_state, &firewall, Some(pub_key_starcast_vpeer)) - .await - .unwrap(); + consolidate_firewall( + &requested_state, + &firewall, + Some(pub_key_starcast_vpeer), + None, + ) + .await + .unwrap(); } #[tokio::test] @@ -1544,10 +1628,10 @@ mod tests { let pub_key_4 = SecretKey::gen().public(); let requested_state = create_requested_state(vec![ - (pub_key_1, vec![], true, true), - (pub_key_2, vec![], true, false), - (pub_key_3, vec![], false, true), - (pub_key_4, vec![], false, false), + (pub_key_1, vec![], true, true, true, true), + (pub_key_2, vec![], true, true, true, false), + (pub_key_3, vec![], false, false, false, true), + (pub_key_4, vec![], false, false, false, false), ]); expect_get_peer_whitelist( @@ -1564,7 +1648,7 @@ mod tests { .expect_get_port_whitelist() .return_once(Default::default); - expect_add_to_peer_whitelist(&mut firewall, pub_key_starcast_vpeer); + expect_add_to_vpeer_whitelist(&mut firewall, pub_key_starcast_vpeer); expect_remove_from_port_whitelist(&mut firewall, pub_key_2); @@ -1573,9 +1657,15 @@ mod tests { expect_remove_from_peer_whitelist(&mut firewall, pub_key_4); expect_remove_from_port_whitelist(&mut firewall, pub_key_4); - consolidate_firewall(&requested_state, &firewall, Some(pub_key_starcast_vpeer)) - .await - .unwrap(); + firewall.expect_set_ip_addresses().once().return_const(()); + consolidate_firewall( + &requested_state, + &firewall, + Some(pub_key_starcast_vpeer), + None, + ) + .await + .unwrap(); } #[tokio::test] @@ -1586,25 +1676,32 @@ mod tests { let pub_key_1 = SecretKey::gen().public(); let pub_key_2 = SecretKey::gen().public(); - let mut requested_state = create_requested_state(vec![(pub_key_1, vec![], false, false)]); + let mut requested_state = + create_requested_state(vec![(pub_key_1, vec![], false, false, false, false)]); requested_state.exit_node = Some(ExitNode { public_key: pub_key_2, ..Default::default() }); - expect_add_to_peer_whitelist(&mut firewall, pub_key_starcast_vpeer); + expect_add_to_vpeer_whitelist(&mut firewall, pub_key_starcast_vpeer); + + firewall.expect_set_ip_addresses().once().return_const(()); expect_get_peer_whitelist(&mut firewall, vec![pub_key_1]); expect_get_port_whitelist(&mut firewall, vec![]); expect_remove_from_peer_whitelist(&mut firewall, pub_key_1); + firewall.expect_add_vpn_peer().once().return_const(()); - expect_add_to_peer_whitelist(&mut firewall, pub_key_2); - - consolidate_firewall(&requested_state, &firewall, Some(pub_key_starcast_vpeer)) - .await - .unwrap(); + consolidate_firewall( + &requested_state, + &firewall, + Some(pub_key_starcast_vpeer), + None, + ) + .await + .unwrap(); } #[tokio::test] @@ -1614,21 +1711,28 @@ mod tests { let pub_key_starcast_vpeer = SecretKey::gen().public(); let pub_key_1 = SecretKey::gen().public(); - let mut requested_state = create_requested_state(vec![(pub_key_1, vec![], false, false)]); + let mut requested_state = + create_requested_state(vec![(pub_key_1, vec![], false, false, false, false)]); requested_state.exit_node = Some(ExitNode { public_key: pub_key_1, ..Default::default() }); - expect_add_to_peer_whitelist(&mut firewall, pub_key_starcast_vpeer); + firewall.expect_set_ip_addresses().once().return_const(()); + expect_add_to_vpeer_whitelist(&mut firewall, pub_key_starcast_vpeer); expect_get_peer_whitelist(&mut firewall, vec![]); expect_get_port_whitelist(&mut firewall, vec![]); - consolidate_firewall(&requested_state, &firewall, Some(pub_key_starcast_vpeer)) - .await - .unwrap(); + consolidate_firewall( + &requested_state, + &firewall, + Some(pub_key_starcast_vpeer), + None, + ) + .await + .unwrap(); } #[tokio::test] @@ -1639,23 +1743,31 @@ mod tests { let pub_key_starcast_vpeer = SecretKey::gen().public(); let pub_key_1 = SecretKey::gen().public(); - let mut requested_state = create_requested_state(vec![(pub_key_1, vec![], false, false)]); + let mut requested_state = + create_requested_state(vec![(pub_key_1, vec![], false, false, false, false)]); requested_state.exit_node = Some(ExitNode { public_key: pub_key_1, ..Default::default() }); - expect_add_to_peer_whitelist(&mut firewall, pub_key_starcast_vpeer); + expect_add_to_vpeer_whitelist(&mut firewall, pub_key_starcast_vpeer); expect_get_peer_whitelist(&mut firewall, vec![pub_key_1]); expect_get_port_whitelist(&mut firewall, vec![]); expect_remove_from_peer_whitelist(&mut firewall, pub_key_1); - consolidate_firewall(&requested_state, &firewall, Some(pub_key_starcast_vpeer)) - .await - .unwrap(); + firewall.expect_set_ip_addresses().once().return_const(()); + + consolidate_firewall( + &requested_state, + &firewall, + Some(pub_key_starcast_vpeer), + None, + ) + .await + .unwrap(); } struct Fixture { diff --git a/src/ffi/defaults_builder.rs b/src/ffi/defaults_builder.rs index e3f721c8f..1cf22eaf2 100644 --- a/src/ffi/defaults_builder.rs +++ b/src/ffi/defaults_builder.rs @@ -1,9 +1,10 @@ -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; +use ipnet::Ipv4Net; use parking_lot::Mutex; use telio_model::features::{ - FeatureDerp, FeatureLana, FeaturePersistentKeepalive, FeatureValidateKeys, FeatureWireguard, - Features, + FeatureDerp, FeatureFirewall, FeatureLana, FeaturePersistentKeepalive, FeatureValidateKeys, + FeatureWireguard, Features, }; pub struct FeaturesDefaultsBuilder { @@ -65,9 +66,20 @@ impl FeaturesDefaultsBuilder { self } - /// Enable firewall connection resets when NepTUN is enabled - pub fn enable_firewall_connection_reset(self: Arc) -> Arc { - self.config.lock().firewall.neptun_reset_conns = true; + /// Enable firewall connection resets when boringtun is enabled + /// custom_ips are needed only for integration tests as the stun server + /// is in a private_ip and it gets blocked, so in order to circumvent that + /// custom_ips was added as a feature + pub fn enable_firewall( + self: Arc, + custom_ips: String, + neptun_reset_conns: bool, + ) -> Arc { + self.config.lock().firewall = FeatureFirewall { + custom_private_ip_range: Ipv4Net::from_str(&custom_ips).ok(), + neptun_reset_conns, + boringtun_reset_conns: neptun_reset_conns, + }; self } diff --git a/src/lib.rs b/src/lib.rs index 31d7a121b..10d243d02 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,9 @@ pub use telio_firewall; pub use uniffi_libtelio::*; #[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used, unwrap_check)] mod uniffi_libtelio { - use std::net::IpAddr; + use std::net::{IpAddr, SocketAddr}; + + use ipnet::Ipv4Net; use super::crypto::{PublicKey, SecretKey}; use super::*; @@ -156,6 +158,19 @@ mod uniffi_libtelio { } } + impl UniffiCustomTypeConverter for Ipv4Net { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + val.parse() + .map_err(|_| anyhow::anyhow!("Invalid IP address".to_owned())) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } + } + impl UniffiCustomTypeConverter for Ipv4Addr { type Builtin = String; diff --git a/src/libtelio.udl b/src/libtelio.udl index b908dbc98..04457264f 100644 --- a/src/libtelio.udl +++ b/src/libtelio.udl @@ -133,6 +133,9 @@ typedef string IpAddr; [Custom] typedef string IpNet; +[Custom] +typedef string Ipv4Net; + [Custom] typedef string Ipv4Addr; @@ -475,7 +478,7 @@ interface FeaturesDefaultsBuilder { /// Enable firewall connection resets when NepTUN is used [Self=ByArc] - FeaturesDefaultsBuilder enable_firewall_connection_reset(); + FeaturesDefaultsBuilder enable_firewall(string custom_ips, boolean neptun_reset_conns); /// Enable direct connections with defaults; [Self=ByArc] @@ -690,6 +693,8 @@ dictionary FeatureFirewall { boolean neptun_reset_conns; /// Turns on connection resets upon VPN server change (Deprecated alias for neptun_reset_conns) boolean boringtun_reset_conns; + /// Custom private IP range + Ipv4Net? custom_private_ip_range; }; /// Link detection mechanism @@ -766,6 +771,10 @@ dictionary Peer { boolean is_local; /// Flag to control whether the peer allows incoming connections boolean allow_incoming_connections; + /// Flag to control whether the Node allows routing through + boolean allow_peer_traffic_routing; + /// Flag to control whether the Node allows incoming local area access + boolean allow_peer_local_network_access; /// Flag to control whether the peer allows incoming files boolean allow_peer_send_files; /// Flag to control whether we allow multicast messages from the peer @@ -836,6 +845,10 @@ dictionary TelioNode { string? hostname; /// Flag to control whether the Node allows incoming connections boolean allow_incoming_connections; + /// Flag to control whether the Node allows routing through + boolean allow_peer_traffic_routing; + /// Flag to control whether the Node allows incoming local area access + boolean allow_peer_local_network_access; /// Flag to control whether the Node allows incoming files boolean allow_peer_send_files; /// Connection type in the network mesh (through Relay or hole punched directly)