Skip to content

Commit 32077c5

Browse files
authored
Merge pull request containerd#4255 from fahedouch/clean-up-iptables-in-oci-post-hook
fix: clean up iptables rules for stopped containers
2 parents 7eab2ce + e9ac446 commit 32077c5

File tree

2 files changed

+178
-1
lines changed

2 files changed

+178
-1
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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+
"strconv"
22+
"testing"
23+
"time"
24+
25+
"github.com/containerd/nerdctl/mod/tigron/expect"
26+
"github.com/containerd/nerdctl/mod/tigron/require"
27+
"github.com/containerd/nerdctl/mod/tigron/test"
28+
29+
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
30+
"github.com/containerd/nerdctl/v2/pkg/testutil"
31+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
32+
"github.com/containerd/nerdctl/v2/pkg/testutil/portlock"
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 container ID from the label
43+
containerID := data.Labels().Get("containerID")
44+
45+
// Remove the container
46+
helpers.Ensure("rm", "-f", containerID)
47+
48+
time.Sleep(1 * time.Second)
49+
50+
// Create a TestableCommand using helpers.Custom
51+
if rootlessutil.IsRootless() {
52+
// In rootless mode, we need to enter the rootlesskit network namespace
53+
if netns, err := rootlessutil.DetachedNetNS(); err != nil {
54+
t.Fatalf("Failed to get detached network namespace: %v", err)
55+
} else {
56+
if netns != "" {
57+
// Use containerd-rootless-setuptool.sh to enter the RootlessKit namespace
58+
return helpers.Custom("containerd-rootless-setuptool.sh", "nsenter", "--", "nsenter", "--net="+netns, "sh", "-ec", iptablesCheckCommand)
59+
}
60+
// Enter into :RootlessKit namespace using containerd-rootless-setuptool.sh
61+
return helpers.Custom("containerd-rootless-setuptool.sh", "nsenter", "--", "sh", "-ec", iptablesCheckCommand)
62+
}
63+
}
64+
65+
// In non-rootless mode, check iptables rules directly on the host
66+
return helpers.Custom("sh", "-ec", iptablesCheckCommand)
67+
}
68+
69+
// TestContainerRmIptables tests that iptables rules are cleared after container deletion
70+
func TestContainerRmIptables(t *testing.T) {
71+
testCase := nerdtest.Setup()
72+
73+
// Require iptables and containerd-rootless-setuptool.sh commands to be available
74+
testCase.Require = require.All(
75+
require.Binary("iptables"),
76+
require.Binary("containerd-rootless-setuptool.sh"),
77+
require.Not(require.Windows),
78+
require.Not(nerdtest.Docker),
79+
)
80+
81+
testCase.SubTests = []*test.Case{
82+
{
83+
Description: "Test iptables rules are cleared after container deletion",
84+
Setup: func(data test.Data, helpers test.Helpers) {
85+
// Get a free port using portlock
86+
port, err := portlock.Acquire(0)
87+
if err != nil {
88+
helpers.T().Fatalf("Failed to acquire port: %v", err)
89+
}
90+
data.Labels().Set("port", strconv.Itoa(port))
91+
92+
// Create a container with port mapping to ensure iptables rules are created
93+
containerID := helpers.Capture("run", "-d", "--name", data.Identifier(), "-p", fmt.Sprintf("%d:80", port), testutil.NginxAlpineImage)
94+
data.Labels().Set("containerID", containerID)
95+
nerdtest.EnsureContainerStarted(helpers, data.Identifier())
96+
},
97+
Cleanup: func(data test.Data, helpers test.Helpers) {
98+
// Make sure container is removed even if test fails
99+
helpers.Anyhow("rm", "-f", data.Identifier())
100+
101+
// Release the acquired port
102+
if portStr := data.Labels().Get("port"); portStr != "" {
103+
port, _ := strconv.Atoi(portStr)
104+
_ = portlock.Release(port)
105+
}
106+
},
107+
Command: testContainerRmIptablesExecutor,
108+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
109+
// Get the container ID from the label
110+
containerID := data.Labels().Get("containerID")
111+
return &test.Expected{
112+
ExitCode: expect.ExitCodeSuccess,
113+
// Verify that the iptables output does not contain the container ID
114+
Output: expect.DoesNotContain(containerID),
115+
}
116+
},
117+
},
118+
}
119+
120+
testCase.Run(t)
121+
}

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
@@ -475,10 +476,19 @@ func applyNetworkSettings(opts *handlerOpts) error {
475476
// See https://github.com/containerd/nerdctl/issues/3355
476477
_ = opts.cni.Remove(ctx, opts.fullID, "", namespaceOpts...)
477478

479+
// Defer CNI configuration removal to ensure idempotency of oci-hook.
480+
defer func() {
481+
if err != nil {
482+
log.L.Warn("Container failed starting. Removing allocated network configuration.")
483+
_ = opts.cni.Remove(ctx, opts.fullID, nsPath, namespaceOpts...)
484+
}
485+
}()
486+
478487
cniRes, err := opts.cni.Setup(ctx, opts.fullID, nsPath, namespaceOpts...)
479488
if err != nil {
480489
return fmt.Errorf("failed to call cni.Setup: %w", err)
481490
}
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)