Skip to content

Add outbound_addr to allow for SNAT instead of MASQ #1180

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 20 additions & 17 deletions src/firewall/iptables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use crate::firewall;
use crate::firewall::firewalld;
use crate::firewall::varktables::types::TeardownPolicy::OnComplete;
use crate::firewall::varktables::types::{
create_network_chains, get_network_chains, get_port_forwarding_chains, TeardownPolicy,
create_network_chains, get_network_chains, get_port_forwarding_chains, NetworkChainConfig,
TeardownPolicy,
};
use crate::network::internal_types::{
PortForwardConfig, SetupNetwork, TearDownNetwork, TeardownPortForward,
Expand Down Expand Up @@ -54,15 +55,16 @@ impl firewall::FirewallDriver for IptablesDriver {
conn = &self.conn6;
}

let chains = get_network_chains(
conn,
let config = NetworkChainConfig {
network,
&network_setup.network_hash_name,
is_ipv6,
network_setup.bridge_name.clone(),
network_setup.isolation,
network_setup.dns_port,
);
network_hash_name: network_setup.network_hash_name.clone(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is cloning the network hash now which should not be necessary AFAICS. It is always good to avoid extra clones where possible. You should keep the hash &str defined (yes that means defining a lifetime one the struct)

interface_name: network_setup.bridge_name.clone(),
isolation: network_setup.isolation,
dns_port: network_setup.dns_port,
outbound_addr4: network_setup.outbound_addr4,
outbound_addr6: network_setup.outbound_addr6,
};
let chains = get_network_chains(conn, config);

create_network_chains(chains)?;

Expand All @@ -83,15 +85,16 @@ impl firewall::FirewallDriver for IptablesDriver {
if is_ipv6 {
conn = &self.conn6;
}
let chains = get_network_chains(
conn,
let config = NetworkChainConfig {
network,
&tear.config.network_hash_name,
is_ipv6,
tear.config.bridge_name.clone(),
tear.config.isolation,
tear.config.dns_port,
);
network_hash_name: tear.config.network_hash_name.clone(),
interface_name: tear.config.bridge_name.clone(),
isolation: tear.config.isolation,
dns_port: tear.config.dns_port,
outbound_addr4: tear.config.outbound_addr4,
outbound_addr6: tear.config.outbound_addr6,
};
let chains = get_network_chains(conn, config);

for c in &chains {
c.remove_rules(tear.complete_teardown)?;
Expand Down
96 changes: 86 additions & 10 deletions src/firewall/nft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const ISOLATION3CHAIN: &str = "NETAVARK-ISOLATION-3";

const MASK: u32 = 0x2000;

const MULTICAST_NET_V4: &str = "224.0.0.0/4";
const MULTICAST_NET_V6: &str = "ff00::/8";

/// The dnat priority for chains
/// This (and the below) are based on https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks#Priority_within_hook
const DNATPRIO: i32 = -100;
Expand Down Expand Up @@ -376,18 +379,91 @@ impl firewall::FirewallDriver for Nftables {
]),
));

// Subnet chain: ip daddr != 224.0.0.0/4 masquerade
// Subnet chain: ip daddr != 224.0.0.0/4 snat/masquerade
let multicast_address: IpNet = match subnet {
IpNet::V4(_) => "224.0.0.0/4".parse()?,
IpNet::V6(_) => "ff::00/8".parse()?,
IpNet::V4(_) => MULTICAST_NET_V4.parse()?,
IpNet::V6(_) => MULTICAST_NET_V6.parse()?,
};
batch.add(make_rule(
chain.clone(),
Cow::Owned(vec![
get_subnet_match(&multicast_address, "daddr", stmt::Operator::NEQ),
stmt::Statement::Masquerade(None),
]),
));

// Use appropriate outbound address based on subnet type
match subnet {
IpNet::V4(_) => {
if let Some(addr4) = network_setup.outbound_addr4 {
log::trace!("Creating IPv4 SNAT rule with outbound address {addr4}");
batch.add(make_rule(
chain.clone(),
Cow::Owned(vec![
get_subnet_match(
&multicast_address,
"daddr",
stmt::Operator::NEQ,
),
stmt::Statement::SNAT(Some(stmt::NAT {
addr: Some(expr::Expression::String(
addr4.to_string().into(),
)),
family: Some(stmt::NATFamily::IP),
port: None,
flags: None,
})),
]),
));
} else {
log::trace!(
"No IPv4 outbound address set, using default MASQUERADE rule"
);
batch.add(make_rule(
chain.clone(),
Cow::Owned(vec![
get_subnet_match(
&multicast_address,
"daddr",
stmt::Operator::NEQ,
),
stmt::Statement::Masquerade(None),
]),
));
}
}
IpNet::V6(_) => {
if let Some(addr6) = network_setup.outbound_addr6 {
log::trace!("Creating IPv6 SNAT rule with outbound address {addr6}");
batch.add(make_rule(
chain.clone(),
Cow::Owned(vec![
get_subnet_match(
&multicast_address,
"daddr",
stmt::Operator::NEQ,
),
stmt::Statement::SNAT(Some(stmt::NAT {
addr: Some(expr::Expression::String(
addr6.to_string().into(),
)),
family: Some(stmt::NATFamily::IP6),
port: None,
flags: None,
})),
]),
));
} else {
log::trace!(
"No IPv6 outbound address set, using default MASQUERADE rule"
);
batch.add(make_rule(
chain.clone(),
Cow::Owned(vec![
get_subnet_match(
&multicast_address,
"daddr",
stmt::Operator::NEQ,
),
stmt::Statement::Masquerade(None),
]),
));
}
}
}

// Next, populate basic chains with forwarding rules
// Input chain: ip saddr <subnet> udp dport 53 accept
Expand Down
2 changes: 2 additions & 0 deletions src/firewall/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ mod tests {
network_hash_name: "hash".to_string(),
isolation: IsolateOption::Never,
dns_port: 53,
outbound_addr4: None,
outbound_addr6: None,
};
let net_conf_json = r#"{"subnets":["10.0.0.0/24"],"bridge_name":"bridge","network_id":"c2c8a073252874648259997d53b0a1bffa491e21f04bc1bf8609266359931395","network_hash_name":"hash","isolation":"Never","dns_port":53}"#;

Expand Down
91 changes: 66 additions & 25 deletions src/firewall/varktables/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::network::internal_types::{IsolateOption, PortForwardConfig};
use ipnet::IpNet;
use iptables::IPTables;
use log::debug;
use std::net::IpAddr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

// Chain names
const NAT: &str = "nat";
Expand Down Expand Up @@ -66,6 +66,7 @@ impl VarkRule {
&self.rule
}
}

// Varkchain is an iptable chain with extra info
pub struct VarkChain<'a> {
// name of chain
Expand Down Expand Up @@ -192,17 +193,24 @@ pub fn create_network_chains(chains: Vec<VarkChain<'_>>) -> NetavarkResult<()> {
Ok(())
}

pub fn get_network_chains<'a>(
conn: &'a IPTables,
network: IpNet,
network_hash_name: &'a str,
is_ipv6: bool,
interface_name: String,
isolation: IsolateOption,
dns_port: u16,
) -> Vec<VarkChain<'a>> {
pub struct NetworkChainConfig {
pub network: IpNet,
pub network_hash_name: String,
pub interface_name: String,
pub isolation: IsolateOption,
pub dns_port: u16,
pub outbound_addr4: Option<Ipv4Addr>,
pub outbound_addr6: Option<Ipv6Addr>,
}

pub fn get_network_chains(conn: &IPTables, config: NetworkChainConfig) -> Vec<VarkChain<'_>> {
let mut chains = Vec::new();
let prefixed_network_hash_name = format!("{}-{}", "NETAVARK", network_hash_name);
let prefixed_network_hash_name = format!("{}-{}", "NETAVARK", config.network_hash_name);

let is_ipv6 = match config.network {
IpNet::V4(_) => false,
IpNet::V6(_) => true,
};

// NETAVARK-HASH
let mut hashed_network_chain = VarkChain::new(
Expand All @@ -214,25 +222,51 @@ pub fn get_network_chains<'a>(
hashed_network_chain.create = true;

hashed_network_chain.build_rule(VarkRule::new(
format!("-d {network} -j {ACCEPT}"),
format!("-d {} -j {}", config.network, ACCEPT),
Some(TeardownPolicy::OnComplete),
));

let mut multicast_dest = MULTICAST_NET_V4;
if is_ipv6 {
multicast_dest = MULTICAST_NET_V6;
}
hashed_network_chain.build_rule(VarkRule::new(
format!("! -d {multicast_dest} -j {MASQUERADE}"),
Some(TeardownPolicy::OnComplete),
));

// Use appropriate outbound address based on subnet type
if is_ipv6 {
if let Some(addr6) = config.outbound_addr6 {
log::trace!("Creating IPv6 SNAT rule with outbound address {addr6}");
hashed_network_chain.build_rule(VarkRule::new(
format!("! -d {multicast_dest} -j SNAT --to-source {addr6}"),
Some(TeardownPolicy::OnComplete),
));
} else {
log::trace!("No IPv6 outbound address set, using default MASQUERADE rule");
hashed_network_chain.build_rule(VarkRule::new(
format!("! -d {multicast_dest} -j {MASQUERADE}"),
Some(TeardownPolicy::OnComplete),
));
}
} else if let Some(addr4) = config.outbound_addr4 {
log::trace!("Creating IPv4 SNAT rule with outbound address {addr4}");
hashed_network_chain.build_rule(VarkRule::new(
format!("! -d {multicast_dest} -j SNAT --to-source {addr4}"),
Some(TeardownPolicy::OnComplete),
));
} else {
log::trace!("No IPv4 outbound address set, using default MASQUERADE rule");
hashed_network_chain.build_rule(VarkRule::new(
format!("! -d {multicast_dest} -j {MASQUERADE}"),
Some(TeardownPolicy::OnComplete),
));
}

chains.push(hashed_network_chain);

// POSTROUTING
let mut postrouting_chain =
VarkChain::new(conn, NAT.to_string(), POSTROUTING.to_string(), None);
postrouting_chain.build_rule(VarkRule::new(
format!("-s {network} -j {prefixed_network_hash_name}"),
format!("-s {} -j {}", config.network, prefixed_network_hash_name),
Some(TeardownPolicy::OnComplete),
));
chains.push(postrouting_chain);
Expand Down Expand Up @@ -272,7 +306,7 @@ pub fn get_network_chains<'a>(
);
netavark_isolation_chain_3.create = true;

if let IsolateOption::Normal | IsolateOption::Strict = isolation {
if let IsolateOption::Normal | IsolateOption::Strict = config.isolation {
debug!("Add extra isolate rules");
// NETAVARK_ISOLATION_1
let mut netavark_isolation_chain_1 = VarkChain::new(
Expand All @@ -290,7 +324,7 @@ pub fn get_network_chains<'a>(
td_policy: Some(TeardownPolicy::OnComplete),
});

let netavark_isolation_1_target = if let IsolateOption::Strict = isolation {
let netavark_isolation_1_target = if let IsolateOption::Strict = config.isolation {
// NETAVARK_ISOLATION_1 -i bridge_name ! -o bridge_name -j NETAVARK_ISOLATION_3
NETAVARK_ISOLATION_3
} else {
Expand All @@ -299,15 +333,16 @@ pub fn get_network_chains<'a>(
};
netavark_isolation_chain_1.build_rule(VarkRule {
rule: format!(
"-i {interface_name} ! -o {interface_name} -j {netavark_isolation_1_target}"
"-i {} ! -o {} -j {}",
config.interface_name, config.interface_name, netavark_isolation_1_target
),
position: Some(ind),
td_policy: Some(TeardownPolicy::OnComplete),
});

// NETAVARK_ISOLATION_2 -o bridge_name -j DROP
netavark_isolation_chain_2.build_rule(VarkRule {
rule: format!("-o {} -j {}", interface_name, "DROP"),
rule: format!("-o {} -j {}", config.interface_name, "DROP"),
position: Some(ind),
td_policy: Some(TeardownPolicy::OnComplete),
});
Expand All @@ -328,7 +363,7 @@ pub fn get_network_chains<'a>(

// NETAVARK_ISOLATION_3 -o bridge_name -j DROP
netavark_isolation_chain_3.build_rule(VarkRule {
rule: format!("-o {} -j {}", interface_name, "DROP"),
rule: format!("-o {} -j {}", config.interface_name, "DROP"),
position: Some(ind),
td_policy: Some(TeardownPolicy::OnComplete),
});
Expand Down Expand Up @@ -375,7 +410,10 @@ pub fn get_network_chains<'a>(
// to gateway when using bridge network with internal dns.
for proto in ["udp", "tcp"] {
netavark_input_chain.build_rule(VarkRule::new(
format!("-p {proto} -s {network} --dport {dns_port} -j {ACCEPT}"),
format!(
"-p {proto} -s {} --dport {} -j {ACCEPT}",
config.network, config.dns_port
),
Some(TeardownPolicy::OnComplete),
));
}
Expand All @@ -392,14 +430,17 @@ pub fn get_network_chains<'a>(
// Create incoming traffic rule
// CNI did this by IP address, this is implemented per subnet
netavark_forward_chain.build_rule(VarkRule::new(
format!("-d {network} -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT"),
format!(
"-d {} -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT",
config.network
),
Some(TeardownPolicy::OnComplete),
));

// Create outgoing traffic rule
// CNI did this by IP address, this is implemented per subnet
netavark_forward_chain.build_rule(VarkRule::new(
format!("-s {network} -j ACCEPT"),
format!("-s {} -j ACCEPT", config.network),
Some(TeardownPolicy::OnComplete),
));
chains.push(netavark_forward_chain);
Expand Down
Loading