@@ -3,10 +3,13 @@ use crate::networking::messages::{AttachTap, DetachTap};
33use futures_util:: { StreamExt , TryStreamExt } ;
44
55use kameo:: { message:: Context , prelude:: * } ;
6- use nftnl:: { Batch , FinalizedBatch , Hook , MsgType , ProtoFamily , Rule , Table , nft_expr} ;
6+ use nftnl:: {
7+ Batch , FinalizedBatch , Hook , MsgType , ProtoFamily , Rule , Table , expr:: InterfaceName , nft_expr,
8+ } ;
79use rtnetlink:: { Error as NetlinkError , Handle , LinkBridge , LinkUnspec } ;
810use stable_eyre:: Report ;
911use stable_eyre:: eyre:: { Context as EyreContext , eyre} ;
12+ use std:: ffi:: CString ;
1013use tracing:: info;
1114
1215pub struct DhcpActor {
@@ -93,7 +96,6 @@ pub struct NetworkAgentActor {
9396 // netlink_handle:
9497 netlink_thread : tokio:: task:: JoinHandle < ( ) > ,
9598 netlink_handle : Handle ,
96- nft_socket : mnl:: Socket ,
9799}
98100
99101impl NetworkAgentActor {
@@ -112,19 +114,20 @@ impl NetworkAgentActor {
112114 . wrap_err_with ( || format ! ( "failed to query link {}" , link_name) )
113115 }
114116
115- fn send_nft_batch ( & mut self , batch : & FinalizedBatch ) -> Result < ( ) , Report > {
116- let portid = self . nft_socket . portid ( ) ;
117+ fn send_nft_batch ( batch : & FinalizedBatch ) -> Result < ( ) , Report > {
118+ let socket = mnl:: Socket :: new ( mnl:: Bus :: Netfilter )
119+ . wrap_err ( "failed to create netfilter netlink socket" ) ?;
120+ let portid = socket. portid ( ) ;
117121
118- self . nft_socket
122+ socket
119123 . send_all ( batch)
120124 . wrap_err ( "failed to send nftables batch to netfilter" ) ?;
121125
122126 let mut buffer = vec ! [ 0 ; nftnl:: nft_nlmsg_maxsize( ) as usize ] ;
123127 let mut expected_seqs = batch. sequence_numbers ( ) ;
124128
125129 while !expected_seqs. is_empty ( ) {
126- for message in self
127- . nft_socket
130+ for message in socket
128131 . recv ( & mut buffer[ ..] )
129132 . wrap_err ( "failed to receive nftables netlink acknowledgement" ) ?
130133 {
@@ -141,12 +144,23 @@ impl NetworkAgentActor {
141144 Ok ( ( ) )
142145 }
143146
144- fn ensure_nat_rules (
145- & mut self ,
146- _bridge : & str ,
147- _subnet : & str ,
148- upstream_iface : & str ,
149- ) -> Result < ( ) , Report > {
147+ /// Ensures the host-only NAT masquerade rule exists for the configured
148+ /// upstream interface.
149+ ///
150+ /// This is intentionally IPv4-only for now.
151+ ///
152+ /// The current host-only networking model is built around IPv4 guest
153+ /// addressing and an IPv4 bridge gateway:
154+ /// - guest subnet config uses `Ipv4Net`
155+ /// - bridge gateway config uses `Ipv4Addr`
156+ /// - bridge address assignment is currently IPv4-only
157+ ///
158+ /// Although nftables can match by interface in `inet` tables, switching
159+ /// this masquerade rule to dual-stack would implicitly opt us into IPv6
160+ /// NAT/NAT66 policy as well. That is a larger design decision than this
161+ /// hook should make on its own. Until odorobo has an intentional IPv6
162+ /// guest-networking story, we keep host-only NAT scoped to IPv4.
163+ fn ensure_nat_rules ( _bridge : & str , _subnet : & str , upstream_iface : & str ) -> Result < ( ) , Report > {
150164 let table = Table :: new ( c"nat" , ProtoFamily :: Ipv4 ) ;
151165
152166 let mut postrouting_chain = nftnl:: Chain :: new ( c"postrouting" , & table) ;
@@ -158,14 +172,18 @@ impl NetworkAgentActor {
158172 batch. add ( & postrouting_chain, MsgType :: Add ) ;
159173
160174 let mut postrouting_rule = Rule :: new ( & postrouting_chain) ;
175+ let upstream_iface = InterfaceName :: Exact (
176+ CString :: new ( upstream_iface)
177+ . wrap_err ( "upstream interface name contained interior NUL" ) ?,
178+ ) ;
161179 postrouting_rule. add_expr ( & nft_expr ! ( meta oifname) ) ;
162- postrouting_rule. add_expr ( & nft_expr ! ( cmp == upstream_iface) ) ;
180+ postrouting_rule. add_expr ( & nft_expr ! ( cmp == & upstream_iface) ) ;
163181 postrouting_rule. add_expr ( & nft_expr ! ( masquerade) ) ;
164182 batch. add ( & postrouting_rule, MsgType :: Add ) ;
165183
166184 let finalized = batch. finalize ( ) ;
167- self . send_nft_batch ( & finalized) . wrap_err_with ( || {
168- format ! ( "failed to apply nftables postrouting masquerade for {upstream_iface}" )
185+ Self :: send_nft_batch ( & finalized) . wrap_err_with ( || {
186+ format ! ( "failed to apply nftables postrouting masquerade for {upstream_iface:? }" )
169187 } ) ?;
170188
171189 Ok ( ( ) )
@@ -180,8 +198,6 @@ impl Actor for NetworkAgentActor {
180198
181199 let ( connection, handle, _) = rtnetlink:: new_connection ( ) ?;
182200 let netlink_thread = tokio:: spawn ( connection) ;
183- let nft_socket = mnl:: Socket :: new ( mnl:: Bus :: Netfilter )
184- . wrap_err ( "failed to create netfilter netlink socket" ) ?;
185201
186202 let common = match args. network_mode . clone ( ) {
187203 NetworkMode :: HostonlyNat {
@@ -239,15 +255,17 @@ impl Actor for NetworkAgentActor {
239255 None
240256 } ;
241257
242- let mut actor = Self {
258+ let actor = Self {
243259 config : args,
244260 common,
245261 dhcp_actor,
246262 netlink_thread,
247263 netlink_handle : handle,
248- nft_socket,
249264 } ;
250265
266+ let common_bridge = actor. common . bridge . clone ( ) ;
267+ let common_subnet = actor. common . subnet . clone ( ) ;
268+
251269 match actor. config . network_mode . clone ( ) {
252270 NetworkMode :: HostonlyNat {
253271 bridge : _,
@@ -272,12 +290,11 @@ impl Actor for NetworkAgentActor {
272290 )
273291 } ) ?;
274292
275- actor
276- . ensure_nat_rules ( & actor. common . bridge , & actor. common . subnet , & upstream_iface)
293+ Self :: ensure_nat_rules ( & common_bridge, & common_subnet, & upstream_iface)
277294 . wrap_err_with ( || {
278295 format ! (
279296 "failed to ensure nftables NAT rules for bridge {} and upstream {}" ,
280- actor . common . bridge , upstream_iface
297+ common_bridge , upstream_iface
281298 )
282299 } ) ?;
283300 }
0 commit comments