From ccf6b29423eafd3e89f6e8458c962b58dfbbb338 Mon Sep 17 00:00:00 2001 From: "Aode (Lion)" Date: Sun, 17 Oct 2021 12:54:47 -0500 Subject: [PATCH] Initial support for VLANs --- config.toml | 3 + src/iptables.rs | 169 +++++----------------------- src/rules.rs | 243 ++++++++++++++++++++++++----------------- src/startup/mod.rs | 83 +++++++++++++- src/startup/preload.rs | 53 +++++++-- 5 files changed, 292 insertions(+), 259 deletions(-) diff --git a/config.toml b/config.toml index 0bb6cb3..17498a5 100644 --- a/config.toml +++ b/config.toml @@ -3,6 +3,9 @@ external = "eth[0-9]+" internal = [ "enp1s0" ] +vlan = [ + "vlan[0-9]+" +] tunnel = [ "wg[0-9]+" ] diff --git a/src/iptables.rs b/src/iptables.rs index b2db26d..ffeae05 100644 --- a/src/iptables.rs +++ b/src/iptables.rs @@ -25,43 +25,7 @@ impl fmt::Display for Proto { } } -pub(crate) async fn input_accept( - proto: Proto, - external_interface: &str, - external_ip: Ipv4Addr, - external_port: u16, - external_mask: u8, -) -> Result<(), anyhow::Error> { - input( - proto, - external_interface, - external_ip, - external_port, - external_mask, - move |cmd| cmd.arg("-I"), - ) - .await -} - -pub(crate) async fn delete_input_accept( - proto: Proto, - external_interface: &str, - external_ip: Ipv4Addr, - external_port: u16, - external_mask: u8, -) -> Result<(), anyhow::Error> { - input( - proto, - external_interface, - external_ip, - external_port, - external_mask, - move |cmd| cmd.arg("-D"), - ) - .await -} - -async fn input( +pub(crate) async fn input( proto: Proto, external_interface: &str, external_ip: Ipv4Addr, @@ -93,39 +57,36 @@ async fn input( .await } -pub(crate) async fn forward_accept( +pub(crate) async fn outbound_forward_established( external_interface: &str, internal_interface: &str, proto: Proto, internal_port: u16, + func: impl Fn(&mut Command) -> &mut Command, ) -> Result<(), anyhow::Error> { - forward( - external_interface, - internal_interface, - proto, - internal_port, - move |cmd| cmd.arg("-I"), - ) + iptables_filter(move |cmd| { + func(cmd).args(&[ + "FORWARD", + "-i", + internal_interface, + "-o", + external_interface, + "-p", + proto.as_iptables_str(), + "--sport", + &internal_port.to_string(), + "-m", + "conntrack", + "--ctstate", + "ESTABLISHED,RELATED", + "-j", + "ACCEPT", + ]) + }) .await } -pub(crate) async fn delete_forward_accept( - external_interface: &str, - internal_interface: &str, - proto: Proto, - internal_port: u16, -) -> Result<(), anyhow::Error> { - forward( - external_interface, - internal_interface, - proto, - internal_port, - move |cmd| cmd.arg("-D"), - ) - .await -} - -async fn forward( +pub(crate) async fn forward( external_interface: &str, internal_interface: &str, proto: Proto, @@ -154,47 +115,7 @@ async fn forward( .await } -pub(crate) async fn forward_postrouting( - proto: Proto, - internal_ip: Ipv4Addr, - internal_mask: u8, - internal_port: u16, - external_ip: Ipv4Addr, - destination_ip: Ipv4Addr, -) -> Result<(), anyhow::Error> { - forward_postrouting_snat( - proto, - internal_ip, - internal_mask, - internal_port, - external_ip, - destination_ip, - |cmd| cmd.arg("-I"), - ) - .await -} - -pub(crate) async fn delete_forward_postrouting( - proto: Proto, - internal_ip: Ipv4Addr, - internal_mask: u8, - internal_port: u16, - external_ip: Ipv4Addr, - destination_ip: Ipv4Addr, -) -> Result<(), anyhow::Error> { - forward_postrouting_snat( - proto, - internal_ip, - internal_mask, - internal_port, - external_ip, - destination_ip, - |cmd| cmd.arg("-D"), - ) - .await -} - -async fn forward_postrouting_snat( +pub(crate) async fn forward_postrouting_snat( proto: Proto, internal_ip: Ipv4Addr, internal_mask: u8, @@ -229,47 +150,7 @@ async fn forward_postrouting_snat( .await } -pub(crate) async fn forward_prerouting( - proto: Proto, - external_ip: Ipv4Addr, - external_mask: u8, - external_port: u16, - destination_ip: Ipv4Addr, - destination_port: u16, -) -> Result<(), anyhow::Error> { - forward_prerouting_dnat( - proto, - external_ip, - external_mask, - external_port, - destination_ip, - destination_port, - |cmd| cmd.arg("-I"), - ) - .await -} - -pub(crate) async fn delete_forward_prerouting( - proto: Proto, - external_ip: Ipv4Addr, - external_mask: u8, - external_port: u16, - destination_ip: Ipv4Addr, - destination_port: u16, -) -> Result<(), anyhow::Error> { - forward_prerouting_dnat( - proto, - external_ip, - external_mask, - external_port, - destination_ip, - destination_port, - |cmd| cmd.arg("-D"), - ) - .await -} - -async fn forward_prerouting_dnat( +pub(crate) async fn forward_prerouting_dnat( proto: Proto, external_ip: Ipv4Addr, external_mask: u8, diff --git a/src/rules.rs b/src/rules.rs index a88b771..4253374 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -78,69 +78,103 @@ pub(crate) async fn delete(db: &Db, rule_id: &str) -> Result Result<(), anyhow::Error> { +async fn set_rule( + interfaces: &Interfaces, + rule: Rule, + func: impl Fn(&mut async_process::Command) -> &mut async_process::Command + Copy, +) -> anyhow::Result<()> { match rule.kind { RuleKind::Accept => { - iptables::delete_input_accept( + iptables::input( rule.proto, &interfaces.external.interface, interfaces.external.ip, rule.port, interfaces.external.mask, + func, ) .await?; } RuleKind::Forward { dest_ip, dest_port } => { - for info in &interfaces.internal { - iptables::delete_forward_accept( - &interfaces.external.interface, - &info.interface, - rule.proto, - dest_port, - ) - .await?; - } - iptables::delete_forward_prerouting( + let internal_iface = interfaces + .internal + .iter() + .chain(&interfaces.tunnel) + .chain(&interfaces.vlan) + .find(|info| mask_matches(info.ip, info.mask, dest_ip)); + + let internal_iface = if let Some(internal_iface) = internal_iface { + internal_iface + } else { + return Ok(()); + }; + + iptables::forward( + &interfaces.external.interface, + &internal_iface.interface, + rule.proto, + dest_port, + func, + ) + .await?; + iptables::forward_prerouting_dnat( rule.proto, interfaces.external.ip, interfaces.external.mask, rule.port, dest_ip, dest_port, + func, + ) + .await?; + iptables::forward_postrouting_snat( + rule.proto, + internal_iface.ip, + internal_iface.mask, + dest_port, + interfaces.external.ip, + dest_ip, + func, ) .await?; - for iface in interfaces.internal.iter().chain(interfaces.tunnel.iter()) { - let is_nat_iface = interfaces - .nats - .iter() - .any(|nat_iface| *nat_iface == iface.interface); + for iface in interfaces + .internal + .iter() + .chain(&interfaces.tunnel) + .chain(&interfaces.vlan) + .filter(|iface| *iface != internal_iface) + { let has_nat_subnet = interfaces.nats.iter().any(|nat_iface| { - *nat_iface != iface.interface - && interfaces - .internal - .iter() - .chain(interfaces.tunnel.iter()) - .any(|other_iface| { - *nat_iface == other_iface.interface - && other_iface.ip == iface.ip - && other_iface.mask == iface.mask - }) + *nat_iface == iface.interface + || *nat_iface != iface.interface + && interfaces + .internal + .iter() + .chain(&interfaces.tunnel) + .chain(&interfaces.vlan) + .any(|other_iface| { + *nat_iface == other_iface.interface + && other_iface.ip == iface.ip + && other_iface.mask == iface.mask + }) }); - if is_nat_iface { - iptables::delete_forward_prerouting( - rule.proto, iface.ip, iface.mask, rule.port, dest_ip, dest_port, + if !has_nat_subnet { + iptables::forward( + &iface.interface, + &internal_iface.interface, + rule.proto, + dest_port, + func, ) .await?; - } else if !has_nat_subnet { - iptables::delete_forward_postrouting( + iptables::outbound_forward_established( + &iface.interface, + &internal_iface.interface, rule.proto, - iface.ip, - iface.mask, dest_port, - interfaces.external.ip, - dest_ip, + func, ) .await?; } @@ -151,6 +185,14 @@ pub(crate) async fn unset(interfaces: &Interfaces, rule: Rule) -> Result<(), any Ok(()) } +pub(crate) async fn apply(interfaces: &Interfaces, rule: Rule) -> Result<(), anyhow::Error> { + set_rule(interfaces, rule, |cmd| cmd.arg("-I")).await +} + +pub(crate) async fn unset(interfaces: &Interfaces, rule: Rule) -> Result<(), anyhow::Error> { + set_rule(interfaces, rule, |cmd| cmd.arg("-D")).await +} + pub(crate) async fn save(db: &Db, rule: &Rule) -> Result<(), anyhow::Error> { let tree = rules_tree(db); @@ -164,77 +206,29 @@ pub(crate) async fn save(db: &Db, rule: &Rule) -> Result<(), anyhow::Error> { Ok(()) } -pub(crate) async fn apply(interfaces: &Interfaces, rule: Rule) -> Result<(), anyhow::Error> { - match rule.kind { - RuleKind::Accept => { - iptables::input_accept( - rule.proto, - &interfaces.external.interface, - interfaces.external.ip, - rule.port, - interfaces.external.mask, - ) - .await?; - } - RuleKind::Forward { dest_ip, dest_port } => { - for info in &interfaces.internal { - iptables::forward_accept( - &interfaces.external.interface, - &info.interface, - rule.proto, - dest_port, - ) - .await?; - } - iptables::forward_prerouting( - rule.proto, - interfaces.external.ip, - interfaces.external.mask, - rule.port, - dest_ip, - dest_port, - ) - .await?; - for iface in interfaces.internal.iter().chain(interfaces.tunnel.iter()) { - let is_nat_iface = interfaces - .nats - .iter() - .any(|nat_iface| *nat_iface == iface.interface); +// 255 - 2^n where n is the index +const MASK_BITS: [u8; 7] = [254, 252, 248, 240, 224, 192, 128]; - let has_nat_subnet = interfaces.nats.iter().any(|nat_iface| { - *nat_iface != iface.interface - && interfaces - .internal - .iter() - .chain(interfaces.tunnel.iter()) - .any(|other_iface| { - *nat_iface == other_iface.interface - && other_iface.ip == iface.ip - && other_iface.mask == iface.mask - }) - }); +fn mask_matches(mask_ip: Ipv4Addr, netmask: u8, rule_ip: Ipv4Addr) -> bool { + let mut count: u8 = 8; + let mut matches = true; - if is_nat_iface { - iptables::forward_prerouting( - rule.proto, iface.ip, iface.mask, rule.port, dest_ip, dest_port, - ) - .await?; - } else if !has_nat_subnet { - iptables::forward_postrouting( - rule.proto, - iface.ip, - iface.mask, - dest_port, - interfaces.external.ip, - dest_ip, - ) - .await?; - } + for (mask_byte, rule_byte) in mask_ip.octets().iter().zip(&rule_ip.octets()) { + if count <= netmask { + matches = matches && mask_byte == rule_byte + } else { + let remaining = count.saturating_sub(netmask); + if remaining < 8 { + let mask = MASK_BITS[remaining.saturating_sub(1) as usize]; + + matches = matches && (mask_byte & mask) == (rule_byte & mask); } } + + count += 8; } - Ok(()) + matches } fn rule_id(id: u64) -> String { @@ -243,11 +237,58 @@ fn rule_id(id: u64) -> String { #[cfg(test)] mod tests { + use super::mask_matches; use crate::{ iptables::Proto, rules::{Rule, RuleKind}, }; + #[test] + fn ips_match_mask() { + let tests = [ + ("192.168.6.0", 24, "192.168.6.1"), + ("192.168.6.0", 24, "192.168.6.254"), + ("192.168.6.0", 25, "192.168.6.1"), + ("192.168.6.0", 26, "192.168.6.1"), + ("192.168.6.0", 27, "192.168.6.1"), + ("192.168.6.0", 31, "192.168.6.1"), + ("192.168.6.0", 32, "192.168.6.0"), + ("192.168.0.0", 16, "192.168.255.0"), + ("192.168.0.0", 16, "192.168.0.255"), + ("192.168.0.0", 16, "192.168.1.0"), + ("192.168.0.0", 16, "192.168.0.1"), + ("192.168.0.0", 16, "192.168.128.0"), + ]; + + for (mask_ip, mask, rule_ip) in tests { + let matches = mask_matches(mask_ip.parse().unwrap(), mask, rule_ip.parse().unwrap()); + + assert!(matches); + } + } + + #[test] + fn ips_dont_match_mask() { + let tests = [ + ("192.168.6.0", 24, "192.168.5.0"), + ("192.168.6.0", 25, "192.168.6.128"), + ("192.168.6.0", 31, "192.168.6.2"), + ("192.169.0.0", 16, "192.168.0.1"), + ("192.1.0.0", 16, "192.0.0.0"), + ("192.0.0.0", 16, "192.1.0.0"), + ("192.255.0.0", 16, "192.0.0.0"), + ("192.0.0.0", 16, "192.255.0.0"), + ("192.168.0.0", 17, "192.168.128.0"), + ]; + + for (mask_ip, mask, rule_ip) in tests { + println!("comparing: {}/{} to {}", mask_ip, mask, rule_ip); + let matches = mask_matches(mask_ip.parse().unwrap(), mask, rule_ip.parse().unwrap()); + + assert!(!matches); + } + } + #[test] fn can_serialize() { let rule = Rule { diff --git a/src/startup/mod.rs b/src/startup/mod.rs index a6ba45c..a57b73d 100644 --- a/src/startup/mod.rs +++ b/src/startup/mod.rs @@ -20,6 +20,7 @@ struct InterfaceConfig { external: String, internal: Vec, tunnel: Vec, + vlan: Vec, } #[derive(serde::Deserialize)] @@ -39,11 +40,13 @@ struct ServerConfig { pub struct Interfaces { pub(crate) external: InterfaceInfo, pub(crate) internal: Vec, + pub(crate) vlan: Vec, pub(crate) tunnel: Vec, pub(crate) shared_internal: bool, pub(crate) nats: Vec, } +#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub(crate) struct InterfaceInfo { pub(crate) interface: String, pub(crate) ip: Ipv4Addr, @@ -90,6 +93,11 @@ impl Interfaces { ip: "192.168.6.1".parse()?, mask: 24, }], + vlan: vec![InterfaceInfo { + interface: String::from("vlan20@enp1s0"), + ip: "192.168.6.20".parse()?, + mask: 24, + }], tunnel: vec![ InterfaceInfo { interface: String::from("wg0"), @@ -135,6 +143,12 @@ impl Interfaces { )); } + let mut vlan = Vec::new(); + + for iface in &config.interface.vlan { + vlan.extend(parse_interface_info(&output, &iface)?); + } + let mut tunnel = Vec::new(); for iface in &config.interface.tunnel { @@ -144,6 +158,7 @@ impl Interfaces { Ok(Interfaces { external, internal, + vlan, tunnel, shared_internal: config.network.shared_internal, nats: config.network.nats.clone(), @@ -218,13 +233,45 @@ mod tests { valid_lft forever preferred_lft forever 3: eth1: mtu 1500 qdisc mq state UP group default qlen 1000 link/ether ca:b8:a7:51:07:3d brd ff:ff:ff:ff:ff:ff - inet 136.49.5.58/20 brd 136.49.15.255 scope global dynamic eth1 - valid_lft 85973sec preferred_lft 85973sec + inet 70.112.254.176/19 brd 70.112.255.255 scope global dynamic eth1 + valid_lft 45780sec preferred_lft 45780sec + inet6 2605:6000:ffc0:77:f985:3af7:8be2:ba44/128 scope global dynamic noprefixroute + valid_lft 584496sec preferred_lft 584496sec inet6 fe80::c8b8:a7ff:fe51:73d/64 scope link valid_lft forever preferred_lft forever -4: wg0: mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000 +4: vlan40@enp1s0: mtu 1500 qdisc noqueue state UP group default qlen 1000 + link/ether 24:4b:fe:37:ad:b1 brd ff:ff:ff:ff:ff:ff + inet 192.168.40.1/24 brd 192.168.40.255 scope global vlan40 + valid_lft forever preferred_lft forever + inet6 fe80::264b:feff:fe37:adb1/64 scope link + valid_lft forever preferred_lft forever +5: vlan30@enp1s0: mtu 1500 qdisc noqueue state UP group default qlen 1000 + link/ether 24:4b:fe:37:ad:b1 brd ff:ff:ff:ff:ff:ff + inet 192.168.30.1/24 brd 192.168.30.255 scope global vlan30 + valid_lft forever preferred_lft forever + inet6 fe80::264b:feff:fe37:adb1/64 scope link + valid_lft forever preferred_lft forever +6: vlan20@enp1s0: mtu 1500 qdisc noqueue state UP group default qlen 1000 + link/ether 24:4b:fe:37:ad:b1 brd ff:ff:ff:ff:ff:ff + inet 192.168.20.1/24 brd 192.168.20.255 scope global vlan20 + valid_lft forever preferred_lft forever + inet6 fe80::264b:feff:fe37:adb1/64 scope link + valid_lft forever preferred_lft forever +7: wg1: mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000 link/none - inet 192.168.5.0/24 scope global wg0 + inet 10.42.6.1/24 scope global wg1 + valid_lft forever preferred_lft forever +8: wg2: mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000 + link/none + inet 10.42.6.1/24 scope global wg2 + valid_lft forever preferred_lft forever +9: wg0: mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000 + link/none + inet 192.168.5.1/24 scope global wg0 + valid_lft forever preferred_lft forever +10: wg3: mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000 + link/none + inet 192.168.4.1/24 scope global wg3 valid_lft forever preferred_lft forever "#; @@ -236,8 +283,8 @@ mod tests { .unwrap(); assert_eq!(info.interface, "eth1"); - assert_eq!(info.ip.to_string(), "136.49.5.58"); - assert_eq!(info.mask, 20); + assert_eq!(info.ip.to_string(), "70.112.254.176"); + assert_eq!(info.mask, 19); } #[test] @@ -251,4 +298,28 @@ mod tests { assert_eq!(info.ip.to_string(), "192.168.6.1"); assert_eq!(info.mask, 24); } + + #[test] + fn parses_tunnel() { + let info = parse_interface_info(OUTPUT, "wg[0-9]+") + .unwrap() + .next() + .unwrap(); + + assert_eq!(info.interface, "wg1"); + assert_eq!(info.ip.to_string(), "10.42.6.1"); + assert_eq!(info.mask, 24); + } + + #[test] + fn parses_vlan() { + let info = parse_interface_info(OUTPUT, "vlan[0-9]+") + .unwrap() + .next() + .unwrap(); + + assert_eq!(info.interface, "vlan40"); + assert_eq!(info.ip.to_string(), "192.168.40.1"); + assert_eq!(info.mask, 24); + } } diff --git a/src/startup/preload.rs b/src/startup/preload.rs index 4c21871..d92c667 100644 --- a/src/startup/preload.rs +++ b/src/startup/preload.rs @@ -26,7 +26,7 @@ fn filter(interfaces: &Interfaces) -> String { ); // Allow internal machines to connect to anything - for iface in &interfaces.internal { + for iface in interfaces.internal.iter().chain(&interfaces.vlan) { filter += &format!( "-A INPUT -i {intif} -s {intip}/{intmask} -d {universe} -j ACCEPT\n", intif = iface.interface, @@ -38,7 +38,7 @@ fn filter(interfaces: &Interfaces) -> String { // Disallow IP spoofing, internal IPs should only come from the internal network // If an internal IP is seen by the external interface, it's BAD!!!! - for iface in &interfaces.internal { + for iface in interfaces.internal.iter().chain(&interfaces.vlan) { filter += &format!( "-A INPUT -i {extif} -s {intip}/{intmask} -d {universe} -j REJECT\n", extif = interfaces.external.interface, @@ -67,7 +67,7 @@ fn filter(interfaces: &Interfaces) -> String { ); // Allow DHCP traffic - for iface in &interfaces.internal { + for iface in interfaces.internal.iter().chain(&interfaces.vlan) { filter += &format!( "-A INPUT -i {intif} -p tcp --sport 68 --dport 67 -j ACCEPT\n", intif = iface.interface, @@ -102,6 +102,27 @@ fn filter(interfaces: &Interfaces) -> String { universe = UNIVERSE, ); + // Allow VLAN traffic only on VLAN + for iface in &interfaces.vlan { + // Allow internal traffic only on network associated with interface + filter += &format!( + "-A OUTPUT -o {intif} -s {extip}/{extmask} -d {intip}/{intmask} -j ACCEPT\n", + intif = iface.interface, + extip = interfaces.external.ip, + extmask = interfaces.external.mask, + intip = iface.ip, + intmask = iface.mask, + ); + + // Allow traffic from self to networks associated with interface + filter += &format!( + "-A OUTPUT -o {intif} -s {intip}/32 -d {intip}/{intmask} -j ACCEPT\n", + intif = iface.interface, + intip = iface.ip, + intmask = iface.mask, + ); + } + if interfaces.shared_internal { for iface in &interfaces.internal { // jface (jeans iface) @@ -148,7 +169,7 @@ fn filter(interfaces: &Interfaces) -> String { } } - for iface in &interfaces.internal { + for iface in interfaces.internal.iter().chain(&interfaces.vlan) { // Deny traffic to internal networks on external interface filter += &format!( "-A OUTPUT -o {extif} -s {universe} -d {intip}/{intmask} -j REJECT\n", @@ -175,7 +196,7 @@ fn filter(interfaces: &Interfaces) -> String { universe = UNIVERSE, ); - for iface in &interfaces.internal { + for iface in interfaces.internal.iter().chain(&interfaces.vlan) { // Allow DHCP traffic out from internal interfaces filter += &format!( "-A OUTPUT -o {intif} -p tcp -s {intip} --sport 67 -d 255.255.255.255 --dport 68 -j ACCEPT\n", @@ -203,6 +224,7 @@ fn filter(interfaces: &Interfaces) -> String { .internal .iter() .chain(interfaces.tunnel.iter()) + .chain(interfaces.vlan.iter()) .find(|iface| iface.interface == *nat_iface) { filter += &format!( @@ -213,13 +235,19 @@ fn filter(interfaces: &Interfaces) -> String { ); } - for iface in interfaces.internal.iter().chain(interfaces.tunnel.iter()) { + for iface in interfaces + .internal + .iter() + .chain(interfaces.tunnel.iter()) + .chain(interfaces.vlan.iter()) + { let is_nat_iface = *nat_iface == iface.interface; let has_nat_subnet = interfaces .internal .iter() .chain(interfaces.tunnel.iter()) + .chain(interfaces.vlan.iter()) .any(|other_iface| { *nat_iface == other_iface.interface && other_iface.ip == iface.ip @@ -257,7 +285,7 @@ fn filter(interfaces: &Interfaces) -> String { } // Accept TCP packets - for iface in &interfaces.internal { + for iface in interfaces.internal.iter().chain(&interfaces.vlan) { filter += &format!( "-A FORWARD -i {extif} -o {intif} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n", extif = interfaces.external.interface, @@ -265,6 +293,14 @@ fn filter(interfaces: &Interfaces) -> String { ); } + for iface in &interfaces.vlan { + // Allow packets across vlan interface + filter += &format!( + "-A FORWARD -i {intif} -o {intif} -j ACCEPT\n", + intif = iface.interface, + ); + } + if interfaces.shared_internal { for iface in &interfaces.internal { // jface (jeans interface) @@ -288,7 +324,7 @@ fn filter(interfaces: &Interfaces) -> String { } // Forward packets to the internet - for iface in &interfaces.internal { + for iface in interfaces.internal.iter().chain(&interfaces.vlan) { filter += &format!( "-A FORWARD -i {intif} -o {extif} -j ACCEPT\n", intif = iface.interface, @@ -326,6 +362,7 @@ fn nat(interfaces: &Interfaces) -> String { .internal .iter() .chain(interfaces.tunnel.iter()) + .chain(interfaces.vlan.iter()) .any(|other_iface| { *nat_iface == other_iface.interface && other_iface.ip == iface.ip