@@ -11,6 +11,7 @@ import (
1111 "strings"
1212
1313 "github.com/sirupsen/logrus"
14+ "go.podman.io/common/libnetwork/pasta"
1415 "go.podman.io/common/libnetwork/types"
1516 "go.podman.io/common/pkg/config"
1617)
@@ -143,9 +144,107 @@ func (n *netavarkNetwork) Setup(namespacePath string, options types.SetupOptions
143144 return nil , fmt .Errorf ("unexpected netavark result length, want (%d), got (%d) networks" , len (options .Networks ), len (result ))
144145 }
145146
147+ if n .rootlessPortForwarder == config .RootlessPortForwarderPasta && n .networkRootless && len (options .NetworkOptions .PortMappings ) > 0 {
148+ // If NetworkStatus was already populated by the caller, pesto setup
149+ // already happened on a prior network connect — skip to avoid duplicate rules.
150+ if options .NetworkOptions .NetworkStatus != nil {
151+ return result , nil
152+ }
153+ opts := options .NetworkOptions
154+ opts .NetworkStatus = result
155+ if len (opts .NetworkOrder ) == 0 {
156+ opts .NetworkOrder = make ([]string , 0 , len (opts .Networks ))
157+ for _ , net := range opts .Networks {
158+ opts .NetworkOrder = append (opts .NetworkOrder , net .Name )
159+ }
160+ }
161+ if err := n .pestoSetup (opts ); err != nil {
162+ return nil , err
163+ }
164+ }
165+
146166 return result , err
147167}
148168
169+ // pestoSetup publishes port mappings via pesto with target address mapping.
170+ // PestoAddPorts is idempotent so this is safe to call on every Setup.
171+ func (n * netavarkNetwork ) pestoSetup (opts types.NetworkOptions ) error {
172+ if n .rootlessNetns == nil {
173+ return nil
174+ }
175+
176+ ipv4 , _ , ipv6 , _ := firstIPsFromStatus (opts .NetworkStatus , opts .NetworkOrder )
177+ if ipv4 == "" && ipv6 == "" {
178+ return nil
179+ }
180+ return pasta .PestoAddPorts (n .config , n .PestoSocketPath (), opts .PortMappings , ipv4 , ipv6 )
181+ }
182+
183+ // pestoTeardown handles pesto port mapping removal on network disconnect.
184+ // It derives the active forwarding IPs from the caller-provided NetworkStatus
185+ // (the podman db state) rather than maintaining a separate state file.
186+ // - If NetworkStatus is empty: nothing to do.
187+ // - If this is the last network: delete all mappings.
188+ // - If the disconnected network supplied the active IP: delete old mappings,
189+ // pick a replacement IP from the remaining networks, and re-add.
190+ // - Otherwise: no port changes needed.
191+ func (n * netavarkNetwork ) pestoTeardown (opts types.NetworkOptions ) error {
192+ if n .rootlessNetns == nil {
193+ return nil
194+ }
195+
196+ if len (opts .NetworkStatus ) == 0 {
197+ return nil
198+ }
199+
200+ // Determine current active IPs from the full set of connected networks,
201+ // respecting connection-time ordering from NetworkOrder.
202+ activeIPv4 , activeIPv4Net , activeIPv6 , activeIPv6Net := firstIPsFromStatus (opts .NetworkStatus , opts .NetworkOrder )
203+
204+ // Build the set of networks being disconnected and the remaining status.
205+ remaining := make (map [string ]types.StatusBlock , len (opts .NetworkStatus ))
206+ disconnectedNets := make (map [string ]struct {}, len (opts .Networks ))
207+ for _ , namedNet := range opts .Networks {
208+ disconnectedNets [namedNet .Name ] = struct {}{}
209+ }
210+ for name , sb := range opts .NetworkStatus {
211+ if _ , removed := disconnectedNets [name ]; ! removed {
212+ remaining [name ] = sb
213+ }
214+ }
215+
216+ // Last network: remove all port mappings.
217+ if len (remaining ) == 0 {
218+ return pasta .PestoDeletePorts (n .config , n .PestoSocketPath (), opts .PortMappings ,
219+ activeIPv4 , activeIPv6 )
220+ }
221+
222+ // Check whether any active IP came from a network being disconnected.
223+ _ , v4Lost := disconnectedNets [activeIPv4Net ]
224+ v4Lost = v4Lost && activeIPv4Net != ""
225+ _ , v6Lost := disconnectedNets [activeIPv6Net ]
226+ v6Lost = v6Lost && activeIPv6Net != ""
227+
228+ if ! v4Lost && ! v6Lost {
229+ return nil
230+ }
231+
232+ // Remap: delete old mappings, pick new IPs from remaining networks, re-add.
233+ if err := pasta .PestoDeletePorts (n .config , n .PestoSocketPath (), opts .PortMappings ,
234+ activeIPv4 , activeIPv6 ); err != nil {
235+ return fmt .Errorf ("deleting port mappings for remap: %w" , err )
236+ }
237+
238+ newIPv4 , _ , newIPv6 , _ := firstIPsFromStatus (remaining , opts .NetworkOrder )
239+ if newIPv4 != "" || newIPv6 != "" {
240+ if err := pasta .PestoAddPorts (n .config , n .PestoSocketPath (), opts .PortMappings ,
241+ newIPv4 , newIPv6 ); err != nil {
242+ return fmt .Errorf ("re-adding port mappings after remap: %w" , err )
243+ }
244+ }
245+ return nil
246+ }
247+
149248// Teardown will teardown the container network namespace.
150249func (n * netavarkNetwork ) Teardown (namespacePath string , options types.TeardownOptions ) error {
151250 n .lock .Lock ()
@@ -163,6 +262,19 @@ func (n *netavarkNetwork) Teardown(namespacePath string, options types.TeardownO
163262 logrus .Error (err )
164263 }
165264
265+ if n .rootlessPortForwarder == config .RootlessPortForwarderPasta && n .networkRootless && len (options .NetworkOptions .PortMappings ) > 0 {
266+ opts := options .NetworkOptions
267+ if len (opts .NetworkOrder ) == 0 {
268+ opts .NetworkOrder = make ([]string , 0 , len (opts .Networks ))
269+ for _ , net := range opts .Networks {
270+ opts .NetworkOrder = append (opts .NetworkOrder , net .Name )
271+ }
272+ }
273+ if err := n .pestoTeardown (opts ); err != nil {
274+ logrus .Errorf ("pesto: %v" , err )
275+ }
276+ }
277+
166278 netavarkOpts , needPlugin , err := n .convertNetOpts (options .NetworkOptions )
167279 if err != nil {
168280 return fmt .Errorf ("failed to convert net opts: %w" , err )
@@ -206,18 +318,6 @@ func (n *netavarkNetwork) getCommonNetavarkOptions(needPlugin bool) []string {
206318}
207319
208320func (n * netavarkNetwork ) convertNetOpts (opts types.NetworkOptions ) (* netavarkOptions , bool , error ) {
209- // In pasta mode, strip HostIP from port mappings. Pasta handles host-side
210- // address binding; netavark only needs DNAT rules inside the netns without
211- // "ip daddr" constraints (pasta's splice changes the destination IP).
212- if n .rootlessPortForwarder == config .RootlessPortForwarderPasta && n .networkRootless && len (opts .PortMappings ) > 0 {
213- stripped := make ([]types.PortMapping , len (opts .PortMappings ))
214- copy (stripped , opts .PortMappings )
215- for i := range stripped {
216- stripped [i ].HostIP = ""
217- }
218- opts .PortMappings = stripped
219- }
220-
221321 netavarkOptions := netavarkOptions {
222322 NetworkOptions : opts ,
223323 Networks : make (map [string ]* types.Network , len (opts .Networks )),
@@ -266,3 +366,31 @@ func (n *netavarkNetwork) PestoSocketPath() string {
266366 }
267367 return n .rootlessNetns .PestoSocketPath ()
268368}
369+
370+ // firstIPsFromStatus picks the first IPv4 and IPv6 container addresses from a
371+ // set of network StatusBlocks, iterating in the order given by networkOrder
372+ // (connection time). Returns the IP strings and the owning network name.
373+ func firstIPsFromStatus (status map [string ]types.StatusBlock , networkOrder []string ) (ipv4 , ipv4Net , ipv6 , ipv6Net string ) {
374+ for _ , name := range networkOrder {
375+ sb , ok := status [name ]
376+ if ! ok {
377+ continue
378+ }
379+ for _ , netInt := range sb .Interfaces {
380+ for _ , netAddr := range netInt .Subnets {
381+ ip := netAddr .IPNet .IP
382+ if ip .To4 () != nil && ipv4 == "" {
383+ ipv4 = ip .String ()
384+ ipv4Net = name
385+ } else if ip .To4 () == nil && ipv6 == "" {
386+ ipv6 = ip .String ()
387+ ipv6Net = name
388+ }
389+ }
390+ }
391+ if ipv4 != "" && ipv6 != "" {
392+ break
393+ }
394+ }
395+ return
396+ }
0 commit comments