Skip to content

Commit abc89b3

Browse files
committed
pesto: integrate port forwarding into netavark Setup/Teardown with state tracking
Fixes: podman-container-tools/podman#28769 Signed-off-by: Jan Rodák <hony.com@seznam.cz>
1 parent daab55b commit abc89b3

6 files changed

Lines changed: 194 additions & 32 deletions

File tree

common/libnetwork/internal/rootlessnetns/netns_linux.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ func (n *Netns) getOrCreateNetns() (netns.NetNS, bool, error) {
132132
logrus.Warnf("failed to read rootless netns program pid: %v", err)
133133
}
134134
// In case of errors continue and setup the network cmd again.
135+
// When pasta restarts all forwarding rules are lost.
136+
// pestoSetup detects this via pesto --show and re-publishes
137+
// on the next Setup call.
135138
} else {
136139
// Special case, the file might exist already but is not a valid netns.
137140
// One reason could be that a previous setup was killed between creating
@@ -634,6 +637,7 @@ func (n *Netns) PestoSocketPath() string {
634637
return n.getPath(pestoSocketFile)
635638
}
636639

640+
637641
func refCount(dir string, inc int) (int, error) {
638642
file := filepath.Join(dir, refCountFile)
639643
content, err := os.ReadFile(file)

common/libnetwork/netavark/network.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,13 @@ type netavarkNetwork struct {
7373
rootlessNetns *rootlessnetns.Netns
7474

7575
// rootlessPortForwarder is the value of config.RootlessPortForwarder from
76-
// containers.conf. When set to config.RootlessPortForwarderPasta, HostIP
77-
// is stripped from port mappings before passing to netavark because pasta's
78-
// splice changes the destination IP.
76+
// containers.conf. When set to config.RootlessPortForwarderPasta, pesto
77+
// handles port forwarding directly and netavark DNAT rules are skipped.
7978
rootlessPortForwarder string
79+
80+
// config is the containers.conf config, needed for FindHelperBinary
81+
// when invoking pesto for dynamic port forwarding.
82+
config *config.Config
8083
}
8184

8285
type InitConfig struct {
@@ -167,6 +170,7 @@ func NewNetworkInterface(conf *InitConfig) (types.ContainerNetwork, error) {
167170
syslog: conf.Syslog,
168171
rootlessNetns: netns,
169172
rootlessPortForwarder: conf.Config.Network.RootlessPortForwarder,
173+
config: conf.Config,
170174
}
171175

172176
return n, nil

common/libnetwork/netavark/run.go

Lines changed: 137 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
107203
func (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

165274
func (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+
}

common/libnetwork/pasta/pasta_linux_test.go

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func Test_createPastaArgs(t *testing.T) {
4242
"--netns", "netns123",
4343
},
4444
wantDNSForward: []string{dnsForwardIpv4},
45-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
45+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
4646
},
4747
{
4848
name: "basic port",
@@ -59,7 +59,7 @@ func Test_createPastaArgs(t *testing.T) {
5959
"--netns", "netns123",
6060
},
6161
wantDNSForward: []string{dnsForwardIpv4},
62-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
62+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
6363
},
6464
{
6565
name: "port range",
@@ -76,7 +76,7 @@ func Test_createPastaArgs(t *testing.T) {
7676
"--netns", "netns123",
7777
},
7878
wantDNSForward: []string{dnsForwardIpv4},
79-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
79+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
8080
},
8181
{
8282
name: "different host and container port",
@@ -93,7 +93,7 @@ func Test_createPastaArgs(t *testing.T) {
9393
"--netns", "netns123",
9494
},
9595
wantDNSForward: []string{dnsForwardIpv4},
96-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
96+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
9797
},
9898
{
9999
name: "tcp and udp port",
@@ -113,7 +113,7 @@ func Test_createPastaArgs(t *testing.T) {
113113
"--netns", "netns123",
114114
},
115115
wantDNSForward: []string{dnsForwardIpv4},
116-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
116+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
117117
},
118118
{
119119
name: "two tcp ports",
@@ -133,7 +133,7 @@ func Test_createPastaArgs(t *testing.T) {
133133
"--netns", "netns123",
134134
},
135135
wantDNSForward: []string{dnsForwardIpv4},
136-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
136+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
137137
},
138138
{
139139
name: "invalid port",
@@ -161,7 +161,7 @@ func Test_createPastaArgs(t *testing.T) {
161161
"--netns", "netns123",
162162
},
163163
wantDNSForward: []string{dnsForwardIpv4},
164-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
164+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
165165
},
166166
{
167167
name: "config options before extra options",
@@ -178,7 +178,7 @@ func Test_createPastaArgs(t *testing.T) {
178178
"--netns", "netns123",
179179
},
180180
wantDNSForward: []string{dnsForwardIpv4},
181-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
181+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
182182
},
183183
{
184184
name: "-T option",
@@ -195,7 +195,7 @@ func Test_createPastaArgs(t *testing.T) {
195195
"--netns", "netns123",
196196
},
197197
wantDNSForward: []string{dnsForwardIpv4},
198-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
198+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
199199
},
200200
{
201201
name: "--tcp-ns option",
@@ -212,7 +212,7 @@ func Test_createPastaArgs(t *testing.T) {
212212
"--netns", "netns123",
213213
},
214214
wantDNSForward: []string{dnsForwardIpv4},
215-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
215+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
216216
},
217217
{
218218
name: "--map-gw option",
@@ -229,7 +229,7 @@ func Test_createPastaArgs(t *testing.T) {
229229
"--netns", "netns123",
230230
},
231231
wantDNSForward: []string{dnsForwardIpv4},
232-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
232+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
233233
},
234234
{
235235
// https://github.com/containers/podman/issues/22477
@@ -246,7 +246,7 @@ func Test_createPastaArgs(t *testing.T) {
246246
"--netns", "netns123",
247247
},
248248
wantDNSForward: []string{dnsForwardIpv4},
249-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
249+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
250250
},
251251
{
252252
name: "two --map-gw",
@@ -263,7 +263,7 @@ func Test_createPastaArgs(t *testing.T) {
263263
"--netns", "netns123",
264264
},
265265
wantDNSForward: []string{dnsForwardIpv4},
266-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
266+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
267267
},
268268
{
269269
name: "--dns-forward option",
@@ -280,7 +280,7 @@ func Test_createPastaArgs(t *testing.T) {
280280
"--netns", "netns123",
281281
},
282282
wantDNSForward: []string{"192.168.255.255"},
283-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
283+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
284284
},
285285
{
286286
name: "two --dns-forward options",
@@ -297,7 +297,7 @@ func Test_createPastaArgs(t *testing.T) {
297297
"--netns", "netns123",
298298
},
299299
wantDNSForward: []string{"192.168.255.255", "::1"},
300-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
300+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
301301
},
302302
{
303303
name: "port and custom opt",
@@ -314,7 +314,7 @@ func Test_createPastaArgs(t *testing.T) {
314314
"--netns", "netns123",
315315
},
316316
wantDNSForward: []string{dnsForwardIpv4},
317-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
317+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
318318
},
319319
{
320320
name: "Add verbose logging",
@@ -332,7 +332,7 @@ func Test_createPastaArgs(t *testing.T) {
332332
"--netns", "netns123",
333333
},
334334
wantDNSForward: []string{dnsForwardIpv4},
335-
wantMapGuestAddr: []string{mapGuestAddrIpv4},
335+
wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6},
336336
},
337337
{
338338
name: "--map-guest-addr option",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package pasta
2+
3+
import (
4+
"errors"
5+
6+
"go.podman.io/common/libnetwork/types"
7+
"go.podman.io/common/pkg/config"
8+
)
9+
10+
var errPestoNotSupported = errors.New("pesto is not supported on FreeBSD")
11+
12+
func PestoAddPorts(_ *config.Config, _ string, _ []types.PortMapping, _, _ string) error {
13+
return errPestoNotSupported
14+
}
15+
16+
func PestoDeletePorts(_ *config.Config, _ string, _ []types.PortMapping, _, _ string) error {
17+
return errPestoNotSupported
18+
}

0 commit comments

Comments
 (0)