Skip to content

Commit 06bc982

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

File tree

2 files changed

+164
-1
lines changed

2 files changed

+164
-1
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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/exec"
22+
"strings"
23+
"testing"
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+
)
33+
34+
// TestContainerRmIptables tests that iptables rules are cleared after container deletion
35+
func TestContainerRmIptables(t *testing.T) {
36+
// Check if iptables command exists
37+
if _, err := exec.LookPath("iptables"); err != nil {
38+
t.Skip("iptables command not found, skipping test")
39+
}
40+
41+
testCase := nerdtest.Setup()
42+
43+
// This test requires Linux
44+
testCase.Require = require.Not(require.Windows)
45+
46+
testCase.SubTests = []*test.Case{
47+
{
48+
Description: "Test iptables rules are cleared after container deletion",
49+
Setup: func(data test.Data, helpers test.Helpers) {
50+
// Create a container with port mapping to ensure iptables rules are created
51+
helpers.Ensure("run", "-d", "--name", data.Identifier(), "-p", "8080:80", testutil.NginxAlpineImage)
52+
},
53+
Cleanup: func(data test.Data, helpers test.Helpers) {
54+
// Make sure container is removed even if test fails
55+
helpers.Anyhow("rm", "-f", data.Identifier())
56+
},
57+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
58+
containerID := data.Identifier()
59+
60+
// Check if iptables rules exist for the container in all tables
61+
tables := []string{"nat", "filter", "mangle"}
62+
rulesFound := false
63+
64+
for _, table := range tables {
65+
iptablesCmd := exec.Command("iptables", "-t", table, "-S")
66+
output, err := iptablesCmd.CombinedOutput()
67+
if err != nil {
68+
t.Logf("Warning: Failed to list iptables rules for table %s: %v", table, err)
69+
continue
70+
}
71+
72+
// Check if rules exist for the container
73+
iptablesOutput := string(output)
74+
if strings.Contains(iptablesOutput, containerID) {
75+
rulesFound = true
76+
t.Logf("Found iptables rules for container %s in table %s", containerID, table)
77+
break
78+
}
79+
}
80+
81+
if !rulesFound {
82+
t.Fatalf("Expected iptables rules for container %s, but none found", containerID)
83+
}
84+
85+
// Remove the container
86+
helpers.Ensure("rm", "-f", containerID)
87+
88+
// Check all tables to ensure rules are cleared
89+
cmd := helpers.Command("run", "--rm", testutil.AlpineImage, "sh", "-c",
90+
fmt.Sprintf(`
91+
for table in nat filter mangle; do
92+
if iptables -t $table -S | grep -q %s; then
93+
echo "iptables rules still exist in table $table"
94+
exit 1
95+
fi
96+
done
97+
echo "iptables rules cleared"
98+
`, containerID))
99+
100+
return cmd
101+
},
102+
Expected: test.Expects(0, nil, expect.Contains("iptables rules cleared")),
103+
},
104+
}
105+
106+
testCase.Run(t)
107+
}

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"
@@ -416,7 +417,7 @@ func getIP6AddressOpts(opts *handlerOpts) ([]cni.NamespaceOpts, error) {
416417
return nil, nil
417418
}
418419

419-
func applyNetworkSettings(opts *handlerOpts) error {
420+
func applyNetworkSettings(opts *handlerOpts) (err error) {
420421
portMapOpts, err := getPortMapOpts(opts)
421422
if err != nil {
422423
return err
@@ -477,6 +478,15 @@ func applyNetworkSettings(opts *handlerOpts) error {
477478
if err != nil {
478479
return fmt.Errorf("failed to call cni.Setup: %w", err)
479480
}
481+
482+
// Defer CNI configuration removal to ensure idempotency of oci-hook.
483+
defer func() {
484+
if err != nil {
485+
log.L.Warn("Container failed starting. Removing allocated network configuration.")
486+
_ = opts.cni.Remove(ctx, opts.fullID, nsPath, namespaceOpts...)
487+
}
488+
}()
489+
480490
cniResRaw := cniRes.Raw()
481491
for i, cniName := range opts.cniNames {
482492
hsMeta.Networks[cniName] = cniResRaw[i]
@@ -620,6 +630,15 @@ func onPostStop(opts *handlerOpts) error {
620630
log.L.WithError(err).Errorf("failed to call cni.Remove")
621631
return err
622632
}
633+
634+
// opts.cni.Remove has trouble removing network configurations when netns is empty.
635+
// Therefore, we force the deletion of iptables rules here to prevent netns exhaustion.
636+
// This is a workaround until https://github.com/containernetworking/plugins/pull/1078 is merged.
637+
if err := cleanupIptablesRules(opts.fullID); err != nil {
638+
log.L.WithError(err).Warnf("failed to clean up iptables rules for container %s", opts.fullID)
639+
// Don't return error here, continue with the rest of the cleanup
640+
}
641+
623642
hs, err := hostsstore.New(opts.dataStore, ns)
624643
if err != nil {
625644
return err
@@ -640,6 +659,43 @@ func onPostStop(opts *handlerOpts) error {
640659
return nil
641660
}
642661

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

0 commit comments

Comments
 (0)