Skip to content

Commit 99d989e

Browse files
committed
fix: improve network settings application and enhance iptables rule deletion
Signed-off-by: fahed dorgaa <[email protected]>
1 parent aee274f commit 99d989e

File tree

2 files changed

+184
-1
lines changed

2 files changed

+184
-1
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package container
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"strconv"
23+
"testing"
24+
"time"
25+
26+
"github.com/containerd/nerdctl/mod/tigron/expect"
27+
"github.com/containerd/nerdctl/mod/tigron/require"
28+
"github.com/containerd/nerdctl/mod/tigron/test"
29+
30+
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
31+
"github.com/containerd/nerdctl/v2/pkg/testutil"
32+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
33+
)
34+
35+
// iptablesCheckCommand is the shell command to check iptables rules
36+
const iptablesCheckCommand = "iptables -t nat -S && iptables -t filter -S && iptables -t mangle -S"
37+
38+
// testContainerRmIptablesExecutor is a common executor function for testing iptables rules cleanup
39+
func testContainerRmIptablesExecutor(data test.Data, helpers test.Helpers) test.TestableCommand {
40+
t := helpers.T()
41+
42+
// Get the actual container ID from the container name and store it in a label
43+
containerID := data.Labels().Get("containerID")
44+
if containerID == "" {
45+
container := nerdtest.InspectContainer(helpers, data.Identifier())
46+
containerID = container.ID
47+
data.Labels().Set("containerID", containerID)
48+
}
49+
50+
// Remove the container
51+
helpers.Ensure("rm", "-f", containerID)
52+
53+
time.Sleep(1 * time.Second)
54+
55+
// Create a TestableCommand using helpers.Custom
56+
if rootlessutil.IsRootless() {
57+
// In rootless mode, we need to enter the rootlesskit network namespace
58+
stateDir, err := rootlessutil.RootlessKitStateDir()
59+
if err != nil {
60+
t.Fatalf("Failed to get rootlesskit state dir: %v", err)
61+
}
62+
63+
childPid, err := rootlessutil.RootlessKitChildPid(stateDir)
64+
if err != nil {
65+
t.Fatalf("Failed to get rootlesskit child pid: %v", err)
66+
}
67+
68+
// Construct the path to the network namespace
69+
uid := os.Getuid()
70+
netnsPath := fmt.Sprintf("/run/user/%d/containerd-rootless/netns", uid)
71+
72+
if netns, err := rootlessutil.DetachedNetNS(); err != nil {
73+
t.Fatalf("Failed to get detached network namespace: %v", err)
74+
} else {
75+
if netns != "" {
76+
// First enter the user and mount namespace, then the network namespace
77+
return helpers.Custom("nsenter", "-t", strconv.Itoa(childPid), "-U", "-m", "--preserve-credentials",
78+
"--", "sh", "-c", fmt.Sprintf("nsenter --net=%s sh -c '%s'", netnsPath, iptablesCheckCommand))
79+
}
80+
// Enter into RootlessKit namespace using containerd-rootless-setuptool.sh
81+
return helpers.Custom("containerd-rootless-setuptool.sh", "nsenter", "--", "sh", "-c", iptablesCheckCommand)
82+
}
83+
}
84+
85+
// In non-rootless mode, check iptables rules directly on the host
86+
return helpers.Custom("sh", "-c", iptablesCheckCommand)
87+
}
88+
89+
// TestContainerRmIptables tests that iptables rules are cleared after container deletion
90+
func TestContainerRmIptables(t *testing.T) {
91+
testCase := nerdtest.Setup()
92+
93+
// Require iptables and containerd-rootless-setuptool.sh commands to be available
94+
testCase.Require = require.All(
95+
require.Binary("iptables"),
96+
require.Binary("containerd-rootless-setuptool.sh"),
97+
require.Not(require.Windows),
98+
require.Not(nerdtest.Docker),
99+
)
100+
101+
testCase.SubTests = []*test.Case{
102+
{
103+
Description: "Test iptables rules are cleared after container deletion",
104+
Setup: func(data test.Data, helpers test.Helpers) {
105+
// Create a container with port mapping to ensure iptables rules are created
106+
helpers.Ensure("run", "-d", "--name", data.Identifier(), "-p", "8080:80", testutil.NginxAlpineImage)
107+
nerdtest.EnsureContainerStarted(helpers, identifier)
108+
},
109+
Cleanup: func(data test.Data, helpers test.Helpers) {
110+
// Make sure container is removed even if test fails
111+
helpers.Anyhow("rm", "-f", data.Identifier())
112+
},
113+
Command: testContainerRmIptablesExecutor,
114+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
115+
// Get the container ID from the label
116+
containerID := data.Labels().Get("containerID")
117+
return &test.Expected{
118+
ExitCode: expect.ExitCodeSuccess,
119+
// Verify that the iptables output does not contain the container ID
120+
Output: expect.DoesNotContain(containerID),
121+
}
122+
},
123+
},
124+
}
125+
126+
testCase.Run(t)
127+
}

pkg/ocihook/ocihook.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"io"
2525
"net"
2626
"os"
27+
"os/exec"
2728
"path/filepath"
2829
"strings"
2930
"time"
@@ -418,7 +419,7 @@ func getIP6AddressOpts(opts *handlerOpts) ([]cni.NamespaceOpts, error) {
418419
return nil, nil
419420
}
420421

421-
func applyNetworkSettings(opts *handlerOpts) error {
422+
func applyNetworkSettings(opts *handlerOpts) (err error) {
422423
portMapOpts, err := getPortMapOpts(opts)
423424
if err != nil {
424425
return err
@@ -479,6 +480,15 @@ func applyNetworkSettings(opts *handlerOpts) error {
479480
if err != nil {
480481
return fmt.Errorf("failed to call cni.Setup: %w", err)
481482
}
483+
484+
// Defer CNI configuration removal to ensure idempotency of oci-hook.
485+
defer func() {
486+
if err != nil {
487+
log.L.Warn("Container failed starting. Removing allocated network configuration.")
488+
_ = opts.cni.Remove(ctx, opts.fullID, nsPath, namespaceOpts...)
489+
}
490+
}()
491+
482492
cniResRaw := cniRes.Raw()
483493
for i, cniName := range opts.cniNames {
484494
hsMeta.Networks[cniName] = cniResRaw[i]
@@ -622,6 +632,15 @@ func onPostStop(opts *handlerOpts) error {
622632
log.L.WithError(err).Errorf("failed to call cni.Remove")
623633
return err
624634
}
635+
636+
// opts.cni.Remove has trouble removing network configurations when netns is empty.
637+
// Therefore, we force the deletion of iptables rules here to prevent netns exhaustion.
638+
// This is a workaround until https://github.com/containernetworking/plugins/pull/1078 is merged.
639+
if err := cleanupIptablesRules(opts.fullID); err != nil {
640+
log.L.WithError(err).Warnf("failed to clean up iptables rules for container %s", opts.fullID)
641+
// Don't return error here, continue with the rest of the cleanup
642+
}
643+
625644
hs, err := hostsstore.New(opts.dataStore, ns)
626645
if err != nil {
627646
return err
@@ -642,6 +661,43 @@ func onPostStop(opts *handlerOpts) error {
642661
return nil
643662
}
644663

664+
// cleanupIptablesRules cleans up iptables rules related to the container
665+
func cleanupIptablesRules(containerID string) error {
666+
// Check if iptables command exists
667+
if _, err := exec.LookPath("iptables"); err != nil {
668+
return fmt.Errorf("iptables command not found: %w", err)
669+
}
670+
671+
// Tables to check for rules
672+
tables := []string{"nat", "filter", "mangle"}
673+
674+
for _, table := range tables {
675+
// Get all iptables rules for this table
676+
cmd := exec.Command("iptables", "-t", table, "-S")
677+
output, err := cmd.CombinedOutput()
678+
if err != nil {
679+
log.L.WithError(err).Warnf("failed to list iptables rules for table %s", table)
680+
continue
681+
}
682+
683+
// Find and delete rules related to the container
684+
rules := strings.Split(string(output), "\n")
685+
for _, rule := range rules {
686+
if strings.Contains(rule, containerID) {
687+
// Execute delete command
688+
deleteCmd := exec.Command("sh", "-c", "--", fmt.Sprintf(`iptables -t %s -D %s`, table, rule[3:]))
689+
if deleteOutput, err := deleteCmd.CombinedOutput(); err != nil {
690+
log.L.WithError(err).Warnf("failed to delete iptables rule: %s, output: %s", rule, string(deleteOutput))
691+
} else {
692+
log.L.Debugf("deleted iptables rule: %s", rule)
693+
}
694+
}
695+
}
696+
}
697+
698+
return nil
699+
}
700+
645701
// writePidFile writes the pid atomically to a file.
646702
// From https://github.com/containerd/containerd/blob/v1.7.0-rc.2/cmd/ctr/commands/commands.go#L265-L282
647703
func writePidFile(path string, pid int) error {

0 commit comments

Comments
 (0)