Skip to content

Commit 032d141

Browse files
committed
idempotent nat
1 parent 2bb9772 commit 032d141

1 file changed

Lines changed: 63 additions & 3 deletions

File tree

odorobo-agent/src/networking/actor.rs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use rtnetlink::{Error as NetlinkError, Handle, LinkBridge, LinkUnspec};
1010
use stable_eyre::Report;
1111
use stable_eyre::eyre::{Context as EyreContext, eyre};
1212
use std::ffi::CString;
13+
use std::process::Command;
1314
use tracing::info;
1415

1516
pub struct DhcpActor {
@@ -144,6 +145,45 @@ impl NetworkAgentActor {
144145
Ok(())
145146
}
146147

148+
// use the nft CLI instead because doing full introspection is kind of a pain
149+
fn nft_table_exists(table: &str) -> Result<bool, Report> {
150+
let output = Command::new("nft")
151+
.args(["list", "table", "ip", table])
152+
.output()
153+
.wrap_err_with(|| format!("failed to query nft table ip {table}"))?;
154+
155+
Ok(output.status.success())
156+
}
157+
158+
fn nft_chain_exists(table: &str, chain: &str) -> Result<bool, Report> {
159+
let output = Command::new("nft")
160+
.args(["list", "chain", "ip", table, chain])
161+
.output()
162+
.wrap_err_with(|| format!("failed to query nft chain ip {table} {chain}"))?;
163+
164+
Ok(output.status.success())
165+
}
166+
167+
fn nft_postrouting_masquerade_exists(
168+
table: &str,
169+
chain: &str,
170+
upstream_iface: &str,
171+
) -> Result<bool, Report> {
172+
let output = Command::new("nft")
173+
.args(["list", "chain", "ip", table, chain])
174+
.output()
175+
.wrap_err_with(|| format!("failed to inspect nft chain ip {table} {chain}"))?;
176+
177+
if !output.status.success() {
178+
return Ok(false);
179+
}
180+
181+
let stdout = String::from_utf8(output.stdout)
182+
.wrap_err_with(|| format!("failed to decode nft output for ip {table} {chain}"))?;
183+
184+
Ok(stdout.contains(&format!("oifname \"{upstream_iface}\" masquerade")))
185+
}
186+
147187
/// Ensures the host-only NAT masquerade rule exists for the configured
148188
/// upstream interface.
149189
///
@@ -161,15 +201,35 @@ impl NetworkAgentActor {
161201
/// hook should make on its own. Until odorobo has an intentional IPv6
162202
/// guest-networking story, we keep host-only NAT scoped to IPv4.
163203
fn ensure_nat_rules(_bridge: &str, _subnet: &str, upstream_iface: &str) -> Result<(), Report> {
164-
let table = Table::new(c"nat", ProtoFamily::Ipv4);
204+
const TABLE_NAME: &str = "odorobo";
205+
const CHAIN_NAME: &str = "postrouting";
206+
207+
let table_exists = Self::nft_table_exists(TABLE_NAME)?;
208+
let chain_exists = if table_exists {
209+
Self::nft_chain_exists(TABLE_NAME, CHAIN_NAME)?
210+
} else {
211+
false
212+
};
213+
214+
if chain_exists
215+
&& Self::nft_postrouting_masquerade_exists(TABLE_NAME, CHAIN_NAME, upstream_iface)?
216+
{
217+
return Ok(());
218+
}
219+
220+
let table = Table::new(c"odorobo", ProtoFamily::Ipv4);
165221

166222
let mut postrouting_chain = nftnl::Chain::new(c"postrouting", &table);
167223
postrouting_chain.set_type(nftnl::ChainType::Nat);
168224
postrouting_chain.set_hook(Hook::PostRouting, 100);
169225

170226
let mut batch = Batch::new();
171-
batch.add(&table, MsgType::Add);
172-
batch.add(&postrouting_chain, MsgType::Add);
227+
if !table_exists {
228+
batch.add(&table, MsgType::Add);
229+
}
230+
if !chain_exists {
231+
batch.add(&postrouting_chain, MsgType::Add);
232+
}
173233

174234
let mut postrouting_rule = Rule::new(&postrouting_chain);
175235
let upstream_iface = InterfaceName::Exact(

0 commit comments

Comments
 (0)