diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index d764b82965..7084813af8 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -5,8 +5,10 @@ package libpod import ( "fmt" "net" + "os" "strings" + "github.com/moby/sys/capability" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" @@ -17,6 +19,31 @@ import ( "go.podman.io/podman/v6/pkg/rootless" ) +func hasCapNetAdmin() (bool, error) { + currentCaps, err := capability.NewPid2(0) + if err != nil { + return false, err + } + if err = currentCaps.Load(); err != nil { + return false, err + } + return currentCaps.Get(capability.EFFECTIVE, capability.CAP_NET_ADMIN), nil +} + +func rootfulBridgeNetworkPreflight(hasNetAdmin func() (bool, error)) error { + // UID 0 inside a delegated container environment should use rootful + // networking semantics; fail clearly if the environment lacks the + // capability needed to configure bridge networking. + hasCap, err := hasNetAdmin() + if err != nil { + return fmt.Errorf("checking CAP_NET_ADMIN for rootful netavark bridge networking: %w", err) + } + if !hasCap { + return fmt.Errorf("rootful netavark bridge networking requires CAP_NET_ADMIN in the current environment; Podman is running as root but this capability is not available") + } + return nil +} + // Create and configure a new network namespace for a container func (r *Runtime) configureNetNS(ctr *Container, ctrNS string) (status map[string]types.StatusBlock, rerr error) { if err := r.exposeMachinePorts(ctr.config.PortMappings); err != nil { @@ -45,6 +72,11 @@ func (r *Runtime) configureNetNS(ctr *Container, ctrNS string) (status map[strin if len(networks) == 0 { return nil, nil } + if os.Geteuid() == 0 && !rootless.IsRootless() { + if err := rootfulBridgeNetworkPreflight(hasCapNetAdmin); err != nil { + return nil, err + } + } netOpts := ctr.getNetworkOptions(networks) netStatus, err := r.setUpNetwork(ctrNS, netOpts) diff --git a/libpod/networking_linux_test.go b/libpod/networking_linux_test.go index 91ea188e01..f3aa103ac8 100644 --- a/libpod/networking_linux_test.go +++ b/libpod/networking_linux_test.go @@ -3,8 +3,10 @@ package libpod import ( + "errors" "net" "reflect" + "strings" "testing" "go.podman.io/common/libnetwork/types" @@ -212,3 +214,51 @@ func Test_resultToBasicNetworkConfig(t *testing.T) { }) } } + +func TestRootfulBridgeNetworkPreflight(t *testing.T) { + t.Parallel() + + testErr := errors.New("capability read failed") + testCases := []struct { + name string + capNetAdmin bool + capErr error + expectErr string + }{ + { + name: "root with CAP_NET_ADMIN is allowed", + capNetAdmin: true, + }, + { + name: "root without CAP_NET_ADMIN fails", + expectErr: "requires CAP_NET_ADMIN", + }, + { + name: "root capability check error fails", + capErr: testErr, + expectErr: "checking CAP_NET_ADMIN", + }, + } + + for _, tcl := range testCases { + tc := tcl + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := rootfulBridgeNetworkPreflight(func() (bool, error) { + return tc.capNetAdmin, tc.capErr + }) + if tc.expectErr == "" { + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + return + } + if err == nil { + t.Fatalf("expected error containing %q", tc.expectErr) + } + if !strings.Contains(err.Error(), tc.expectErr) { + t.Fatalf("expected error containing %q, got %q", tc.expectErr, err.Error()) + } + }) + } +}