buildah: add pasta(1) support

Like podman allow buildah and therefore podman build to use the network
mode pasta. The pasta integration is very simple and we do not even
need a teardown handler for that as pasta will exit on its own when the
netns path is removed.

However right now this is broken, pasta will fail to open
/proc/$pid/ns/net. I send a patch[1] to fix this upstream in pasta.
I assume this will land quickly so I like to get this in now just so we
have this included in podman v4.6. Thus the test is skipped for now.

[1] https://archives.passt.top/passt-dev/20230623082531.25947-2-pholzing@redhat.com/

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
Paul Holzinger 2023-06-22 18:14:50 +02:00
parent 11ba328e2b
commit 74b885b9e2
No known key found for this signature in database
GPG Key ID: EB145DD938A3CAF2
9 changed files with 348 additions and 6 deletions

View File

@ -542,6 +542,43 @@ Valid _mode_ values are:
- **outbound_addr=IPv4**: Specify the outbound ipv4 address slirp binds to.
- **outbound_addr6=INTERFACE**: Specify the outbound interface slirp binds to (ipv6 traffic only).
- **outbound_addr6=IPv6**: Specify the outbound ipv6 address slirp binds to.
- **pasta[:OPTIONS,...]**: use **pasta**(1) to create a user-mode networking
stack. \
This is only supported in rootless mode. \
By default, IPv4 and IPv6 addresses and routes, as well as the pod interface
name, are copied from the host. If port forwarding isn't configured, ports
are forwarded dynamically as services are bound on either side (init
namespace or container namespace). Port forwarding preserves the original
source IP address. Options described in pasta(1) can be specified as
comma-separated arguments. \
In terms of pasta(1) options, **--config-net** is given by default, in
order to configure networking when the container is started, and
**--no-map-gw** is also assumed by default, to avoid direct access from
container to host using the gateway address. The latter can be overridden
by passing **--map-gw** in the pasta-specific options (despite not being an
actual pasta(1) option). \
Also, **-t none** and **-u none** are passed to disable
automatic port forwarding based on bound ports. Similarly, **-T none** and
**-U none** are given to disable the same functionality from container to
host. \
Some examples:
- **pasta:--map-gw**: Allow the container to directly reach the host using the
gateway address.
- **pasta:--mtu,1500**: Specify a 1500 bytes MTU for the _tap_ interface in
the container.
- **pasta:--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,-m,1500,--no-ndp,--no-dhcpv6,--no-dhcp**,
equivalent to default slirp4netns(1) options: disable IPv6, assign
`10.0.2.0/24` to the `tap0` interface in the container, with gateway
`10.0.2.3`, enable DNS forwarder reachable at `10.0.2.3`, set MTU to 1500
bytes, disable NDP, DHCPv6 and DHCP support.
- **pasta:-I,tap0,--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,--no-ndp,--no-dhcpv6,--no-dhcp**,
equivalent to default slirp4netns(1) options with Podman overrides: same as
above, but leave the MTU to 65520 bytes
- **pasta:-t,auto,-u,auto,-T,auto,-U,auto**: enable automatic port forwarding
based on observed bound ports from both host and container sides
- **pasta:-T,5201**: enable forwarding of TCP port 5201 from container to
host, using the loopback interface instead of the tap interface for improved
performance
**--no-cache**

View File

@ -300,6 +300,43 @@ Valid _mode_ values are:
- **outbound_addr=IPv4**: Specify the outbound ipv4 address slirp binds to.
- **outbound_addr6=INTERFACE**: Specify the outbound interface slirp binds to (ipv6 traffic only).
- **outbound_addr6=IPv6**: Specify the outbound ipv6 address slirp binds to.
- **pasta[:OPTIONS,...]**: use **pasta**(1) to create a user-mode networking
stack. \
This is only supported in rootless mode. \
By default, IPv4 and IPv6 addresses and routes, as well as the pod interface
name, are copied from the host. If port forwarding isn't configured, ports
are forwarded dynamically as services are bound on either side (init
namespace or container namespace). Port forwarding preserves the original
source IP address. Options described in pasta(1) can be specified as
comma-separated arguments. \
In terms of pasta(1) options, **--config-net** is given by default, in
order to configure networking when the container is started, and
**--no-map-gw** is also assumed by default, to avoid direct access from
container to host using the gateway address. The latter can be overridden
by passing **--map-gw** in the pasta-specific options (despite not being an
actual pasta(1) option). \
Also, **-t none** and **-u none** are passed to disable
automatic port forwarding based on bound ports. Similarly, **-T none** and
**-U none** are given to disable the same functionality from container to
host. \
Some examples:
- **pasta:--map-gw**: Allow the container to directly reach the host using the
gateway address.
- **pasta:--mtu,1500**: Specify a 1500 bytes MTU for the _tap_ interface in
the container.
- **pasta:--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,-m,1500,--no-ndp,--no-dhcpv6,--no-dhcp**,
equivalent to default slirp4netns(1) options: disable IPv6, assign
`10.0.2.0/24` to the `tap0` interface in the container, with gateway
`10.0.2.3`, enable DNS forwarder reachable at `10.0.2.3`, set MTU to 1500
bytes, disable NDP, DHCPv6 and DHCP support.
- **pasta:-I,tap0,--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,--no-ndp,--no-dhcpv6,--no-dhcp**,
equivalent to default slirp4netns(1) options with Podman overrides: same as
above, but leave the MTU to 65520 bytes
- **pasta:-t,auto,-u,auto,-T,auto,-U,auto**: enable automatic port forwarding
based on observed bound ports from both host and container sides
- **pasta:-T,5201**: enable forwarding of TCP port 5201 from container to
host, using the loopback interface instead of the tap interface for improved
performance
**--os**="OS"

View File

@ -179,6 +179,43 @@ Valid _mode_ values are:
- **outbound_addr=IPv4**: Specify the outbound ipv4 address slirp binds to.
- **outbound_addr6=INTERFACE**: Specify the outbound interface slirp binds to (ipv6 traffic only).
- **outbound_addr6=IPv6**: Specify the outbound ipv6 address slirp binds to.
- **pasta[:OPTIONS,...]**: use **pasta**(1) to create a user-mode networking
stack. \
This is only supported in rootless mode. \
By default, IPv4 and IPv6 addresses and routes, as well as the pod interface
name, are copied from the host. If port forwarding isn't configured, ports
are forwarded dynamically as services are bound on either side (init
namespace or container namespace). Port forwarding preserves the original
source IP address. Options described in pasta(1) can be specified as
comma-separated arguments. \
In terms of pasta(1) options, **--config-net** is given by default, in
order to configure networking when the container is started, and
**--no-map-gw** is also assumed by default, to avoid direct access from
container to host using the gateway address. The latter can be overridden
by passing **--map-gw** in the pasta-specific options (despite not being an
actual pasta(1) option). \
Also, **-t none** and **-u none** are passed to disable
automatic port forwarding based on bound ports. Similarly, **-T none** and
**-U none** are given to disable the same functionality from container to
host. \
Some examples:
- **pasta:--map-gw**: Allow the container to directly reach the host using the
gateway address.
- **pasta:--mtu,1500**: Specify a 1500 bytes MTU for the _tap_ interface in
the container.
- **pasta:--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,-m,1500,--no-ndp,--no-dhcpv6,--no-dhcp**,
equivalent to default slirp4netns(1) options: disable IPv6, assign
`10.0.2.0/24` to the `tap0` interface in the container, with gateway
`10.0.2.3`, enable DNS forwarder reachable at `10.0.2.3`, set MTU to 1500
bytes, disable NDP, DHCPv6 and DHCP support.
- **pasta:-I,tap0,--ipv4-only,-a,10.0.2.0,-n,24,-g,10.0.2.2,--dns-forward,10.0.2.3,--no-ndp,--no-dhcpv6,--no-dhcp**,
equivalent to default slirp4netns(1) options with Podman overrides: same as
above, but leave the MTU to 65520 bytes
- **pasta:-t,auto,-u,auto,-T,auto,-U,auto**: enable automatic port forwarding
based on observed bound ports from both host and container sides
- **pasta:-T,5201**: enable forwarding of TCP port 5201 from container to
host, using the loopback interface instead of the tap interface for improved
performance
**--no-hosts**

2
go.mod
View File

@ -5,6 +5,7 @@ go 1.18
require (
github.com/containerd/containerd v1.7.2
github.com/containernetworking/cni v1.1.2
github.com/containernetworking/plugins v1.3.0
github.com/containers/common v0.53.1-0.20230622141517-6eebb3712106
github.com/containers/image/v5 v5.25.1-0.20230613183705-07ced6137083
github.com/containers/ocicrypt v1.1.7
@ -51,7 +52,6 @@ require (
github.com/container-orchestrated-devices/container-device-interface v0.5.4 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/containernetworking/plugins v1.3.0 // indirect
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20230514072755-504adb8a8af1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect

View File

@ -13,6 +13,7 @@ import (
"strings"
"syscall"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containers/buildah/bind"
"github.com/containers/buildah/chroot"
"github.com/containers/buildah/copier"
@ -22,9 +23,11 @@ import (
"github.com/containers/buildah/pkg/overlay"
"github.com/containers/buildah/pkg/parse"
"github.com/containers/buildah/util"
"github.com/containers/common/libnetwork/pasta"
"github.com/containers/common/libnetwork/resolvconf"
"github.com/containers/common/libnetwork/slirp4netns"
nettypes "github.com/containers/common/libnetwork/types"
netUtil "github.com/containers/common/libnetwork/util"
"github.com/containers/common/pkg/capabilities"
"github.com/containers/common/pkg/chown"
"github.com/containers/common/pkg/config"
@ -516,19 +519,64 @@ func setupSlirp4netnsNetwork(netns, cid string, options []string) (func(), map[s
}, netStatus, nil
}
func setupPasta(netns string, options []string) (func(), map[string]nettypes.StatusBlock, error) {
defConfig, err := config.Default()
if err != nil {
return nil, nil, fmt.Errorf("failed to get container config: %w", err)
}
err = pasta.Setup(&pasta.SetupOptions{
Config: defConfig,
Netns: netns,
ExtraOptions: options,
})
if err != nil {
return nil, nil, err
}
var ip string
err = ns.WithNetNSPath(netns, func(_ ns.NetNS) error {
// get the first ip in the netns and use this as our ip for /etc/hosts
ip = netUtil.GetLocalIP()
return nil
})
if err != nil {
return nil, nil, err
}
// create fake status to make sure we get the correct ip in hosts
subnet := nettypes.IPNet{IPNet: net.IPNet{
IP: net.ParseIP(ip),
Mask: net.IPv4Mask(255, 255, 255, 0),
}}
netStatus := map[string]nettypes.StatusBlock{
slirp4netns.BinaryName: nettypes.StatusBlock{
Interfaces: map[string]nettypes.NetInterface{
"tap0": {
Subnets: []nettypes.NetAddress{{IPNet: subnet}},
},
},
},
}
return nil, netStatus, nil
}
func (b *Builder) runConfigureNetwork(pid int, isolation define.Isolation, options RunOptions, network, containerName string) (teardown func(), netStatus map[string]nettypes.StatusBlock, err error) {
netns := fmt.Sprintf("/proc/%d/ns/net", pid)
var configureNetworks []string
name, networkOpts, hasOpts := strings.Cut(network, ":")
var netOpts []string
if hasOpts {
netOpts = strings.Split(networkOpts, ",")
}
switch {
case name == slirp4netns.BinaryName,
isolation == IsolationOCIRootless && name == "":
var slirpOpts []string
if hasOpts {
slirpOpts = strings.Split(networkOpts, ",")
}
return setupSlirp4netnsNetwork(netns, containerName, slirpOpts)
return setupSlirp4netnsNetwork(netns, containerName, netOpts)
case name == pasta.BinaryName:
return setupPasta(netns, netOpts)
// Basically default case except we make sure to not split an empty
// name as this would return a slice with one empty string which is

View File

@ -6008,6 +6008,30 @@ _EOF
assert "$output" =~ "mtu 2000" "ip addr shows mtu 2000"
}
@test "bud with --network pasta" {
skip_if_no_runtime
skip_if_chroot
skip_if_root_environment "pasta only works rootless"
# FIXME: unskip when we have a new pasta version with:
# https://archives.passt.top/passt-dev/20230623082531.25947-2-pholzing@redhat.com/
skip "pasta bug prevents this from working"
_prefetch alpine
# pasta by default copies the host ip
ip=$(hostname -I | cut -f 1 -d " ")
run_buildah bud $WITH_POLICY_JSON --network pasta $BUDFILES/network
assert "$output" =~ "$ip" "ip addr shows default subnet"
# check some entwork options, it accepts raw pasta(1) areguments
mac="9a:dd:31:ea:92:98"
run_buildah bud $WITH_POLICY_JSON --network pasta:--mtu,2000,--ns-mac-addr,"$mac" $BUDFILES/network
assert "$output" =~ "$mac" "ip addr shows custom mac address"
assert "$output" =~ "mtu 2000" "ip addr shows mtu 2000"
}
@test "bud WORKDIR owned by USER" {
_prefetch alpine
target=alpine-image

View File

@ -711,6 +711,24 @@ function configure_and_check_user() {
run_buildah rm -a
}
@test "run check /etc/hosts with --network pasta" {
skip_if_no_runtime
skip_if_chroot
skip_if_root_environment "pasta only works rootless"
# FIXME: unskip when we have a new pasta version with:
# https://archives.passt.top/passt-dev/20230623082531.25947-2-pholzing@redhat.com/
skip "pasta bug prevents this from working"
run_buildah from --quiet --pull=false $WITH_POLICY_JSON debian
cid=$output
local hostname=h-$(random_string)
ip=$(hostname -I | cut -f 1 -d " ")
run_buildah run --network pasta --hostname $hostname $cid cat /etc/hosts
assert "$output" =~ "$ip[[:blank:]]$hostname $cid"
}
@test "run check /etc/resolv.conf" {
skip_if_rootless_environment
skip_if_no_runtime

View File

@ -0,0 +1,140 @@
// 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"
"os/exec"
"strings"
"github.com/containers/common/libnetwork/types"
"github.com/containers/common/pkg/config"
"github.com/sirupsen/logrus"
)
const (
BinaryName = "pasta"
)
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 any special cleanup logic, the pasta process will
// automatically exit when the netns path is deleted.
func Setup(opts *SetupOptions) error {
NoTCPInitPorts := true
NoUDPInitPorts := true
NoTCPNamespacePorts := true
NoUDPNamespacePorts := true
NoMapGW := true
path, err := opts.Config.FindHelperBinary(BinaryName, true)
if err != nil {
return fmt.Errorf("could not find pasta, the network namespace can't be configured: %w", err)
}
cmdArgs := []string{}
cmdArgs = append(cmdArgs, "--config-net")
for _, i := range opts.Ports {
protocols := strings.Split(i.Protocol, ",")
for _, protocol := range protocols {
var addr string
if i.HostIP != "" {
addr = fmt.Sprintf("%s/", i.HostIP)
}
switch protocol {
case "tcp":
cmdArgs = append(cmdArgs, "-t")
case "udp":
cmdArgs = append(cmdArgs, "-u")
default:
return 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)
}
}
// first append options set in the config
cmdArgs = append(cmdArgs, opts.Config.Network.PastaOptions...)
// then append the ones that were set on the cli
cmdArgs = append(cmdArgs, opts.ExtraOptions...)
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 "--map-gw":
NoMapGW = false
// not an actual pasta(1) option
cmdArgs = append(cmdArgs[:i], cmdArgs[i+1:]...)
}
}
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")
}
cmdArgs = append(cmdArgs, "--netns", opts.Netns)
logrus.Debugf("pasta arguments: %s", strings.Join(cmdArgs, " "))
// pasta forks once ready, and quits once we delete the target namespace
_, err = exec.Command(path, cmdArgs...).Output()
if err != nil {
exitErr := &exec.ExitError{}
if errors.As(err, &exitErr) {
return fmt.Errorf("pasta failed with exit code %d:\n%s",
exitErr.ExitCode(), exitErr.Stderr)
}
return fmt.Errorf("failed to start pasta: %w", err)
}
return nil
}

1
vendor/modules.txt vendored
View File

@ -98,6 +98,7 @@ github.com/containers/common/libnetwork/etchosts
github.com/containers/common/libnetwork/internal/util
github.com/containers/common/libnetwork/netavark
github.com/containers/common/libnetwork/network
github.com/containers/common/libnetwork/pasta
github.com/containers/common/libnetwork/resolvconf
github.com/containers/common/libnetwork/slirp4netns
github.com/containers/common/libnetwork/types