File: pasta_linux.go

package info (click to toggle)
golang-github-containers-common 0.64.1%2Bds1-2
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 5,932 kB
  • sloc: makefile: 132; sh: 111
file content (280 lines) | stat: -rw-r--r-- 9,090 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
// SPDX-License-Identifier: Apache-2.0
//
// pasta.go - Start pasta(1) for user-mode connectivity
//
// Copyright (c) 2022 Red Hat GmbH
// Author: Stefano Brivio <sbrivio@redhat.com>

// This file has been imported from the podman repository
// (libpod/networking_pasta_linux.go), for the full history see there.

package pasta

import (
	"errors"
	"fmt"
	"net"
	"os/exec"
	"slices"
	"strings"

	"github.com/containernetworking/plugins/pkg/ns"
	"github.com/containers/common/libnetwork/types"
	"github.com/containers/common/libnetwork/util"
	"github.com/containers/common/pkg/config"
	"github.com/sirupsen/logrus"
)

const (
	dnsForwardOpt   = "--dns-forward"
	mapGuestAddrOpt = "--map-guest-addr"

	// dnsForwardIpv4 static ip used as nameserver address inside the netns,
	// given this is a "link local" ip it should be very unlikely that it causes conflicts
	dnsForwardIpv4 = "169.254.1.1"

	// mapGuestAddrIpv4 static ip used as forwarder address inside the netns to reach the host,
	// given this is a "link local" ip it should be very unlikely that it causes conflicts
	mapGuestAddrIpv4 = "169.254.1.2"
)

type SetupOptions struct {
	// Config used to get pasta options and binary path via HelperBinariesDir
	Config *config.Config
	// Netns is the path to the container Netns
	Netns string
	// Ports that should be forwarded in the container
	Ports []types.PortMapping
	// ExtraOptions are pasta(1) cli options, these will be appended after the
	// pasta options from containers.conf to allow some form of overwrite.
	ExtraOptions []string
}

// Setup start the pasta process for the given netns.
// The pasta binary is looked up in the HelperBinariesDir and $PATH.
// Note that there is no need for any special cleanup logic, the pasta
// process will automatically exit when the netns path is deleted.
func Setup(opts *SetupOptions) (*SetupResult, error) {
	path, err := opts.Config.FindHelperBinary(BinaryName, true)
	if err != nil {
		return nil, fmt.Errorf("could not find pasta, the network namespace can't be configured: %w", err)
	}

	cmdArgs, dnsForwardIPs, mapGuestAddrIPs, err := createPastaArgs(opts)
	if err != nil {
		return nil, err
	}

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

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

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

	var ipv4, ipv6 bool
	result := &SetupResult{}
	err = ns.WithNetNSPath(opts.Netns, func(_ ns.NetNS) error {
		addrs, err := net.InterfaceAddrs()
		if err != nil {
			return err
		}
		for _, addr := range addrs {
			// make sure to skip loopback and multicast addresses
			if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && !ipnet.IP.IsMulticast() {
				if util.IsIPv4(ipnet.IP) {
					result.IPAddresses = append(result.IPAddresses, ipnet.IP)
					ipv4 = true
				} else if !ipnet.IP.IsLinkLocalUnicast() {
					// Else must be ipv6.
					// We shouldn't resolve hosts.containers.internal to IPv6
					// link-local addresses, for two reasons:
					// 1. even if IPv6 is disabled in pasta (--ipv4-only), the
					//    kernel will configure an IPv6 link-local address in the
					//    container, but that doesn't mean that IPv6 connectivity
					//    is actually working
					// 2. link-local addresses need to be suffixed by the zone
					//    (interface) to be of any use, but we can't do it here
					//
					// Thus, don't include IPv6 link-local addresses in
					// IPAddresses: Podman uses them for /etc/hosts entries, and
					// those need to be functional.
					result.IPAddresses = append(result.IPAddresses, ipnet.IP)
					ipv6 = true
				}
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	result.IPv6 = ipv6
	result.DNSForwardIPs = filterIpFamily(dnsForwardIPs, ipv4, ipv6)
	result.MapGuestAddrIPs = filterIpFamily(mapGuestAddrIPs, ipv4, ipv6)

	return result, nil
}

func filterIpFamily(ips []string, ipv4, ipv6 bool) []string {
	var result []string
	for _, ip := range ips {
		ipp := net.ParseIP(ip)
		// add the ip only if the address family matches
		if ipv4 && util.IsIPv4(ipp) || ipv6 && util.IsIPv6(ipp) {
			result = append(result, ip)
		}
	}
	return result
}

// createPastaArgs creates the pasta arguments, it returns the args to be passed to pasta(1)
// and as second arg the dns forward ips used. As third arg the map guest addr ips used.
func createPastaArgs(opts *SetupOptions) ([]string, []string, []string, error) {
	noTCPInitPorts := true
	noUDPInitPorts := true
	noTCPNamespacePorts := true
	noUDPNamespacePorts := true
	noMapGW := true
	quiet := true

	cmdArgs := []string{"--config-net"}
	// first append options set in the config
	cmdArgs = append(cmdArgs, opts.Config.Network.PastaOptions.Get()...)
	// then append the ones that were set on the cli
	cmdArgs = append(cmdArgs, opts.ExtraOptions...)

	cmdArgs = slices.DeleteFunc(cmdArgs, func(s string) bool {
		// --map-gw is not a real pasta(1) option so we must remove it
		// and not add --no-map-gw below
		if s == "--map-gw" {
			noMapGW = false
			return true
		}
		return false
	})

	var dnsForwardIPs []string
	var mapGuestAddrIPs []string
	for i, opt := range cmdArgs {
		switch opt {
		case "-t", "--tcp-ports":
			noTCPInitPorts = false
		case "-u", "--udp-ports":
			noUDPInitPorts = false
		case "-T", "--tcp-ns":
			noTCPNamespacePorts = false
		case "-U", "--udp-ns":
			noUDPNamespacePorts = false
		case "-d", "--debug", "--trace":
			quiet = false
		case dnsForwardOpt:
			// if there is no arg after it pasta will likely error out anyway due invalid cli args
			if len(cmdArgs) > i+1 {
				dnsForwardIPs = append(dnsForwardIPs, cmdArgs[i+1])
			}
		case mapGuestAddrOpt:
			if len(cmdArgs) > i+1 {
				mapGuestAddrIPs = append(mapGuestAddrIPs, cmdArgs[i+1])
			}
		}
	}

	for _, i := range opts.Ports {
		protocols := strings.Split(i.Protocol, ",")
		for _, protocol := range protocols {
			var addr string

			if i.HostIP != "" {
				addr = i.HostIP + "/"
			}

			switch protocol {
			case "tcp":
				noTCPInitPorts = false
				cmdArgs = append(cmdArgs, "-t")
			case "udp":
				noUDPInitPorts = false
				cmdArgs = append(cmdArgs, "-u")
			default:
				return nil, nil, nil, fmt.Errorf("can't forward protocol: %s", protocol)
			}

			arg := fmt.Sprintf("%s%d-%d:%d-%d", addr,
				i.HostPort,
				i.HostPort+i.Range-1,
				i.ContainerPort,
				i.ContainerPort+i.Range-1)
			cmdArgs = append(cmdArgs, arg)
		}
	}

	if len(dnsForwardIPs) == 0 {
		// the user did not request custom --dns-forward so add our own.
		cmdArgs = append(cmdArgs, dnsForwardOpt, dnsForwardIpv4)
		dnsForwardIPs = append(dnsForwardIPs, dnsForwardIpv4)
	}

	if noTCPInitPorts {
		cmdArgs = append(cmdArgs, "-t", "none")
	}
	if noUDPInitPorts {
		cmdArgs = append(cmdArgs, "-u", "none")
	}
	if noTCPNamespacePorts {
		cmdArgs = append(cmdArgs, "-T", "none")
	}
	if noUDPNamespacePorts {
		cmdArgs = append(cmdArgs, "-U", "none")
	}
	if noMapGW {
		cmdArgs = append(cmdArgs, "--no-map-gw")
	}
	if quiet {
		// pass --quiet to silence the info output from pasta if verbose/trace pasta is not required
		cmdArgs = append(cmdArgs, "--quiet")
	}

	cmdArgs = append(cmdArgs, "--netns", opts.Netns)

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

	return cmdArgs, dnsForwardIPs, mapGuestAddrIPs, nil
}