Skip to content

Commit ca51a25

Browse files
committed
Add outbound_addr to allow for SNAT instead of MASQ
Signed-off-by: lto-dev <[email protected]>
1 parent c4318b5 commit ca51a25

File tree

10 files changed

+202
-50
lines changed

10 files changed

+202
-50
lines changed

src/firewall/iptables.rs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use crate::firewall;
33
use crate::firewall::firewalld;
44
use crate::firewall::varktables::types::TeardownPolicy::OnComplete;
55
use crate::firewall::varktables::types::{
6-
create_network_chains, get_network_chains, get_port_forwarding_chains, TeardownPolicy,
6+
create_network_chains, get_network_chains, get_port_forwarding_chains, NetworkChainConfig,
7+
TeardownPolicy,
78
};
89
use crate::network::internal_types::{
910
PortForwardConfig, SetupNetwork, TearDownNetwork, TeardownPortForward,
@@ -54,15 +55,15 @@ impl firewall::FirewallDriver for IptablesDriver {
5455
conn = &self.conn6;
5556
}
5657

57-
let chains = get_network_chains(
58-
conn,
58+
let config = NetworkChainConfig {
5959
network,
60-
&network_setup.network_hash_name,
61-
is_ipv6,
62-
network_setup.bridge_name.clone(),
63-
network_setup.isolation,
64-
network_setup.dns_port,
65-
);
60+
network_hash_name: network_setup.network_hash_name.clone(),
61+
interface_name: network_setup.bridge_name.clone(),
62+
isolation: network_setup.isolation,
63+
dns_port: network_setup.dns_port,
64+
outbound_addr: network_setup.outbound_addr,
65+
};
66+
let chains = get_network_chains(conn, config);
6667

6768
create_network_chains(chains)?;
6869

@@ -83,15 +84,15 @@ impl firewall::FirewallDriver for IptablesDriver {
8384
if is_ipv6 {
8485
conn = &self.conn6;
8586
}
86-
let chains = get_network_chains(
87-
conn,
87+
let config = NetworkChainConfig {
8888
network,
89-
&tear.config.network_hash_name,
90-
is_ipv6,
91-
tear.config.bridge_name.clone(),
92-
tear.config.isolation,
93-
tear.config.dns_port,
94-
);
89+
network_hash_name: tear.config.network_hash_name.clone(),
90+
interface_name: tear.config.bridge_name.clone(),
91+
isolation: tear.config.isolation,
92+
dns_port: tear.config.dns_port,
93+
outbound_addr: tear.config.outbound_addr,
94+
};
95+
let chains = get_network_chains(conn, config);
9596

9697
for c in &chains {
9798
c.remove_rules(tear.complete_teardown)?;

src/firewall/nft.rs

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -367,18 +367,69 @@ impl firewall::FirewallDriver for Nftables {
367367
],
368368
));
369369

370-
// Subnet chain: ip daddr != 224.0.0.0/4 masquerade
370+
// Subnet chain: ip daddr != 224.0.0.0/4 snat/masquerade
371371
let multicast_address: IpNet = match subnet {
372372
IpNet::V4(_) => "224.0.0.0/4".parse()?,
373373
IpNet::V6(_) => "ff::00/8".parse()?,
374374
};
375-
batch.add(make_rule(
376-
&chain,
377-
vec![
378-
get_subnet_match(&multicast_address, "daddr", stmt::Operator::NEQ),
379-
stmt::Statement::Masquerade(None),
380-
],
381-
));
375+
376+
// If outbound_addr is set and valid IPv4, use SNAT, otherwise use MASQUERADE
377+
if let Some(addr) = network_setup.outbound_addr {
378+
if let IpNet::V4(_) = subnet {
379+
if addr.is_ipv4() {
380+
log::trace!("Creating SNAT rule with outbound address {}", addr);
381+
batch.add(make_rule(
382+
&chain,
383+
vec![
384+
get_subnet_match(
385+
&multicast_address,
386+
"daddr",
387+
stmt::Operator::NEQ,
388+
),
389+
stmt::Statement::SNAT(Some(stmt::NAT {
390+
addr: Some(expr::Expression::String(addr.to_string())),
391+
family: Some(stmt::NATFamily::IP),
392+
port: None,
393+
flags: None,
394+
})),
395+
],
396+
));
397+
} else {
398+
log::trace!(
399+
"Outbound address {} is not IPv4, using default MASQUERADE rule",
400+
addr
401+
);
402+
batch.add(make_rule(
403+
&chain,
404+
vec![
405+
get_subnet_match(
406+
&multicast_address,
407+
"daddr",
408+
stmt::Operator::NEQ,
409+
),
410+
stmt::Statement::Masquerade(None),
411+
],
412+
));
413+
}
414+
} else {
415+
batch.add(make_rule(
416+
&chain,
417+
vec![
418+
get_subnet_match(&multicast_address, "daddr", stmt::Operator::NEQ),
419+
stmt::Statement::Masquerade(None),
420+
],
421+
));
422+
}
423+
} else {
424+
log::trace!("No outbound address set, using default MASQUERADE rule");
425+
batch.add(make_rule(
426+
&chain,
427+
vec![
428+
get_subnet_match(&multicast_address, "daddr", stmt::Operator::NEQ),
429+
stmt::Statement::Masquerade(None),
430+
],
431+
));
432+
}
382433

383434
// Next, populate basic chains with forwarding rules
384435
// Input chain: ip saddr <subnet> udp dport 53 accept

src/firewall/state.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ mod tests {
272272
network_hash_name: "hash".to_string(),
273273
isolation: IsolateOption::Never,
274274
dns_port: 53,
275+
outbound_addr: None,
275276
};
276277
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}"#;
277278

src/firewall/varktables/types.rs

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ impl VarkRule {
6666
&self.rule
6767
}
6868
}
69+
6970
// Varkchain is an iptable chain with extra info
7071
pub struct VarkChain<'a> {
7172
// name of chain
@@ -192,17 +193,23 @@ pub fn create_network_chains(chains: Vec<VarkChain<'_>>) -> NetavarkResult<()> {
192193
Ok(())
193194
}
194195

195-
pub fn get_network_chains<'a>(
196-
conn: &'a IPTables,
197-
network: IpNet,
198-
network_hash_name: &'a str,
199-
is_ipv6: bool,
200-
interface_name: String,
201-
isolation: IsolateOption,
202-
dns_port: u16,
203-
) -> Vec<VarkChain<'a>> {
196+
pub struct NetworkChainConfig {
197+
pub network: IpNet,
198+
pub network_hash_name: String,
199+
pub interface_name: String,
200+
pub isolation: IsolateOption,
201+
pub dns_port: u16,
202+
pub outbound_addr: Option<IpAddr>,
203+
}
204+
205+
pub fn get_network_chains(conn: &IPTables, config: NetworkChainConfig) -> Vec<VarkChain<'_>> {
204206
let mut chains = Vec::new();
205-
let prefixed_network_hash_name = format!("{}-{}", "NETAVARK", network_hash_name);
207+
let prefixed_network_hash_name = format!("{}-{}", "NETAVARK", config.network_hash_name);
208+
209+
let is_ipv6 = match config.network {
210+
IpNet::V4(_) => false,
211+
IpNet::V6(_) => true,
212+
};
206213

207214
// NETAVARK-HASH
208215
let mut hashed_network_chain = VarkChain::new(
@@ -214,25 +221,45 @@ pub fn get_network_chains<'a>(
214221
hashed_network_chain.create = true;
215222

216223
hashed_network_chain.build_rule(VarkRule::new(
217-
format!("-d {network} -j {ACCEPT}"),
224+
format!("-d {} -j {}", config.network, ACCEPT),
218225
Some(TeardownPolicy::OnComplete),
219226
));
220227

221228
let mut multicast_dest = MULTICAST_NET_V4;
222229
if is_ipv6 {
223230
multicast_dest = MULTICAST_NET_V6;
224231
}
225-
hashed_network_chain.build_rule(VarkRule::new(
226-
format!("! -d {multicast_dest} -j {MASQUERADE}"),
227-
Some(TeardownPolicy::OnComplete),
228-
));
232+
if let Some(addr) = config.outbound_addr {
233+
if !is_ipv6 && addr.is_ipv4() {
234+
log::trace!("Creating SNAT rule with outbound address {}", addr);
235+
hashed_network_chain.build_rule(VarkRule::new(
236+
format!("! -d {multicast_dest} -j SNAT --to-source {}", addr),
237+
Some(TeardownPolicy::OnComplete),
238+
));
239+
} else {
240+
log::trace!(
241+
"Outbound address {} is not IPv4, using default MASQUERADE rule",
242+
addr
243+
);
244+
hashed_network_chain.build_rule(VarkRule::new(
245+
format!("! -d {multicast_dest} -j {MASQUERADE}"),
246+
Some(TeardownPolicy::OnComplete),
247+
));
248+
}
249+
} else {
250+
log::trace!("No outbound address set, using default MASQUERADE rule");
251+
hashed_network_chain.build_rule(VarkRule::new(
252+
format!("! -d {multicast_dest} -j {MASQUERADE}"),
253+
Some(TeardownPolicy::OnComplete),
254+
));
255+
}
229256
chains.push(hashed_network_chain);
230257

231258
// POSTROUTING
232259
let mut postrouting_chain =
233260
VarkChain::new(conn, NAT.to_string(), POSTROUTING.to_string(), None);
234261
postrouting_chain.build_rule(VarkRule::new(
235-
format!("-s {network} -j {prefixed_network_hash_name}"),
262+
format!("-s {} -j {}", config.network, prefixed_network_hash_name),
236263
Some(TeardownPolicy::OnComplete),
237264
));
238265
chains.push(postrouting_chain);
@@ -272,7 +299,7 @@ pub fn get_network_chains<'a>(
272299
);
273300
netavark_isolation_chain_3.create = true;
274301

275-
if let IsolateOption::Normal | IsolateOption::Strict = isolation {
302+
if let IsolateOption::Normal | IsolateOption::Strict = config.isolation {
276303
debug!("Add extra isolate rules");
277304
// NETAVARK_ISOLATION_1
278305
let mut netavark_isolation_chain_1 = VarkChain::new(
@@ -290,7 +317,7 @@ pub fn get_network_chains<'a>(
290317
td_policy: Some(TeardownPolicy::OnComplete),
291318
});
292319

293-
let netavark_isolation_1_target = if let IsolateOption::Strict = isolation {
320+
let netavark_isolation_1_target = if let IsolateOption::Strict = config.isolation {
294321
// NETAVARK_ISOLATION_1 -i bridge_name ! -o bridge_name -j NETAVARK_ISOLATION_3
295322
NETAVARK_ISOLATION_3
296323
} else {
@@ -299,15 +326,16 @@ pub fn get_network_chains<'a>(
299326
};
300327
netavark_isolation_chain_1.build_rule(VarkRule {
301328
rule: format!(
302-
"-i {interface_name} ! -o {interface_name} -j {netavark_isolation_1_target}"
329+
"-i {} ! -o {} -j {}",
330+
config.interface_name, config.interface_name, netavark_isolation_1_target
303331
),
304332
position: Some(ind),
305333
td_policy: Some(TeardownPolicy::OnComplete),
306334
});
307335

308336
// NETAVARK_ISOLATION_2 -o bridge_name -j DROP
309337
netavark_isolation_chain_2.build_rule(VarkRule {
310-
rule: format!("-o {} -j {}", interface_name, "DROP"),
338+
rule: format!("-o {} -j {}", config.interface_name, "DROP"),
311339
position: Some(ind),
312340
td_policy: Some(TeardownPolicy::OnComplete),
313341
});
@@ -328,7 +356,7 @@ pub fn get_network_chains<'a>(
328356

329357
// NETAVARK_ISOLATION_3 -o bridge_name -j DROP
330358
netavark_isolation_chain_3.build_rule(VarkRule {
331-
rule: format!("-o {} -j {}", interface_name, "DROP"),
359+
rule: format!("-o {} -j {}", config.interface_name, "DROP"),
332360
position: Some(ind),
333361
td_policy: Some(TeardownPolicy::OnComplete),
334362
});
@@ -377,7 +405,7 @@ pub fn get_network_chains<'a>(
377405
netavark_input_chain.build_rule(VarkRule::new(
378406
format!(
379407
"-p {} -s {} --dport {} -j {}",
380-
proto, network, dns_port, ACCEPT
408+
proto, config.network, config.dns_port, ACCEPT
381409
),
382410
Some(TeardownPolicy::OnComplete),
383411
));
@@ -395,14 +423,17 @@ pub fn get_network_chains<'a>(
395423
// Create incoming traffic rule
396424
// CNI did this by IP address, this is implemented per subnet
397425
netavark_forward_chain.build_rule(VarkRule::new(
398-
format!("-d {network} -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT"),
426+
format!(
427+
"-d {} -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT",
428+
config.network
429+
),
399430
Some(TeardownPolicy::OnComplete),
400431
));
401432

402433
// Create outgoing traffic rule
403434
// CNI did this by IP address, this is implemented per subnet
404435
netavark_forward_chain.build_rule(VarkRule::new(
405-
format!("-s {network} -j ACCEPT"),
436+
format!("-s {} -j ACCEPT", config.network),
406437
Some(TeardownPolicy::OnComplete),
407438
));
408439
chains.push(netavark_forward_chain);

src/network/bridge.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ use super::{
2323
constants::{
2424
ISOLATE_OPTION_FALSE, ISOLATE_OPTION_STRICT, ISOLATE_OPTION_TRUE,
2525
NO_CONTAINER_INTERFACE_ERROR, OPTION_HOST_INTERFACE_NAME, OPTION_ISOLATE, OPTION_METRIC,
26-
OPTION_MODE, OPTION_MTU, OPTION_NO_DEFAULT_ROUTE, OPTION_VLAN, OPTION_VRF,
26+
OPTION_MODE, OPTION_MTU, OPTION_NO_DEFAULT_ROUTE, OPTION_OUTBOUND_ADDR, OPTION_VLAN,
27+
OPTION_VRF,
2728
},
2829
core_utils::{self, get_ipam_addresses, join_netns, parse_option, CoreUtils},
2930
driver::{self, DriverInfo},
@@ -392,6 +393,10 @@ impl<'a> Bridge<'a> {
392393
network_hash_name: id_network_hash.clone(),
393394
isolation: isolate,
394395
dns_port: self.info.dns_port,
396+
outbound_addr: parse_option::<IpAddr>(
397+
&self.info.network.options,
398+
OPTION_OUTBOUND_ADDR,
399+
)?,
395400
};
396401

397402
let mut has_ipv4 = false;

src/network/constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub const OPTION_BCLIM: &str = "bclim";
2424
pub const OPTION_VRF: &str = "vrf";
2525
pub const OPTION_VLAN: &str = "vlan";
2626
pub const OPTION_HOST_INTERFACE_NAME: &str = "host_interface_name";
27+
pub const OPTION_OUTBOUND_ADDR: &str = "outbound_addr";
2728

2829
/// 100 is the default metric for most Linux networking tools.
2930
pub const DEFAULT_METRIC: u32 = 100;

src/network/internal_types.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ pub struct SetupNetwork {
2626
pub isolation: IsolateOption,
2727
/// port used for the dns server
2828
pub dns_port: u16,
29+
/// outbound address for SNAT
30+
#[serde(skip_serializing_if = "Option::is_none")]
31+
#[serde(default)]
32+
pub outbound_addr: Option<IpAddr>,
2933
}
3034

3135
#[derive(Debug)]

test/100-bridge-iptables.bats

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,19 @@ function check_simple_bridge_iptables() {
10931093
assert "${#lines[@]}" = 4 "too many NETAVARK_FORWARD rules"
10941094
}
10951095

1096+
@test "$fw_driver - bridge with outbound addr" {
1097+
run_netavark --file ${TESTSDIR}/testfiles/bridge-outbound-addr.json setup $(get_container_netns_path)
1098+
1099+
# Check that the iptables rules were created with SNAT
1100+
run_in_host_netns iptables -t nat -S NETAVARK-F11DC6A6D09CF
1101+
assert "${lines[2]}" == "-A NETAVARK-F11DC6A6D09CF ! -d 224.0.0.0/4 -j SNAT --to-source 100.1.100.1"
1102+
1103+
run_netavark --file ${TESTSDIR}/testfiles/bridge-outbound-addr.json teardown $(get_container_netns_path)
1104+
1105+
# Check that the chain is removed
1106+
expected_rc=1 run_in_host_netns iptables -t nat -nvL NETAVARK-F11DC6A6D09CF
1107+
}
1108+
10961109
@test "$fw_driver - aardvark-dns error cleanup" {
10971110
expected_rc=1 run_netavark -a /usr/bin/false --file ${TESTSDIR}/testfiles/dualstack-bridge-custom-dns-server.json setup $(get_container_netns_path)
10981111
assert_json ".error" "error while applying dns entries: IO error: aardvark-dns exited unexpectedly without error message" "aardvark-dns error"

test/250-bridge-nftables.bats

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,19 @@ function check_simple_bridge_nftables() {
10321032
assert "$output" == $'table inet netavark {\n\tchain NETAVARK-HOSTPORT-DNAT {\n\t}\n}' "NETAVARK-HOSTPORT-DNAT chain must be empty"
10331033
}
10341034

1035+
@test "$fw_driver - bridge with outbound addr" {
1036+
run_netavark --file ${TESTSDIR}/testfiles/bridge-outbound-addr.json setup $(get_container_netns_path)
1037+
1038+
# Check that the nftables rules were created with SNAT
1039+
run_in_host_netns nft list chain inet netavark nv_2f259bab_10_89_0_0_nm24
1040+
assert "${lines[3]}" =~ "ip daddr != 224.0.0.0/4 snat ip to 100.1.100.1"
1041+
1042+
run_netavark --file ${TESTSDIR}/testfiles/bridge-outbound-addr.json teardown $(get_container_netns_path)
1043+
1044+
# Check that the chain is removed
1045+
expected_rc=1 run_in_host_netns nft list chain inet netavark nv_2f259bab_10_89_0_0_nm24
1046+
}
1047+
10351048
@test "$fw_driver - aardvark-dns error cleanup" {
10361049
expected_rc=1 run_netavark -a /usr/bin/false --file ${TESTSDIR}/testfiles/dualstack-bridge-custom-dns-server.json setup $(get_container_netns_path)
10371050
assert_json ".error" "error while applying dns entries: IO error: aardvark-dns exited unexpectedly without error message" "aardvark-dns error"

0 commit comments

Comments
 (0)