Skip to content

Commit bc2ef3d

Browse files
authored
Merge pull request #1004 from Honny1/passta-and-pessto
pesto: target address mapping and netavark integration
2 parents 1820317 + f95a330 commit bc2ef3d

9 files changed

Lines changed: 430 additions & 118 deletions

File tree

common/libnetwork/internal/rootlessnetns/netns_linux.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,19 @@ func (n *Netns) setupPasta(nsPath string) error {
209209
extraOpts = append(extraOpts, "-c", n.getPath(pestoSocketFile))
210210
}
211211

212+
// For the rootless netns, hard-code IPv6 --map-guest-addr, --address, and
213+
// --gateway so pasta uses routable addresses for inbound IPv6 forwarding
214+
// (instead of link-local which is not routable across bridges).
215+
// The v4 --map-guest-addr must also be listed here because providing any
216+
// --map-guest-addr suppresses the default one added by createPastaArgs.
217+
// See: https://bugs.passt.top/show_bug.cgi?id=217
218+
extraOpts = append(extraOpts,
219+
"--map-guest-addr", pasta.MapGuestAddrIpv4,
220+
"--map-guest-addr", pasta.MapGuestAddrIpv6,
221+
"--address", pasta.GuestAddrIpv6,
222+
"--gateway", pasta.GatewayIpv6,
223+
)
224+
212225
pastaOpts := pasta.SetupOptions{
213226
Config: n.config,
214227
Netns: nsPath,

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: 140 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
150249
func (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

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

common/libnetwork/pasta/pasta_linux.go

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,26 @@ const (
3636
// mapGuestAddrIpv4 static ip used as forwarder address inside the netns to reach the host,
3737
// given this is a "link local" ip it should be very unlikely that it causes conflicts.
3838
mapGuestAddrIpv4 = "169.254.1.2"
39+
40+
// mapGuestAddrIpv6 static ip used as IPv6 forwarder address inside the netns to reach the host.
41+
mapGuestAddrIpv6 = "fc00::2"
42+
43+
// gatewayIpv6 is the IPv6 default gateway for the guest. pasta uses this as the source
44+
// address for inbound IPv6 forwarding. Must differ from mapGuestAddrIpv6 to avoid
45+
// --map-guest-addr intercepting reply traffic.
46+
// See: https://bugs.passt.top/show_bug.cgi?id=217
47+
gatewayIpv6 = "fc00::1"
48+
49+
// guestAddrIpv6 is assigned to the guest interface so the IPv6 gateway is reachable.
50+
guestAddrIpv6 = "fc00::3"
51+
)
52+
53+
// Exported IPv6 address constants for use by the rootless netns setup.
54+
const (
55+
MapGuestAddrIpv4 = mapGuestAddrIpv4
56+
MapGuestAddrIpv6 = mapGuestAddrIpv6
57+
GatewayIpv6 = gatewayIpv6
58+
GuestAddrIpv6 = guestAddrIpv6
3959
)
4060

4161
type SetupOptions struct {
@@ -67,39 +87,24 @@ func Setup(opts *SetupOptions) (*SetupResult, error) {
6787

6888
logrus.Debugf("pasta arguments: %s", strings.Join(cmdArgs, " "))
6989

70-
for {
71-
// pasta forks once ready, and quits once we delete the target namespace
72-
out, err := exec.Command(path, cmdArgs...).CombinedOutput()
73-
if err != nil {
74-
exitErr := &exec.ExitError{}
75-
if errors.As(err, &exitErr) {
76-
// special backwards compat check, --map-guest-addr was added in pasta version 20240814 so we
77-
// cannot hard require it yet. Once we are confident that the update is most distros we can remove it.
78-
if exitErr.ExitCode() == 1 &&
79-
strings.Contains(string(out), "unrecognized option '"+mapGuestAddrOpt) &&
80-
len(mapGuestAddrIPs) == 1 && mapGuestAddrIPs[0] == mapGuestAddrIpv4 {
81-
// we did add the default --map-guest-addr option, if users set something different we want
82-
// to get to the error below. We have to unset mapGuestAddrIPs here to avoid a infinite loop.
83-
mapGuestAddrIPs = nil
84-
// Trim off last two args which are --map-guest-addr 169.254.1.2.
85-
cmdArgs = cmdArgs[:len(cmdArgs)-2]
86-
continue
87-
}
88-
return nil, fmt.Errorf("pasta failed with exit code %d:\n%s",
89-
exitErr.ExitCode(), string(out))
90-
}
91-
return nil, fmt.Errorf("failed to start pasta: %w", err)
90+
// pasta forks once ready, and quits once we delete the target namespace
91+
out, err := exec.Command(path, cmdArgs...).CombinedOutput()
92+
if err != nil {
93+
exitErr := &exec.ExitError{}
94+
if errors.As(err, &exitErr) {
95+
return nil, fmt.Errorf("pasta failed with exit code %d:\n%s",
96+
exitErr.ExitCode(), string(out))
9297
}
98+
return nil, fmt.Errorf("failed to start pasta: %w", err)
99+
}
93100

94-
if len(out) > 0 {
95-
// TODO: This should be warning but as of August 2024 pasta still prints
96-
// things with --quiet that we do not care about. In podman CI I still see
97-
// "Couldn't get any nameserver address" so until this is fixed we cannot
98-
// enable it. For now info is fine and we can bump it up later, it is only a
99-
// nice to have.
100-
logrus.Infof("pasta logged warnings: %q", strings.TrimSpace(string(out)))
101-
}
102-
break
101+
if len(out) > 0 {
102+
// TODO: This should be warning but as of August 2024 pasta still prints
103+
// things with --quiet that we do not care about. In podman CI I still see
104+
// "Couldn't get any nameserver address" so until this is fixed we cannot
105+
// enable it. For now info is fine and we can bump it up later, it is only a
106+
// nice to have.
107+
logrus.Infof("pasta logged warnings: %q", strings.TrimSpace(string(out)))
103108
}
104109

105110
var ipv4, ipv6 bool
@@ -265,15 +270,12 @@ func createPastaArgs(opts *SetupOptions) ([]string, []string, []string, error) {
265270
cmdArgs = append(cmdArgs, "--quiet")
266271
}
267272

268-
cmdArgs = append(cmdArgs, "--netns", opts.Netns)
269-
270-
// do this as last arg so we can easily trim them off in the error case when we have an older version
271273
if len(mapGuestAddrIPs) == 0 {
272-
// the user did not request custom --map-guest-addr so add our own so that we can use this
273-
// for our own host.containers.internal host entry.
274274
cmdArgs = append(cmdArgs, mapGuestAddrOpt, mapGuestAddrIpv4)
275275
mapGuestAddrIPs = append(mapGuestAddrIPs, mapGuestAddrIpv4)
276276
}
277277

278+
cmdArgs = append(cmdArgs, "--netns", opts.Netns)
279+
278280
return cmdArgs, dnsForwardIPs, mapGuestAddrIPs, nil
279281
}

0 commit comments

Comments
 (0)