@@ -10,6 +10,7 @@ use rtnetlink::{Error as NetlinkError, Handle, LinkBridge, LinkUnspec};
1010use stable_eyre:: Report ;
1111use stable_eyre:: eyre:: { Context as EyreContext , eyre} ;
1212use std:: ffi:: CString ;
13+ use std:: process:: Command ;
1314use tracing:: info;
1415
1516pub 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