Skip to content

Commit

Permalink
Cleanup for exit modes
Browse files Browse the repository at this point in the history
Removing some extra cruft now that CGNAT mode does not explicitly
assign or keep track of clients <-> external ips
  • Loading branch information
ch-iara committed Feb 18, 2025
1 parent b8071ea commit 3eb4503
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 172 deletions.
27 changes: 4 additions & 23 deletions althea_kernel_interface/src/exit_server_tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use super::KernelInterfaceError;
use crate::ip_addr::{add_ipv4, add_ipv4_mask, delete_ipv4};
use crate::iptables::add_iptables_rule;
use crate::netfilter::{
add_prerouting_chain, delete_forward_rule, delete_postrouting_rule, does_nftables_exist,
init_filter_chain, init_nat_chain, insert_nft_exit_forward_rules,
delete_forward_rule, delete_postrouting_rule, does_nftables_exist, init_filter_chain,
init_nat_chain, insert_nft_exit_forward_rules,
};
use crate::open_tunnel::to_wg_local;
use crate::run_command;
Expand Down Expand Up @@ -368,18 +368,17 @@ pub fn teardown_snat(
Ok(())
}

/// Sets up the CGNAT rules for the exit server run on startup
/// Sets up the CGNAT rules for the exit server run on startup. The actual internal <-> external ip
/// allocation is done by the kernel at random, and clients may be assigned any ip in the given range.
pub fn setup_cgnat(
exit_ip: Ipv4Addr,
mask: u32,
ex_nic: &str,
possible_ips: Vec<Ipv4Addr>,
exit_subnet: Ipv4Network,
internal_subnet: Ipv4Network,
) -> Result<(), Error> {
init_filter_chain()?;
let _ = add_ipv4_mask(exit_ip, mask, ex_nic);
add_prerouting_chain()?;
// get the ip range from first and last in possible ips
let ip_range = format!(
"{}-{}",
Expand Down Expand Up @@ -408,24 +407,6 @@ pub fn setup_cgnat(
ip_range.as_str(),
],
)?;
// nft add rule ip nat prerouting ip daddr 10.0.0.0/24 dnat to 10.0.0.2
run_command(
"nft",
&[
"add",
"rule",
"ip",
"nat",
"prerouting",
"ip",
"daddr",
&format!("{}", exit_subnet),
"counter",
"dnat",
"to",
&format!("{}", exit_ip),
],
)?;
// for each ip in the possible ips range, add it to the external interface
for ip in possible_ips {
add_ipv4(ip, ex_nic)?;
Expand Down
27 changes: 0 additions & 27 deletions althea_kernel_interface/src/netfilter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,33 +91,6 @@ fn create_nat_table() -> Result<(), KernelInterfaceError> {
Ok(())
}

pub fn add_prerouting_chain() -> Result<(), KernelInterfaceError> {
run_command(
"nft",
&[
"create",
"chain",
"ip",
"nat",
"prerouting",
"{",
"type",
"nat",
"hook",
"prerouting",
"priority",
"100",
";",
"policy",
"accept",
";",
"}",
],
)?;

Ok(())
}

fn create_filter_table() -> Result<(), KernelInterfaceError> {
// create the table
run_command("nft", &["create", "table", "ip", "filter"])?;
Expand Down
5 changes: 1 addition & 4 deletions integration_tests/src/cgnat_exit.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use althea_kernel_interface::run_command;
use ipnetwork::Ipv4Network;
use settings::exit::ExitIpv4RoutingSettings;

Expand All @@ -10,9 +9,7 @@ use crate::utils::{
register_all_namespaces_to_exit, test_all_internet_connectivity, test_reach_all, test_routes,
};
use std::net::Ipv4Addr;
use std::str::{from_utf8, FromStr};
use std::thread;
use std::time::Duration;
use std::str::FromStr;

/// Runs a five node fixed network map test scenario, this does basic network setup and tests reachability to
/// all destinations
Expand Down
2 changes: 1 addition & 1 deletion integration_tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ extern crate log;

use std::time::Duration;

pub mod cgnat_exit;
pub mod contract_test;
pub mod debts;
pub mod five_nodes;
Expand All @@ -14,7 +15,6 @@ pub mod payments_althea;
pub mod payments_eth;
pub mod setup_utils;
pub mod snat_exit;
pub mod cgnat_exit;
pub mod utils;

/// The amount of time we wait for a network to stabalize before testing
Expand Down
2 changes: 1 addition & 1 deletion integration_tests/src/setup_utils/namespaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ pub fn setup_ns(spaces: NamespaceInfo, exit_mode: &str) -> Result<(), KernelInte
let veth_exit_to_native = format!("vout-{}-o", name.get_name());
let exit_ip = match exit_mode {
"snat" => "10.0.0.2/24".to_string(),
"cgnat" => "10.0.0.2/24".to_string(),
"cgnat" => "10.0.0.2/29".to_string(),
_ => format!(
"10.0.{}.{}/24",
name.id.to_be_bytes()[0],
Expand Down
122 changes: 21 additions & 101 deletions rita_exit/src/database/ipddr_assignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ pub struct ClientListAnIpAssignmentMap {
/// A map of ipv4 addresses assigned to clients, these are used internally for the wg_exit tunnel
/// and never external, the external ip is determined by the exit's nat settings
internal_ip_assignments: DualMap<Ipv4Addr, Identity>,
/// The external ip for a specific client or set of clients depending on the ipv4 nat mode. Under CGNAT
/// each ip will have multiple fixed clients, under SNAT each ip will have one client
external_ip_assignemnts: HashMap<Ipv4Addr, HashSet<Identity>>,
/// The external ip for a specific client or set of clients depending on the ipv4 nat mode. under SNAT
/// each ip will have one client, under CGNAT multiple clients may share an ip but are not explicitly
/// assigned to any one.
external_ip_assignemnts: HashMap<Ipv4Addr, Identity>,
/// A set of all clients that have been registered with the exit
registered_clients: HashSet<Identity>,
/// A list of clients that have been inactive past WG_INACTIVE_THRESHOLD. in SNAT mode these
Expand Down Expand Up @@ -70,7 +71,7 @@ impl ClientListAnIpAssignmentMap {
}
}

pub fn get_external_ip_assignments(&self) -> &HashMap<Ipv4Addr, HashSet<Identity>> {
pub fn get_external_ip_assignments(&self) -> &HashMap<Ipv4Addr, Identity> {
&self.external_ip_assignemnts
}

Expand Down Expand Up @@ -147,83 +148,18 @@ impl ClientListAnIpAssignmentMap {
Ok(None)
}
ExitIpv4RoutingSettings::CGNAT {
subnet,
static_assignments,
gateway_ipv4,
external_ipv4,
broadcast_ipv4,
static_assignments, ..
} => {
// check static assignmetns first
// only static assignments have a fixed external ip
for id in static_assignments {
if their_record == id.client_id {
// make sure we have assigned this clients external ip. in CGNAT mode static clients just get
// the same ip every time, they don't get that ip exclusively assigned to them, so adding to this
// list is mostly a way to load balance the clients across the available ips including any static assignments
// in that count.
match self.external_ip_assignemnts.get_mut(&id.client_external_ip) {
Some(clients) => {
clients.insert(their_record);
}
None => {
let mut new_clients = HashSet::new();
new_clients.insert(their_record);
self.external_ip_assignemnts
.insert(id.client_external_ip, new_clients);
}
}

// in CGNAT mode static clients are assigned an external ip at random from the available ips
// in the exit's external subnet, so only those with explicit static assignments will have a
// fixed ip returned here
return Ok(Some(id.client_external_ip));
}
}

// check for already assigned ips
for (ip, clients) in self.external_ip_assignemnts.iter() {
if clients.contains(&their_record) {
return Ok(Some(*ip));
}
}

// if we don't have a static assignment, we need to assign an ip, we should pick the ip with the fewest clients
// note this code is designed for relatively small subnets, but since public ipv4 are so valuable it's improbable
// anyone with a /8 is going to show up and use this.
let mut possible_ips: Vec<Ipv4Addr> = subnet.into_iter().collect();
// we don't want to assign the first ip in the subnet as it's the gateway
possible_ips.remove(0);
possible_ips.pop(); // we don't want to assign the last ip in the subnet as it's the broadcast address

let mut target_ip = None;
let mut last_num_assigned = usize::MAX;
for ip in possible_ips {
match self.external_ip_assignemnts.get(&ip) {
Some(clients) => {
if clients.len() < last_num_assigned {
target_ip = Some(ip);
last_num_assigned = clients.len();
}
}
None => {
target_ip = Some(ip);
// may as well break here, it's impossible to do better than an ip unused
// by any other clients
break;
}
}
}

// finally we add the newly assigned ip to the list of clients
let target_ip = target_ip.unwrap();
match self.external_ip_assignemnts.get_mut(&target_ip) {
Some(clients) => {
clients.insert(their_record);
}
None => {
let mut new_clients = HashSet::new();
new_clients.insert(their_record);
self.external_ip_assignemnts.insert(target_ip, new_clients);
}
}

Ok(Some(target_ip))
Ok(None)
}
ExitIpv4RoutingSettings::SNAT {
subnet,
Expand All @@ -236,15 +172,13 @@ impl ClientListAnIpAssignmentMap {
// so we need to make sure the static ip assignments are handled first by building the full list
for id in static_assignments {
// duplicate static assignments are a configuration error
let mut new_clients = HashSet::new();
new_clients.insert(id.client_id);
self.external_ip_assignemnts
.insert(id.client_external_ip, new_clients);
.insert(id.client_external_ip, id.client_id);
}

// check for already assigned ips
for (ip, clients) in self.external_ip_assignemnts.iter() {
if clients.contains(&their_record) {
for (ip, client) in self.external_ip_assignemnts.iter() {
if client == &their_record {
return Ok(Some(*ip));
}
}
Expand All @@ -266,10 +200,7 @@ impl ClientListAnIpAssignmentMap {

match target_ip {
Some(ip) => {
// since this is SNAT we never have to deal with multiple clients on the same ip
let mut new_clients = HashSet::new();
new_clients.insert(their_record);
self.external_ip_assignemnts.insert(ip, new_clients);
self.external_ip_assignemnts.insert(ip, their_record);
self.client_first_connect
.insert(their_record.wg_public_key, SystemTime::now());
Ok(Some(ip))
Expand All @@ -289,7 +220,7 @@ impl ClientListAnIpAssignmentMap {
match self
.external_ip_assignemnts
.iter()
.find(|(_, clients)| clients.contains(&id))
.find(|(_, client)| *client == &id)
.map(|(ip, _)| *ip)
{
Some(ip) => {
Expand Down Expand Up @@ -466,11 +397,7 @@ mod tests {
ClientIpv4StaticAssignment, ClientIpv6StaticAssignment, ExitInternalIpv4Settings,
ExitIpv4RoutingSettings, ExitIpv6RoutingSettings,
};
use std::{
collections::{HashMap, HashSet},
net::Ipv4Addr,
vec,
};
use std::{collections::HashSet, net::Ipv4Addr};

pub fn get_ipv4_internal_test_subnet() -> Ipv4Network {
"10.0.0.0/8".parse().unwrap()
Expand Down Expand Up @@ -585,7 +512,7 @@ mod tests {
fn test_cgnat_external_ip_assignment() {
let static_assignments = vec![ClientIpv4StaticAssignment {
client_id: random_identity(),
client_external_ip: "172.168.1.12".parse().unwrap(),
client_external_ip: "172.168.1.254".parse().unwrap(),
}];
let mut data = get_test_config_cgnat(static_assignments.clone());

Expand All @@ -599,20 +526,14 @@ mod tests {
clients.push(random_identity());
}

let mut assigned_ip_count = HashMap::new();

// assign everyone an ip, make sure static assignments are respected
// make sure static assignments are respected
for client in clients {
let ip = data.get_or_add_client_external_ip(client).unwrap().unwrap();
let ip = data.get_or_add_client_external_ip(client).unwrap();
for assignment in static_assignments.iter() {
if assignment.client_id == client {
assert_eq!(ip, assignment.client_external_ip);
assert_eq!(ip.unwrap(), assignment.client_external_ip);
}
}
assigned_ip_count
.entry(ip)
.and_modify(|e| *e += 1)
.or_insert(1);
}
}

Expand Down Expand Up @@ -646,7 +567,6 @@ mod tests {
}

for assignment in data.get_external_ip_assignments() {
assert_eq!(assignment.1.len(), 1);
// make sure we can't receive the gateway, exit external or broadcast ips
assert_ne!(assignment.0, &Ipv4Addr::new(172, 168, 1, 1));
assert_ne!(assignment.0, &Ipv4Addr::new(172, 168, 1, 2));
Expand Down
7 changes: 3 additions & 4 deletions rita_exit/src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,6 @@ pub struct CurrentExitClientState {
/// into a single very long wg tunnel setup command which is then applied to the
/// wg_exit tunnel (or created if it's the first run). This is the offically supported
/// way to update live WireGuard tunnels and should not disrupt traffic
/// TODO: notes for teardown of geoip blacklisted clients- is that already happening?
pub fn setup_clients(client_data: &mut RitaExitData) -> Result<(), Box<RitaExitError>> {
info!("Starting exit setup loop");
let start = Instant::now();
Expand Down Expand Up @@ -398,7 +397,7 @@ pub fn setup_clients(client_data: &mut RitaExitData) -> Result<(), Box<RitaExitE
};
if !external_assignments
.values()
.any(|v| v.contains(registered_client_id))
.any(|v| v == registered_client_id)
{
clients_needing_setup.insert(*registered_client_id);
}
Expand Down Expand Up @@ -598,15 +597,15 @@ pub fn teardown_inactive_clients(client_data: &mut RitaExitData) {
}

fn try_teardown_client_snat(
ext_assignments: HashMap<Ipv4Addr, HashSet<Identity>>,
ext_assignments: HashMap<Ipv4Addr, Identity>,
int_assignments: HashMap<Ipv4Addr, Identity>,
client: Identity,
ext_nic: &str,
) -> Result<(), RitaExitError> {
// get this client's assigned external and internal ips
let external_ip: Vec<Ipv4Addr> = ext_assignments
.iter()
.filter(|(_k, v)| v.contains(&client))
.filter(|(_k, v)| *v == &client)
.map(|(&k, _v)| k)
.collect();
let internal_ip: Vec<Ipv4Addr> = int_assignments
Expand Down
Loading

0 comments on commit 3eb4503

Please sign in to comment.