Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 50 additions & 5 deletions cni-plugin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"

Expand All @@ -34,6 +36,7 @@ import (
"github.com/linkerd/linkerd2-proxy-init/proxy-init/cmd"

"github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
Expand Down Expand Up @@ -80,9 +83,13 @@ type PluginConf struct {
RawPrevResult *map[string]interface{} `json:"prevResult"`
PrevResult *cniv1.Result `json:"-"`

LogLevel string `json:"log_level"`
ProxyInit ProxyInit `json:"linkerd"`
Kubernetes Kubernetes `json:"kubernetes"`
LogLevel string `json:"log_level"`
LogFile string `json:"log_file"`
LogFileMaxSizeMB int `json:"log_file_max_size_mb"`
LogFileMaxAgeDays int `json:"log_file_max_age_days"`
LogFileMaxCount int `json:"log_file_max_count"`
ProxyInit ProxyInit `json:"linkerd"`
Kubernetes Kubernetes `json:"kubernetes"`
}

func main() {
Expand All @@ -99,7 +106,10 @@ func main() {
)
}

func configureLoggingLevel(logLevel string) {
// configureLogging sets log level and configures outputs to both stderr and a file with rotation.
// If logFilePath is empty, a sensible default is used.
// maxSizeMB, maxAgeDays, and maxCount configure the log rotation behavior.
func configureLogging(logLevel, logFilePath string, maxSizeMB, maxAgeDays, maxCount int) {
switch strings.ToLower(logLevel) {
case "debug":
logrus.SetLevel(logrus.DebugLevel)
Expand All @@ -108,6 +118,40 @@ func configureLoggingLevel(logLevel string) {
default:
logrus.SetLevel(logrus.WarnLevel)
}

// Default log file path
if strings.TrimSpace(logFilePath) == "" {
logFilePath = "/var/log/linkerd-cni-plugin.log"
}

// Default rotation parameters if not specified (matching Calico CNI defaults)
if maxSizeMB <= 0 {
maxSizeMB = 100 // 100 MB default
}
if maxAgeDays <= 0 {
maxAgeDays = 30 // 30 days default
}
if maxCount <= 0 {
maxCount = 10 // 10 files default
}

// Ensure directory exists
if dir := filepath.Dir(logFilePath); dir != "" && dir != "." {
_ = os.MkdirAll(dir, 0o755)
}

// Configure log rotation with lumberjack
logger := &lumberjack.Logger{
Filename: logFilePath,
MaxSize: maxSizeMB, // megabytes
MaxBackups: maxCount, // number of backups
MaxAge: maxAgeDays, // days
Compress: true, // compress rotated files
}

// Tee logs to both stderr and the rotating log file
mw := io.MultiWriter(os.Stderr, logger)
logrus.SetOutput(mw)
}

// parseConfig parses the supplied configuration (and prevResult) from stdin.
Expand Down Expand Up @@ -147,7 +191,8 @@ func cmdAdd(args *skel.CmdArgs) error {
logrus.Errorf("error parsing config: %e", err)
return err
}
configureLoggingLevel(conf.LogLevel)
// Configure logging level and outputs with rotation
configureLogging(conf.LogLevel, conf.LogFile, conf.LogFileMaxSizeMB, conf.LogFileMaxAgeDays, conf.LogFileMaxCount)

if conf.PrevResult != nil {
logrus.WithFields(logrus.Fields{
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ require (
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
66 changes: 64 additions & 2 deletions pkg/iptables/iptables.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ type FirewallConfiguration struct {
// https://github.com/istio/istio/blob/e83411e/pilot/docker/prepare_proxy.sh
func ConfigureFirewall(firewallConfiguration FirewallConfiguration) error {
log.Debugf("tracing script execution as [%s]", executionTraceID)
log.Debugf("using '%s' to set-up firewall rules", firewallConfiguration.BinPath)
log.Debugf("using '%s' to list all available rules", firewallConfiguration.SaveBinPath)

// Before executing, ensure the configured iptables binaries exist; if not, attempt a fallback.
resolveBinFallback(&firewallConfiguration, exec.LookPath)

existingRules, err := executeCommand(firewallConfiguration, firewallConfiguration.makeShowAllRules())
if err != nil {
Expand Down Expand Up @@ -112,6 +113,9 @@ func CleanupFirewallConfig(firewallConfiguration FirewallConfiguration) error {
log.Debugf("using '%s' to clean-up firewall rules", firewallConfiguration.BinPath)
log.Debugf("using '%s' to list all available rules", firewallConfiguration.SaveBinPath)

// Ensure binaries exist before attempting cleanup as well
resolveBinFallback(&firewallConfiguration, exec.LookPath)

commands := make([]*exec.Cmd, 0)
commands = firewallConfiguration.cleanupRules(commands)

Expand Down Expand Up @@ -448,3 +452,61 @@ func asDestination(portRange util.PortRange) string {

return fmt.Sprintf("%d:%d", portRange.LowerBound, portRange.UpperBound)
}

// resolveBinFallback ensures the configured BinPath and SaveBinPath exist on PATH; if not, it
// tries reasonable alternatives of the same family (ip6tables vs iptables).
func resolveBinFallback(fc *FirewallConfiguration, lookPath func(string) (string, error)) {
// helper to check presence
has := func(name string) bool {
_, err := lookPath(name)
return err == nil
}

// Both present? nothing to do
if has(fc.BinPath) && has(fc.SaveBinPath) {
log.WithFields(log.Fields{
"requestedBin": fc.BinPath,
"requestedSaveBin": fc.SaveBinPath,
}).Debug("iptables: using configured binaries")
return
}

// Decide family based on current name
ipv6 := strings.Contains(fc.BinPath, "ip6tables") || strings.Contains(fc.SaveBinPath, "ip6tables")

// Candidate orders: prefer nft, then plain, then legacy
var candidates [][2]string
if ipv6 {
candidates = [][2]string{
{"ip6tables-nft", "ip6tables-nft-save"},
{"ip6tables", "ip6tables-save"},
{"ip6tables-legacy", "ip6tables-legacy-save"},
}
} else {
candidates = [][2]string{
{"iptables-nft", "iptables-nft-save"},
{"iptables", "iptables-save"},
{"iptables-legacy", "iptables-legacy-save"},
}
}

// Use first candidate where both exist
for _, pair := range candidates {
if has(pair[0]) && has(pair[1]) {
if pair[0] != fc.BinPath || pair[1] != fc.SaveBinPath {
log.WithFields(log.Fields{
"requestedBin": fc.BinPath,
"requestedSaveBin": fc.SaveBinPath,
"fallbackBin": pair[0],
"fallbackSaveBin": pair[1],
}).Warn("iptables: configured binaries not found; applying fallback to available binaries")
}
fc.BinPath = pair[0]
fc.SaveBinPath = pair[1]
return
}
}

// No candidates found; keep as-is and let execution fail with a clear error later
log.WithFields(log.Fields{"binPath": fc.BinPath, "saveBinPath": fc.SaveBinPath}).Error("iptables: no suitable binaries found on PATH; commands may fail")
}
89 changes: 89 additions & 0 deletions pkg/iptables/resolve_fallback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package iptables

import (
"errors"
"testing"
)

// fakeLookPath returns a LookPath-like function backed by a set of available names.
func fakeLookPath(available []string) func(string) (string, error) {
return func(name string) (string, error) {
for _, a := range available {
if a == name {
return "/fake/" + name, nil
}
}
return "", errors.New("not found")
}
}

func TestResolveBinFallback_KeepWhenPresent(t *testing.T) {
fc := &FirewallConfiguration{BinPath: "iptables-nft", SaveBinPath: "iptables-nft-save"}
lp := fakeLookPath([]string{
"iptables-nft",
"iptables-nft-save",
})

resolveBinFallback(fc, lp)

if fc.BinPath != "iptables-nft" || fc.SaveBinPath != "iptables-nft-save" {
t.Fatalf("expected to keep configured binaries, got bin=%q save=%q", fc.BinPath, fc.SaveBinPath)
}
}

func TestResolveBinFallback_FallbackToNFT_IPv4(t *testing.T) {
fc := &FirewallConfiguration{BinPath: "iptables-notreal", SaveBinPath: "iptables-notreal-save"}
lp := fakeLookPath([]string{
// Only nft pair is available
"iptables-nft",
"iptables-nft-save",
})

resolveBinFallback(fc, lp)

if fc.BinPath != "iptables-nft" || fc.SaveBinPath != "iptables-nft-save" {
t.Fatalf("expected fallback to iptables-nft, got bin=%q save=%q", fc.BinPath, fc.SaveBinPath)
}
}

func TestResolveBinFallback_FallbackOrder_Plain(t *testing.T) {
fc := &FirewallConfiguration{BinPath: "iptables-missing", SaveBinPath: "iptables-missing-save"}
lp := fakeLookPath([]string{
// Only plain iptables present
"iptables",
"iptables-save",
})

resolveBinFallback(fc, lp)

if fc.BinPath != "iptables" || fc.SaveBinPath != "iptables-save" {
t.Fatalf("expected fallback to iptables/iptable-save, got bin=%q save=%q", fc.BinPath, fc.SaveBinPath)
}
}

func TestResolveBinFallback_IPv6_FallbackLegacy(t *testing.T) {
fc := &FirewallConfiguration{BinPath: "ip6tables-missing", SaveBinPath: "ip6tables-missing-save"}
lp := fakeLookPath([]string{
// Only legacy pair present for IPv6
"ip6tables-legacy",
"ip6tables-legacy-save",
})

resolveBinFallback(fc, lp)

if fc.BinPath != "ip6tables-legacy" || fc.SaveBinPath != "ip6tables-legacy-save" {
t.Fatalf("expected fallback to ip6tables-legacy, got bin=%q save=%q", fc.BinPath, fc.SaveBinPath)
}
}

func TestResolveBinFallback_NoCandidates(t *testing.T) {
origBin, origSave := "iptables-missing", "iptables-missing-save"
fc := &FirewallConfiguration{BinPath: origBin, SaveBinPath: origSave}
lp := fakeLookPath([]string{})

resolveBinFallback(fc, lp)

if fc.BinPath != origBin || fc.SaveBinPath != origSave {
t.Fatalf("expected no change when no candidates found, got bin=%q save=%q", fc.BinPath, fc.SaveBinPath)
}
}
Loading