@@ -11,6 +11,7 @@ import (
1111
1212 "github.com/sirupsen/logrus"
1313 "go.podman.io/common/libnetwork/internal/util"
14+ "go.podman.io/common/libnetwork/pasta"
1415 "go.podman.io/common/libnetwork/types"
1516 "go.podman.io/common/pkg/config"
1617)
@@ -100,9 +101,104 @@ func (n *netavarkNetwork) Setup(namespacePath string, options types.SetupOptions
100101 return nil , fmt .Errorf ("unexpected netavark result length, want (%d), got (%d) networks" , len (options .Networks ), len (result ))
101102 }
102103
104+ if n .rootlessPortForwarder == config .RootlessPortForwarderPasta && n .networkRootless && len (options .NetworkOptions .PortMappings ) > 0 {
105+ opts := options .NetworkOptions
106+ if opts .NetworkStatus == nil {
107+ opts .NetworkStatus = result
108+ }
109+ if len (opts .NetworkOrder ) == 0 {
110+ opts .NetworkOrder = make ([]string , 0 , len (opts .Networks ))
111+ for _ , net := range opts .Networks {
112+ opts .NetworkOrder = append (opts .NetworkOrder , net .Name )
113+ }
114+ }
115+ if err := n .pestoSetup (opts ); err != nil {
116+ return nil , err
117+ }
118+ }
119+
103120 return result , err
104121}
105122
123+ // pestoSetup publishes port mappings via pesto with target address mapping.
124+ // PestoAddPorts is idempotent so this is safe to call on every Setup.
125+ func (n * netavarkNetwork ) pestoSetup (opts types.NetworkOptions ) error {
126+ if n .rootlessNetns == nil {
127+ return nil
128+ }
129+
130+ ipv4 , _ , ipv6 , _ := firstIPsFromStatus (opts .NetworkStatus , opts .NetworkOrder )
131+ if ipv4 == "" && ipv6 == "" {
132+ return nil
133+ }
134+ return pasta .PestoAddPorts (n .config , n .PestoSocketPath (), opts .PortMappings , ipv4 , ipv6 )
135+ }
136+
137+ // pestoTeardown handles pesto port mapping removal on network disconnect.
138+ // It derives the active forwarding IPs from the caller-provided NetworkStatus
139+ // (the podman db state) rather than maintaining a separate state file.
140+ // - If NetworkStatus is empty: nothing to do.
141+ // - If this is the last network: delete all mappings.
142+ // - If the disconnected network supplied the active IP: delete old mappings,
143+ // pick a replacement IP from the remaining networks, and re-add.
144+ // - Otherwise: no port changes needed.
145+ func (n * netavarkNetwork ) pestoTeardown (opts types.NetworkOptions ) error {
146+ if n .rootlessNetns == nil {
147+ return nil
148+ }
149+
150+ if len (opts .NetworkStatus ) == 0 {
151+ return nil
152+ }
153+
154+ // Determine current active IPs from the full set of connected networks,
155+ // respecting connection-time ordering from NetworkOrder.
156+ activeIPv4 , activeIPv4Net , activeIPv6 , activeIPv6Net := firstIPsFromStatus (opts .NetworkStatus , opts .NetworkOrder )
157+
158+ // Build the set of networks being disconnected and the remaining status.
159+ remaining := make (map [string ]types.StatusBlock , len (opts .NetworkStatus ))
160+ disconnectedNets := make (map [string ]struct {}, len (opts .Networks ))
161+ for _ , namedNet := range opts .Networks {
162+ disconnectedNets [namedNet .Name ] = struct {}{}
163+ }
164+ for name , sb := range opts .NetworkStatus {
165+ if _ , removed := disconnectedNets [name ]; ! removed {
166+ remaining [name ] = sb
167+ }
168+ }
169+
170+ // Last network: remove all port mappings.
171+ if len (remaining ) == 0 {
172+ return pasta .PestoDeletePorts (n .config , n .PestoSocketPath (), opts .PortMappings ,
173+ activeIPv4 , activeIPv6 )
174+ }
175+
176+ // Check whether any active IP came from a network being disconnected.
177+ _ , v4Lost := disconnectedNets [activeIPv4Net ]
178+ v4Lost = v4Lost && activeIPv4Net != ""
179+ _ , v6Lost := disconnectedNets [activeIPv6Net ]
180+ v6Lost = v6Lost && activeIPv6Net != ""
181+
182+ if ! v4Lost && ! v6Lost {
183+ return nil
184+ }
185+
186+ // Remap: delete old mappings, pick new IPs from remaining networks, re-add.
187+ if err := pasta .PestoDeletePorts (n .config , n .PestoSocketPath (), opts .PortMappings ,
188+ activeIPv4 , activeIPv6 ); err != nil {
189+ return fmt .Errorf ("deleting port mappings for remap: %w" , err )
190+ }
191+
192+ newIPv4 , _ , newIPv6 , _ := firstIPsFromStatus (remaining , opts .NetworkOrder )
193+ if newIPv4 != "" || newIPv6 != "" {
194+ if err := pasta .PestoAddPorts (n .config , n .PestoSocketPath (), opts .PortMappings ,
195+ newIPv4 , newIPv6 ); err != nil {
196+ return fmt .Errorf ("re-adding port mappings after remap: %w" , err )
197+ }
198+ }
199+ return nil
200+ }
201+
106202// Teardown will teardown the container network namespace.
107203func (n * netavarkNetwork ) Teardown (namespacePath string , options types.TeardownOptions ) error {
108204 n .lock .Lock ()
@@ -120,6 +216,19 @@ func (n *netavarkNetwork) Teardown(namespacePath string, options types.TeardownO
120216 logrus .Error (err )
121217 }
122218
219+ if n .rootlessPortForwarder == config .RootlessPortForwarderPasta && n .networkRootless && len (options .NetworkOptions .PortMappings ) > 0 {
220+ opts := options .NetworkOptions
221+ if len (opts .NetworkOrder ) == 0 {
222+ opts .NetworkOrder = make ([]string , 0 , len (opts .Networks ))
223+ for _ , net := range opts .Networks {
224+ opts .NetworkOrder = append (opts .NetworkOrder , net .Name )
225+ }
226+ }
227+ if err := n .pestoTeardown (opts ); err != nil {
228+ logrus .Errorf ("pesto: %v" , err )
229+ }
230+ }
231+
123232 netavarkOpts , needPlugin , err := n .convertNetOpts (options .NetworkOptions )
124233 if err != nil {
125234 return fmt .Errorf ("failed to convert net opts: %w" , err )
@@ -163,18 +272,6 @@ func (n *netavarkNetwork) getCommonNetavarkOptions(needPlugin bool) []string {
163272}
164273
165274func (n * netavarkNetwork ) convertNetOpts (opts types.NetworkOptions ) (* netavarkOptions , bool , error ) {
166- // In pasta mode, strip HostIP from port mappings. Pasta handles host-side
167- // address binding; netavark only needs DNAT rules inside the netns without
168- // "ip daddr" constraints (pasta's splice changes the destination IP).
169- if n .rootlessPortForwarder == config .RootlessPortForwarderPasta && n .networkRootless && len (opts .PortMappings ) > 0 {
170- stripped := make ([]types.PortMapping , len (opts .PortMappings ))
171- copy (stripped , opts .PortMappings )
172- for i := range stripped {
173- stripped [i ].HostIP = ""
174- }
175- opts .PortMappings = stripped
176- }
177-
178275 netavarkOptions := netavarkOptions {
179276 NetworkOptions : opts ,
180277 Networks : make (map [string ]* types.Network , len (opts .Networks )),
@@ -223,3 +320,31 @@ func (n *netavarkNetwork) PestoSocketPath() string {
223320 }
224321 return n .rootlessNetns .PestoSocketPath ()
225322}
323+
324+ // firstIPsFromStatus picks the first IPv4 and IPv6 container addresses from a
325+ // set of network StatusBlocks, iterating in the order given by networkOrder
326+ // (connection time). Returns the IP strings and the owning network name.
327+ func firstIPsFromStatus (status map [string ]types.StatusBlock , networkOrder []string ) (ipv4 , ipv4Net , ipv6 , ipv6Net string ) {
328+ for _ , name := range networkOrder {
329+ sb , ok := status [name ]
330+ if ! ok {
331+ continue
332+ }
333+ for _ , netInt := range sb .Interfaces {
334+ for _ , netAddr := range netInt .Subnets {
335+ ip := netAddr .IPNet .IP
336+ if ip .To4 () != nil && ipv4 == "" {
337+ ipv4 = ip .String ()
338+ ipv4Net = name
339+ } else if ip .To4 () == nil && ipv6 == "" {
340+ ipv6 = ip .String ()
341+ ipv6Net = name
342+ }
343+ }
344+ }
345+ if ipv4 != "" && ipv6 != "" {
346+ break
347+ }
348+ }
349+ return
350+ }
0 commit comments