From 1c47025ca9df4bd9a1387149f490f9b00c6b9a3d Mon Sep 17 00:00:00 2001 From: Steve Date: Sat, 15 Aug 2026 09:55:53 +0100 Subject: [PATCH] fix(k8s): fall back to legacy iptables Probe the nftables backend during node preparation and select the legacy iptables alternatives when the node kernel cannot use nftables. Apply the MSS rules through the selected system backend. Fixes #2120 --- .../Support/K8sHelper+Bootstrap.swift | 20 +++- .../K8sNodePreparationTests.swift | 109 ++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 Tests/K8sPluginTests/K8sNodePreparationTests.swift diff --git a/Sources/ContainerK8s/Support/K8sHelper+Bootstrap.swift b/Sources/ContainerK8s/Support/K8sHelper+Bootstrap.swift index 15512dc63..12e0079fd 100644 --- a/Sources/ContainerK8s/Support/K8sHelper+Bootstrap.swift +++ b/Sources/ContainerK8s/Support/K8sHelper+Bootstrap.swift @@ -139,8 +139,24 @@ extension K8sHelper { sysctl -w net.bridge.bridge-nf-call-ip6tables=1 2>/dev/null || true systemctl restart containerd ctr -n k8s.io images tag registry.k8s.io/pause:3.10 registry.k8s.io/pause:3.10.1 2>/dev/null || true - /usr/sbin/iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1220 - /usr/sbin/iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1220 + \(iptablesSetupScript()) + """ + } + + static func iptablesSetupScript( + nftPath: String = "/usr/sbin/iptables-nft", + legacyPath: String = "/usr/sbin/iptables-legacy", + ip6LegacyPath: String = "/usr/sbin/ip6tables-legacy", + updateAlternativesPath: String = "/usr/bin/update-alternatives", + iptablesPath: String = "/usr/sbin/iptables" + ) -> String { + """ + if ! \(nftPath) -t mangle -S >/dev/null 2>&1; then + \(updateAlternativesPath) --set iptables \(legacyPath) + \(updateAlternativesPath) --set ip6tables \(ip6LegacyPath) + fi + \(iptablesPath) -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1220 + \(iptablesPath) -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1220 """ } diff --git a/Tests/K8sPluginTests/K8sNodePreparationTests.swift b/Tests/K8sPluginTests/K8sNodePreparationTests.swift new file mode 100644 index 000000000..2f5d4820e --- /dev/null +++ b/Tests/K8sPluginTests/K8sNodePreparationTests.swift @@ -0,0 +1,109 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the container project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation +import Testing + +@testable import ContainerK8s + +struct K8sNodePreparationTests { + @Test + func fallsBackToLegacyIptablesWhenNftablesIsUnavailable() throws { + let fixture = try ShellFixture(nftExitCode: 1) + defer { fixture.remove() } + + let result = try fixture.run() + + #expect(result.status == 0) + #expect(result.log.contains("update-alternatives --set iptables \(fixture.legacyPath.path)")) + #expect(result.log.contains("update-alternatives --set ip6tables \(fixture.ip6LegacyPath.path)")) + #expect(result.log.contains("iptables -t mangle -A OUTPUT")) + #expect(result.log.contains("iptables -t mangle -A FORWARD")) + } + + @Test + func keepsNftablesWhenItIsAvailable() throws { + let fixture = try ShellFixture(nftExitCode: 0) + defer { fixture.remove() } + + let result = try fixture.run() + + #expect(result.status == 0) + #expect(!result.log.contains("update-alternatives")) + #expect(result.log.contains("iptables -t mangle -A OUTPUT")) + #expect(result.log.contains("iptables -t mangle -A FORWARD")) + } +} + +private struct ShellFixture { + let root: URL + let logPath: URL + let nftPath: URL + let legacyPath: URL + let ip6LegacyPath: URL + let updateAlternativesPath: URL + let iptablesPath: URL + + init(nftExitCode: Int32) throws { + root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + logPath = root.appendingPathComponent("calls.log") + nftPath = root.appendingPathComponent("iptables-nft") + legacyPath = root.appendingPathComponent("iptables-legacy") + ip6LegacyPath = root.appendingPathComponent("ip6tables-legacy") + updateAlternativesPath = root.appendingPathComponent("update-alternatives") + iptablesPath = root.appendingPathComponent("iptables") + + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) + try writeExecutable(nftPath, body: "exit \(nftExitCode)") + try writeExecutable( + updateAlternativesPath, + body: "printf 'update-alternatives %s\\n' \"$*\" >> \"$CALL_LOG\"" + ) + try writeExecutable( + iptablesPath, + body: "printf 'iptables %s\\n' \"$*\" >> \"$CALL_LOG\"" + ) + } + + func run() throws -> (status: Int32, log: String) { + let process = Process() + process.executableURL = URL(fileURLWithPath: "/bin/sh") + process.arguments = [ + "-c", + K8sHelper.iptablesSetupScript( + nftPath: nftPath.path, + legacyPath: legacyPath.path, + ip6LegacyPath: ip6LegacyPath.path, + updateAlternativesPath: updateAlternativesPath.path, + iptablesPath: iptablesPath.path + ), + ] + process.environment = ["CALL_LOG": logPath.path] + try process.run() + process.waitUntilExit() + let log = (try? String(contentsOf: logPath, encoding: .utf8)) ?? "" + return (process.terminationStatus, log) + } + + func remove() { + try? FileManager.default.removeItem(at: root) + } + + private func writeExecutable(_ path: URL, body: String) throws { + try "#!/bin/sh\n\(body)\n".write(to: path, atomically: true, encoding: .utf8) + try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path) + } +}